Before You Begin
You should have:- An existing Meteor app on a separate development computer (you can view the example "Todo List" app here; instructions are provided later in the tutorial)
- A fresh Ubuntu 14.04 server; existing Meteor installations should work in most cases
- root access to the server to execute commands
- Updated package lists. Execute:apt-get update
- Replace todos.net with the domain name you are actually using (or leave it if you don't have a domain and will be using an IP address instead)
- Replace todos (without .net) with the name of your application
Step 1 — Setting Up an Nginx Web Server
We will install and set up Nginx because it allows us to encrypt web traffic with SSL, a feature that Meteor's built-in web server does not provide. Nginx will also let us serve other websites on the same server, and filter and log traffic. In our configuration, we will secure our site with an SSL certificate and redirect all traffic from HTTP to HTTPS. We will also utilize a few new security practices to enhance the security of the SSL connection. In order to install Nginx we execute:apt-get install nginx
Create a virtual host configuration file in /etc/nginx/sites-available
.
Below is an annotated config file which we can create as /etc/nginx/sites-available/todos
with the following contents. Explanations for all of the configuration settings are included in the comments in the file:
server_tokens off; # for security-by-obscurity: stop displaying nginx version
# this section is needed to proxy web-socket connections
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# HTTP
server {
listen 80 default_server; # if this is not a default server, remove "default_server"
listen [::]:80 default_server ipv6only=on;
root /usr/share/nginx/html; # root is irrelevant
index index.html index.htm; # this is also irrelevant
server_name todos.net; # the domain on which we want to host the application. Since we set "default_server" previously, nginx will answer all hosts anyway.
# redirect non-SSL to SSL
location / {
rewrite ^ https://$server_name$request_uri? permanent;
}
}
# HTTPS server
server {
listen 443 ssl spdy; # we enable SPDY here
server_name todos.net; # this domain must match Common Name (CN) in the SSL certificate
root html; # irrelevant
index index.html; # irrelevant
ssl_certificate /etc/nginx/ssl/todos.pem; # full path to SSL certificate and CA certificate concatenated together
ssl_certificate_key /etc/nginx/ssl/todos.key; # full path to SSL key
# performance enhancement for SSL
ssl_stapling on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 5m;
# safety enhancement to SSL: make sure we actually use a safe cipher
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-RC4-SHA:RC4-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK';
# config to enable HSTS(HTTP Strict Transport Security) https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security
# to avoid ssl stripping https://en.wikipedia.org/wiki/SSL_stripping#SSL_stripping
add_header Strict-Transport-Security "max-age=31536000;";
# If your application is not compatible with IE <= 10, this will redirect visitors to a page advising a browser update
# This works because IE 11 does not present itself as MSIE anymore
if ($http_user_agent ~ "MSIE" ) {
return 303 https://browser-update.org/update.html;
}
# pass all requests to Meteor
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # allow websockets
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $remote_addr; # preserve client IP
# this setting allows the browser to cache the application in a way compatible with Meteor
# on every applicaiton update the name of CSS and JS file is different, so they can be cache infinitely (here: 30 days)
# the root path (/) MUST NOT be cached
if ($uri != '/') {
expires 30d;
}
}
}
If you'd like to adapt the configuration file to your needs, and for more explanation, have a look at this tutorial on Nginx virtual hosts.
As seen in the virtual host config file, Nginx will expect a valid SSL certificate and key in /etc/nginx/ssl
. We need to create this directory and secure it:
mkdir /etc/nginx/ssl
chmod 0700 /etc/nginx/ssl
Then we can create the files containing the certificate (and chain certificate if required) and the key in the locations we defined in the configuration above:
- certificate:
/etc/nginx/ssl/todos.pem
- key:
/etc/nginx/ssl/todos.key
todos.pem
and todos.key
files mentioned above.
Next, we should disable the default vhost:
rm /etc/nginx/sites-enabled/default
And enable our Meteor vhost:
ln -s /etc/nginx/sites-available/todos /etc/nginx/sites-enabled/todos
Test that the vhost configuration is error free (you will see an error related to ssl_stapling if you have a self-signed certificate; this is okay):
nginx -t
If everything is looking good we can apply the changes to Nginx:
nginx -s reload
At this point, you can use your web browser to visit https://todos.net (or your IP address). It will show us 502 Bad Gateway. That is OK, because we don't have Meteor running yet!
Step Two — Setting Up a MongoDB Database
We will install MongoDB from the regular Ubuntu repository. The standard configuration should be fine. There is no authentication required to connect to the database, but connections are only possible from localhost. This means that no external connections are possible, and thus the database is safe, as long as we don't have untrusted users with SSH access to the system. Install the MongoDB server package:apt-get install mongodb-server
This is everything we need to do to get MongoDB running. To be sure that access from external hosts is not possible, we execute the following to be sure that MongoDB is bound to 127.0.0.1. Check with this command:
netstat -ln | grep -E '27017|28017'
Expected output:
tcp 0 0 127.0.0.1:27017 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:28017 0.0.0.0:* LISTEN
unix 2 [ ACC ] STREAM LISTENING 6091441 /tmp/mongodb-27017.sock
In order to have daily backups available in case something goes wrong, we can optionally install a simple command as a daily cron job. Create a file /etc/cron.d/mongodb-backup
:
@daily root mkdir -p /var/backups/mongodb; mongodump --db todos --out /var/backups/mongodb/$(date +'\%Y-\%m-\%d')
Step 3 — Installing the Meteor Application
First, we need to install Node.js. Since Meteor typically requires a version of Node.js newer than what's available in the standard repository, we will use a custom PPA (at the time of writing, Ubuntu 14.04 provides nodejs=0.10.25~dfsg2-2ubuntu1 while Meteor 0.8.3 requires Node.js 0.10.29 or newer). Execute the following to add a PPA with Node.js, and confirm by pressing Enter:add-apt-repository ppa:chris-lea/node.js
Output:
Evented I/O for V8 javascript. Node's goal is to provide an easy way to build scalable network programs
More info: https://launchpad.net/~chris-lea/+archive/ubuntu/node.js
Press [ENTER] to continue or ctrl-c to cancel adding it
gpg: keyring `/tmp/tmphsbizg3u/secring.gpg' created
gpg: keyring `/tmp/tmphsbizg3u/pubring.gpg' created
gpg: requesting key C7917B12 from hkp server keyserver.ubuntu.com
gpg: /tmp/tmphsbizg3u/trustdb.gpg: trustdb created
gpg: key C7917B12: public key "Launchpad chrislea" imported
gpg: Total number processed: 1
gpg: imported: 1 (RSA: 1)
OK
Now we must refresh the repository cache, and then we can install Node.js and npm (Node.js package manager):
apt-get update
apt-get install nodejs
It's a good practice to run our Meteor application as a regular user. Therefore, we will create a new system user specifically for that purpose:
adduser --disabled-login todos
Output:
Adding user `todos' ...
Adding new group `todos' (1001) ...
Adding new user `todos' (1001) with group `todos' ...
Creating home directory `/home/todos' ...
Copying files from `/etc/skel' ...
Changing the user information for todos
Enter the new value, or press ENTER for the default
Full Name []:
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n]
Step Four — Configuring Upstart
Now we are ready to create the Upstart service to manage our Meteor app. Upstart will automatically start the app on boot and restart Meteor in case it dies. You can read more about creating Upstart service files in this tutorial. Create the file/etc/init/todos.conf
. Once again, it is annotated in-line:
# upstart service file at /etc/init/todos.conf
description "Meteor.js (NodeJS) application"
author "Daniel Speichert <[email protected]>"
# When to start the service
start on started mongodb and runlevel [2345]
# When to stop the service
stop on shutdown
# Automatically restart process if crashed
respawn
respawn limit 10 5
# we don't use buil-in log because we use a script below
# console log
# drop root proviliges and switch to mymetorapp user
setuid todos
setgid todos
script
export PATH=/opt/local/bin:/opt/local/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export NODE_PATH=/usr/lib/nodejs:/usr/lib/node_modules:/usr/share/javascript
# set to home directory of the user Meteor will be running as
export PWD=/home/todos
export HOME=/home/todos
# leave as 127.0.0.1 for security
export BIND_IP=127.0.0.1
# the port nginx is proxying requests to
export PORT=8080
# this allows Meteor to figure out correct IP address of visitors
export HTTP_FORWARDED_COUNT=1
# MongoDB connection string using todos as database name
export MONGO_URL=mongodb://localhost:27017/todos
# The domain name as configured previously as server_name in nginx
export ROOT_URL=https://todos.net
# optional JSON config - the contents of file specified by passing "--settings" parameter to meteor command in development mode
export METEOR_SETTINGS='{ "somesetting": "someval", "public": { "othersetting": "anothervalue" } }'
# this is optional: http://docs.meteor.com/#email
# commented out will default to no email being sent
# you must register with MailGun to have a username and password there
# export MAIL_URL=smtp://[email protected]:[email protected]
# alternatively install "apt-get install default-mta" and uncomment:
# export MAIL_URL=smtp://localhost
exec node /home/todos/bundle/main.js >> /home/todos/todos.log
end script
One thing to notice in this configuration file is the METEOR_SETTINGS
parameter. If you use meteor --settings config.json
when launching Meteor's development mode, then you should paste the contents of config.json
as a variable in METEOR_SETTINGS
.
The MAIL_URL
must be a valid SMTP URL only if you plan to use Meteor's Email package. You can use MailGun (as recommended by Meteor), a local mail server, etc.
As we can see in the file, the log will be saved to /home/todos/todos.log.
This file will not rotate and WILL GROW over time. It's a good idea to keep an eye on it. Ideally, there should not be a lot of content (errors) in it. Optionally, you can set up log rotation or replace >>
with >
at the end of Upstart scripts in order to overwrite the whole file instead of appending to the end of it.
Don't start this service yet as we don't have the actual Meteor application files in place yet!
Step Five — Deploying the Meteor Application
Optional: If you do not have a Meteor project yet If you don't have a Meteor project yet and would like to use a demo app, that's not a problem! Do this next step on your home computer or a development Linux server. Commands may vary based on your OS. Move to your home folder:cd ~
First, install Meteor's development version:
curl https://install.meteor.com | /bin/sh
Then create an application from an example, called Todo List:
meteor create --example todos
Now enter the directory of your application and you are ready to continue:
cd todos
All Meteor projects
It's time to create a production-version bundle from our Meteor app. The following commands should be executed on your home computer or development Linux server, wherever your Meteor application lives. Go to your project directory:
cd /app/dir
And execute:
meteor build .
This will create an archive file like todos.tar.gz
in the directory /app/dir
. Copy this file to your ~
directory on your Droplet.
scp todos.tar.gz root@todos.net:~
Now go back to your Droplet. Create a project directory and move the archive project file into it. Note that this is the home folder for the project user that we created previously, not your root home folder:
mkdir /home/todos
mv todos.tar.gz /home/todos
Move into the project directory and unpack it:
cd /home/todos
tar -zxf todos.tar.gz
Take a look at the project README:
cat /home/todos/bundle/README
The bundle includes a README
file with contents:
This is a Meteor application bundle. It has only one external dependency:
Node.js 0.10.29 or newer. To run the application:
$ (cd programs/server && npm install)
$ export MONGO_URL='mongodb://user:password@host:port/databasename'
$ export ROOT_URL='http://example.com'
$ export MAIL_URL='smtp://user:password@mailhost:port/'
$ node main.js
Use the PORT environment variable to set the port where the
application will listen. The default is 80, but that will require
root on most systems.
Find out more about Meteor at meteor.com.
This recipe is reflected in our /etc/init/todos.conf
file. There is still one more thing mentioned in the README that we need to do.
Now we need to install some required npm modules. To be able to build some of them, we also need to install g++ and make:
apt-get install g++ make
cd /home/todos/bundle/programs/server
npm install
You should see output like this:
npm WARN package.json [email protected] No description
npm WARN package.json [email protected] No repository field.
npm WARN package.json [email protected] No README data
> [email protected] install /home/todos/bundle/programs/server/node_modules/fibers
> node ./build.js
`linux-x64-v8-3.14` exists; testing
Binary is fine; exiting
[email protected] node_modules/underscore
[email protected] node_modules/semver
[email protected] node_modules/source-map-support
└── [email protected] ([email protected])
[email protected] node_modules/fibers
The reason we need to do this is because our application bundle does not contain modules and libraries that are platform-dependent.
We are almost ready to run the application, but since we operated on files as root, and they should be owned by the todos
user, we need to update the ownership of the project directory:
chown todos:todos /home/todos -R
Step Six — Showtime
At this point we have everything we need to run our Meteor application:- Node.js environment installed
- Application installed in its project directory
- Upstart service configured to run the application
- MongoDB database
- Nginx proxy server in front of our Meteor application to provide SSL encryption
start todos
Now you should be able to view your application in the browser at https://todos.net.
Re-deploying the Application
When you make changes in the development mode (and you will; we are developers after all!), you can simply repeat Step Five (starting frommeteor build
) and go through most of the steps until the restart todos
command, which will reload your application via Upstart.
This way you can push a new version with no downtime. Clients (visitors to your website) will automatically pull the new version of code and refresh their page - that is Meteor's magic!
If you want to test this, you can make a simple change to the text in the todos/client/todos.html
page in your development copy of the app on your home computer or development server.
Development server:
Build:
meteor build /app/dir
Upload:
scp todos.tar.gz root@todos.net:/home/todos
Production server:
Expand:
tar -zxf /home/todos/todos.tar.gz
Move into the project folder:
cd /home/todos/bundle/programs/server
Update the npm modules (you may see a few warnings):
npm install
Restart the app:
restart todos
Troubleshooting
If something goes wrong, here are a few hints on where to look for problems:- Check
/home/todos/todos.log
if your application starts and dies; it should throw an appropriate error message (e.g. in case of a programming error). - Check
/var/log/nginx/error.log
if you see an HTTP error in stead of your application. - Check
/var/log/mongodb/mongodb.log
if you think there might a problem with the database.
status todos
service nginx status
status mongodb
Revisions
- May 16, 2018 @ 16:25:03 [Current Revision] by Sharing Solution
- May 16, 2018 @ 16:25:03 by Sharing Solution
- May 16, 2018 @ 16:24:44 by Sharing Solution
Revision Differences
May 16, 2018 @ 16:24:44 | Current Revision | ||
---|---|---|---|
Content | |||
Unchanged: <div class="section-content section-content-growable content Tutorial-content content-has-mobile-image"> | Unchanged: <div class="section-content section-content-growable content Tutorial-content content-has-mobile-image"> | ||
Unchanged: <div class="content-body tutorial-content" data-growable- markdown=""> | Unchanged: <div class="content-body tutorial-content" data-growable- markdown=""> | ||
Unchanged: <h2 id="before-you- begin">Before You Begin</h2> | Unchanged: <h2 id="before-you- begin">Before You Begin</h2> | ||
Unchanged: You should have: | Unchanged: You should have: | ||
Unchanged: <ul> | Unchanged: <ul> | ||
Unchanged: <li>An existing Meteor app on a separate development computer (you can view the example "Todo List" app <a href="https:/ /www.meteor.com/examples/ todos">here</a>; instructions are provided later in the tutorial)</li> | Unchanged: <li>An existing Meteor app on a separate development computer (you can view the example "Todo List" app <a href="https:/ /www.meteor.com/examples/ todos">here</a>; instructions are provided later in the tutorial)</li> | ||
Unchanged: <li>A fresh <strong>Ubuntu 14.04</strong> server; existing Meteor installations should work in most cases</li> | Unchanged: <li>A fresh <strong>Ubuntu 14.04</strong> server; existing Meteor installations should work in most cases</li> | ||
Unchanged: <li><strong>root< /strong> access to the server to execute commands</li> | Unchanged: <li><strong>root< /strong> access to the server to execute commands</li> | ||
Deleted: <li>Updated package lists. Execute: | Added: <li>Updated package lists. Execute:apt-get update</li> | ||
Deleted: apt-get update</li> | |||
Unchanged: <li>Replace <span class="highlight" >todos.net</span> with the domain name you are actually using (or leave it if you don't have a domain and will be using an IP address instead)</li> | Unchanged: <li>Replace <span class="highlight" >todos.net</span> with the domain name you are actually using (or leave it if you don't have a domain and will be using an IP address instead)</li> | ||
Unchanged: <li>Replace <span class="highlight" >todos</span> (without .net) with the name of your application</li> | Unchanged: <li>Replace <span class="highlight" >todos</span> (without .net) with the name of your application</li> | ||
Unchanged: </ul> | Unchanged: </ul> | ||
Unchanged: <div data-unique=" step-1-—-setting-up-an- nginx-web-server"></div> | Unchanged: <div data-unique=" step-1-—-setting-up-an- nginx-web-server"></div> | ||
Unchanged: <h2 id="step-1-—- setting-up-an- nginx-web-server">Step 1 — Setting Up an Nginx Web Server</h2> | Unchanged: <h2 id="step-1-—- setting-up-an- nginx-web-server">Step 1 — Setting Up an Nginx Web Server</h2> | ||
Unchanged: We will install and set up Nginx because it allows us to encrypt web traffic with SSL, a feature that Meteor's built-in web server does not provide. Nginx will also let us serve other websites on the same server, and filter and log traffic. | Unchanged: We will install and set up Nginx because it allows us to encrypt web traffic with SSL, a feature that Meteor's built-in web server does not provide. Nginx will also let us serve other websites on the same server, and filter and log traffic. | ||
Unchanged: In our configuration, we will secure our site with an SSL certificate and redirect all traffic from HTTP to HTTPS. We will also utilize a few new security practices to enhance the security of the SSL connection. | Unchanged: In our configuration, we will secure our site with an SSL certificate and redirect all traffic from HTTP to HTTPS. We will also utilize a few new security practices to enhance the security of the SSL connection. | ||
Unchanged: In order to install Nginx we execute: | Unchanged: In order to install Nginx we execute: | ||
Unchanged: <pre class="code-pre "><code>apt-get install nginx | Unchanged: <pre class="code-pre "><code>apt-get install nginx | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Create a virtual host configuration file in <code>/etc/ nginx/sites-available</code>. | Unchanged: Create a virtual host configuration file in <code>/etc/ nginx/sites-available</code>. | ||
Unchanged: Below is an annotated config file which we can create as <code>/etc/ nginx/sites-available/<span class="highlight" >todos</span> </code> with the following contents. Explanations for all of the configuration settings are included in the comments in the file: | Unchanged: Below is an annotated config file which we can create as <code>/etc/ nginx/sites-available/<span class="highlight" >todos</span> </code> with the following contents. Explanations for all of the configuration settings are included in the comments in the file: | ||
Unchanged: <pre class="code-pre "><code>server_tokens off; # for security-by-obscurity: stop displaying nginx version | Unchanged: <pre class="code-pre "><code>server_tokens off; # for security-by-obscurity: stop displaying nginx version | ||
Unchanged: # this section is needed to proxy web-socket connections | Unchanged: # this section is needed to proxy web-socket connections | ||
Unchanged: map $http_upgrade $connection_upgrade { | Unchanged: map $http_upgrade $connection_upgrade { | ||
Unchanged: default upgrade; | Unchanged: default upgrade; | ||
Unchanged: '' close; | Unchanged: '' close; | ||
Unchanged: } | Unchanged: } | ||
Unchanged: # HTTP | Unchanged: # HTTP | ||
Unchanged: server { | Unchanged: server { | ||
Unchanged: listen 80 default_server; # if this is not a default server, remove "default_server" | Unchanged: listen 80 default_server; # if this is not a default server, remove "default_server" | ||
Unchanged: listen [::]:80 default_server ipv6only=on; | Unchanged: listen [::]:80 default_server ipv6only=on; | ||
Unchanged: root /usr/share/nginx/html; # root is irrelevant | Unchanged: root /usr/share/nginx/html; # root is irrelevant | ||
Unchanged: index index.html index.htm; # this is also irrelevant | Unchanged: index index.html index.htm; # this is also irrelevant | ||
Unchanged: server_name <span class="highlight" >todos.net</span>; # the domain on which we want to host the application. Since we set "default_server" previously, nginx will answer all hosts anyway. | Unchanged: server_name <span class="highlight" >todos.net</span>; # the domain on which we want to host the application. Since we set "default_server" previously, nginx will answer all hosts anyway. | ||
Unchanged: # redirect non-SSL to SSL | Unchanged: # redirect non-SSL to SSL | ||
Unchanged: location / { | Unchanged: location / { | ||
Unchanged: rewrite ^ https://$server_ name$request_uri? permanent; | Unchanged: rewrite ^ https://$server_ name$request_uri? permanent; | ||
Unchanged: } | Unchanged: } | ||
Unchanged: } | Unchanged: } | ||
Unchanged: # HTTPS server | Unchanged: # HTTPS server | ||
Unchanged: server { | Unchanged: server { | ||
Unchanged: listen 443 ssl spdy; # we enable SPDY here | Unchanged: listen 443 ssl spdy; # we enable SPDY here | ||
Unchanged: server_name <span class="highlight" >todos.net</span>; # this domain must match Common Name (CN) in the SSL certificate | Unchanged: server_name <span class="highlight" >todos.net</span>; # this domain must match Common Name (CN) in the SSL certificate | ||
Unchanged: root html; # irrelevant | Unchanged: root html; # irrelevant | ||
Unchanged: index index.html; # irrelevant | Unchanged: index index.html; # irrelevant | ||
Unchanged: ssl_certificate /etc/nginx/ssl/<span class="highlight" >todos.pem</span>; # full path to SSL certificate and CA certificate concatenated together | Unchanged: ssl_certificate /etc/nginx/ssl/<span class="highlight" >todos.pem</span>; # full path to SSL certificate and CA certificate concatenated together | ||
Unchanged: ssl_certificate_key /etc/nginx/ssl/<span class="highlight" >todos.key</span>; # full path to SSL key | Unchanged: ssl_certificate_key /etc/nginx/ssl/<span class="highlight" >todos.key</span>; # full path to SSL key | ||
Unchanged: # performance enhancement for SSL | Unchanged: # performance enhancement for SSL | ||
Unchanged: ssl_stapling on; | Unchanged: ssl_stapling on; | ||
Unchanged: ssl_session_cache shared:SSL:10m; | Unchanged: ssl_session_cache shared:SSL:10m; | ||
Unchanged: ssl_session_timeout 5m; | Unchanged: ssl_session_timeout 5m; | ||
Unchanged: # safety enhancement to SSL: make sure we actually use a safe cipher | Unchanged: # safety enhancement to SSL: make sure we actually use a safe cipher | ||
Unchanged: ssl_prefer_server_ciphers on; | Unchanged: ssl_prefer_server_ciphers on; | ||
Unchanged: ssl_protocols TLSv1 TLSv1.1 TLSv1.2; | Unchanged: ssl_protocols TLSv1 TLSv1.1 TLSv1.2; | ||
Unchanged: ssl_ciphers 'ECDHE-RSA-AES128- GCM-SHA256:ECDHE- ECDSA-AES128- GCM-SHA256:ECDHE-RSA-AES256- GCM-SHA384:ECDHE- ECDSA-AES256- GCM-SHA384:kEDH+ AESGCM:ECDHE- RSA-AES128-SHA256: ECDHE-ECDSA- AES128-SHA256: ECDHE-RSA-AES128- SHA:ECDHE-ECDSA-AES128-SHA: ECDHE-RSA-AES256- SHA384:ECDHE- ECDSA-AES256- SHA384:ECDHE- RSA-AES256-SHA: ECDHE-ECDSA- AES256-SHA:DHE-RSA-AES128- SHA256:DHE-RSA-AES128-SHA: DHE-RSA-AES256-SHA256:DHE- DSS-AES256-SHA:AES128-GCM- SHA256:AES256-GCM-SHA384: ECDHE-RSA-RC4- SHA:ECDHE-ECDSA- RC4-SHA:RC4-SHA: HIGH:!aNULL: !eNULL:!EXPORT: !DES:!3DES:!MD5:!PSK'; | Unchanged: ssl_ciphers 'ECDHE-RSA-AES128- GCM-SHA256:ECDHE- ECDSA-AES128- GCM-SHA256:ECDHE-RSA-AES256- GCM-SHA384:ECDHE- ECDSA-AES256- GCM-SHA384:kEDH+ AESGCM:ECDHE- RSA-AES128-SHA256: ECDHE-ECDSA- AES128-SHA256: ECDHE-RSA-AES128- SHA:ECDHE-ECDSA-AES128-SHA: ECDHE-RSA-AES256- SHA384:ECDHE- ECDSA-AES256- SHA384:ECDHE- RSA-AES256-SHA: ECDHE-ECDSA- AES256-SHA:DHE-RSA-AES128- SHA256:DHE-RSA-AES128-SHA: DHE-RSA-AES256-SHA256:DHE- DSS-AES256-SHA:AES128-GCM- SHA256:AES256-GCM-SHA384: ECDHE-RSA-RC4- SHA:ECDHE-ECDSA- RC4-SHA:RC4-SHA: HIGH:!aNULL: !eNULL:!EXPORT: !DES:!3DES:!MD5:!PSK'; | ||
Unchanged: # config to enable HSTS(HTTP Strict Transport Security) https://developer.mozilla.org/ en-US/docs/Security/HTTP_ Strict_Transport_Security | Unchanged: # config to enable HSTS(HTTP Strict Transport Security) https://developer.mozilla.org/ en-US/docs/Security/HTTP_ Strict_Transport_Security | ||
Unchanged: # to avoid ssl stripping https://en.wikipedia.org/ wiki/SSL_stripping#SSL_stripping | Unchanged: # to avoid ssl stripping https://en.wikipedia.org/ wiki/SSL_stripping#SSL_stripping | ||
Unchanged: add_header Strict-Transport-Security "max-age=31536000;"; | Unchanged: add_header Strict-Transport-Security "max-age=31536000;"; | ||
Unchanged: # If your application is not compatible with IE <= 10, this will redirect visitors to a page advising a browser update | Unchanged: # If your application is not compatible with IE <= 10, this will redirect visitors to a page advising a browser update | ||
Unchanged: # This works because IE 11 does not present itself as MSIE anymore | Unchanged: # This works because IE 11 does not present itself as MSIE anymore | ||
Unchanged: if ($http_user_agent ~ "MSIE" ) { | Unchanged: if ($http_user_agent ~ "MSIE" ) { | ||
Unchanged: return 303 https://browser- update.org/update.html; | Unchanged: return 303 https://browser- update.org/update.html; | ||
Unchanged: } | Unchanged: } | ||
Unchanged: # pass all requests to Meteor | Unchanged: # pass all requests to Meteor | ||
Unchanged: location / { | Unchanged: location / { | ||
Unchanged: proxy_pass http://127.0.0.1:8080; | Unchanged: proxy_pass http://127.0.0.1:8080; | ||
Unchanged: proxy_http_version 1.1; | Unchanged: proxy_http_version 1.1; | ||
Unchanged: proxy_set_header Upgrade $http_upgrade; # allow websockets | Unchanged: proxy_set_header Upgrade $http_upgrade; # allow websockets | ||
Unchanged: proxy_set_header Connection $connection_upgrade; | Unchanged: proxy_set_header Connection $connection_upgrade; | ||
Unchanged: proxy_set_header X-Forwarded-For $remote_addr; # preserve client IP | Unchanged: proxy_set_header X-Forwarded-For $remote_addr; # preserve client IP | ||
Unchanged: # this setting allows the browser to cache the application in a way compatible with Meteor | Unchanged: # this setting allows the browser to cache the application in a way compatible with Meteor | ||
Unchanged: # on every applicaiton update the name of CSS and JS file is different, so they can be cache infinitely (here: 30 days) | Unchanged: # on every applicaiton update the name of CSS and JS file is different, so they can be cache infinitely (here: 30 days) | ||
Unchanged: # the root path (/) MUST NOT be cached | Unchanged: # the root path (/) MUST NOT be cached | ||
Unchanged: if ($uri != '/') { | Unchanged: if ($uri != '/') { | ||
Unchanged: expires 30d; | Unchanged: expires 30d; | ||
Unchanged: } | Unchanged: } | ||
Unchanged: } | Unchanged: } | ||
Unchanged: } | Unchanged: } | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: If you'd like to adapt the configuration file to your needs, and for more explanation, have a look at this tutorial on <a href="https:/ /www.digitalocean.com/community/ tutorials/how- to-set-up-nginx- server-blocks- virtual-hosts- on-ubuntu-14- 04-lts">Nginx virtual hosts</a>. | Unchanged: If you'd like to adapt the configuration file to your needs, and for more explanation, have a look at this tutorial on <a href="https:/ /www.digitalocean.com/community/ tutorials/how- to-set-up-nginx- server-blocks- virtual-hosts- on-ubuntu-14- 04-lts">Nginx virtual hosts</a>. | ||
Unchanged: As seen in the virtual host config file, Nginx will expect a valid SSL certificate and key in <code>/etc/ nginx/ssl</code>. We need to create this directory and secure it: | Unchanged: As seen in the virtual host config file, Nginx will expect a valid SSL certificate and key in <code>/etc/ nginx/ssl</code>. We need to create this directory and secure it: | ||
Unchanged: <pre class="code-pre "><code>mkdir /etc/nginx/ssl | Unchanged: <pre class="code-pre "><code>mkdir /etc/nginx/ssl | ||
Unchanged: chmod 0700 /etc/nginx/ssl | Unchanged: chmod 0700 /etc/nginx/ssl | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Then we can create the files containing the certificate (and chain certificate if required) and the key in the locations we defined in the configuration above: | Unchanged: Then we can create the files containing the certificate (and chain certificate if required) and the key in the locations we defined in the configuration above: | ||
Unchanged: <ul> | Unchanged: <ul> | ||
Unchanged: <li>certificate: <code>/etc/ nginx/ssl/<span class="highlight" >todos.pem</span> </code></li> | Unchanged: <li>certificate: <code>/etc/ nginx/ssl/<span class="highlight" >todos.pem</span> </code></li> | ||
Unchanged: <li>key: <code> /etc/nginx/ssl/<span class="highlight" >todos.key</span> </code></li> | Unchanged: <li>key: <code> /etc/nginx/ssl/<span class="highlight" >todos.key</span> </code></li> | ||
Unchanged: </ul> | Unchanged: </ul> | ||
Unchanged: If you do not have an SSL certificate and key already, you should now create a self-signed certificate using this <a href="https:/ /www.digitalocean.com/community/ tutorials/how- to-create-a- ssl-certificate-on-nginx- for-ubuntu-12- 04">tutorial on creating self-signed SSL certificates for Nginx</a>. Remember that you'll want to use the same names from the configuration file, like <strong> todos.key</strong> as the key name, and <strong> todos.pem</strong> as the certificate name. While a self-signed certificate is fine for testing, it is recommended to use a commercial, signed certificate for production use. A self-signed certificate will provoke Nginx warnings connected to ssl_stapling, and a security warning in the web browser. | Unchanged: If you do not have an SSL certificate and key already, you should now create a self-signed certificate using this <a href="https:/ /www.digitalocean.com/community/ tutorials/how- to-create-a- ssl-certificate-on-nginx- for-ubuntu-12- 04">tutorial on creating self-signed SSL certificates for Nginx</a>. Remember that you'll want to use the same names from the configuration file, like <strong> todos.key</strong> as the key name, and <strong> todos.pem</strong> as the certificate name. While a self-signed certificate is fine for testing, it is recommended to use a commercial, signed certificate for production use. A self-signed certificate will provoke Nginx warnings connected to ssl_stapling, and a security warning in the web browser. | ||
Unchanged: When you're done creating or obtaining your certificate, make sure you have the <code>todos.pem< /code> and < code>todos.key< /code> files mentioned above. | Unchanged: When you're done creating or obtaining your certificate, make sure you have the <code>todos.pem< /code> and < code>todos.key< /code> files mentioned above. | ||
Unchanged: Next, we should disable the default vhost: | Unchanged: Next, we should disable the default vhost: | ||
Unchanged: <pre class="code-pre "><code>rm /etc/nginx/sites- enabled/default | Unchanged: <pre class="code-pre "><code>rm /etc/nginx/sites- enabled/default | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: And enable our Meteor vhost: | Unchanged: And enable our Meteor vhost: | ||
Unchanged: <pre class="code-pre "><code>ln -s /etc/nginx/sites- available/<span class="highlight" >todos</span> /etc/nginx/sites- enabled/<span class="highlight" >todos</span> | Unchanged: <pre class="code-pre "><code>ln -s /etc/nginx/sites- available/<span class="highlight" >todos</span> /etc/nginx/sites- enabled/<span class="highlight" >todos</span> | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Test that the vhost configuration is error free (you will see an error related to ssl_stapling if you have a self-signed certificate; this is okay): | Unchanged: Test that the vhost configuration is error free (you will see an error related to ssl_stapling if you have a self-signed certificate; this is okay): | ||
Unchanged: <pre class="code-pre "><code>nginx -t | Unchanged: <pre class="code-pre "><code>nginx -t | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: If everything is looking good we can apply the changes to Nginx: | Unchanged: If everything is looking good we can apply the changes to Nginx: | ||
Unchanged: <pre class="code-pre "><code>nginx -s reload | Unchanged: <pre class="code-pre "><code>nginx -s reload | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: At this point, you can use your web browser to visit https://<span class="highlight" >todos.net</span> (or your IP address). It will show us <strong>502 Bad Gateway</strong>. That is OK, because we don't have Meteor running yet! | Unchanged: At this point, you can use your web browser to visit https://<span class="highlight" >todos.net</span> (or your IP address). It will show us <strong>502 Bad Gateway</strong>. That is OK, because we don't have Meteor running yet! | ||
Unchanged: <div data-unique=" step-two-—- setting-up-a- mongodb-database"></div> | Unchanged: <div data-unique=" step-two-—- setting-up-a- mongodb-database"></div> | ||
Unchanged: <h2 id="step-two- —-setting-up-a-mongodb- database">Step Two — Setting Up a MongoDB Database</h2> | Unchanged: <h2 id="step-two- —-setting-up-a-mongodb- database">Step Two — Setting Up a MongoDB Database</h2> | ||
Unchanged: We will install MongoDB from the regular Ubuntu repository. The standard configuration should be fine. There is no authentication required to connect to the database, but connections are only possible from localhost. This means that no external connections are possible, and thus the database is safe, as long as we don't have untrusted users with SSH access to the system. | Unchanged: We will install MongoDB from the regular Ubuntu repository. The standard configuration should be fine. There is no authentication required to connect to the database, but connections are only possible from localhost. This means that no external connections are possible, and thus the database is safe, as long as we don't have untrusted users with SSH access to the system. | ||
Unchanged: Install the MongoDB server package: | Unchanged: Install the MongoDB server package: | ||
Unchanged: <pre class="code-pre "><code>apt-get install mongodb-server | Unchanged: <pre class="code-pre "><code>apt-get install mongodb-server | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: This is everything we need to do to get MongoDB running. To be sure that access from external hosts is not possible, we execute the following to be sure that MongoDB is bound to <strong>127.0.0.1</strong>. Check with this command: | Unchanged: This is everything we need to do to get MongoDB running. To be sure that access from external hosts is not possible, we execute the following to be sure that MongoDB is bound to <strong>127.0.0.1</strong>. Check with this command: | ||
Unchanged: <pre class="code-pre "><code>netstat -ln | grep -E '27017|28017' | Unchanged: <pre class="code-pre "><code>netstat -ln | grep -E '27017|28017' | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Expected output: | Unchanged: Expected output: | ||
Unchanged: <pre class="code-pre "><code>tcp 0 0 127.0.0.1:27017 0.0.0.0:* LISTEN | Unchanged: <pre class="code-pre "><code>tcp 0 0 127.0.0.1:27017 0.0.0.0:* LISTEN | ||
Unchanged: tcp 0 0 127.0.0.1:28017 0.0.0.0:* LISTEN | Unchanged: tcp 0 0 127.0.0.1:28017 0.0.0.0:* LISTEN | ||
Unchanged: unix 2 [ ACC ] STREAM LISTENING 6091441 /tmp/mongodb-27017.sock | Unchanged: unix 2 [ ACC ] STREAM LISTENING 6091441 /tmp/mongodb-27017.sock | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: In order to have daily backups available in case something goes wrong, we can <strong> optionally</strong> install a simple command as a daily cron job. Create a file <code>/ etc/cron.d/mongodb- backup</code>: | Unchanged: In order to have daily backups available in case something goes wrong, we can <strong> optionally</strong> install a simple command as a daily cron job. Create a file <code>/ etc/cron.d/mongodb- backup</code>: | ||
Unchanged: <pre class="code-pre "><code>@daily root mkdir -p /var/backups/mongodb; mongodump --db <span class="highlight" >todos</span> --out /var/backups/ mongodb/$(date +'\%Y-\%m-\%d') | Unchanged: <pre class="code-pre "><code>@daily root mkdir -p /var/backups/mongodb; mongodump --db <span class="highlight" >todos</span> --out /var/backups/ mongodb/$(date +'\%Y-\%m-\%d') | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: <div data-unique=" step-3-—-installing-the- meteor-application"></div> | Unchanged: <div data-unique=" step-3-—-installing-the- meteor-application"></div> | ||
Unchanged: <h2 id="step-3-—- installing-the- meteor-application">Step 3 — Installing the Meteor Application</h2> | Unchanged: <h2 id="step-3-—- installing-the- meteor-application">Step 3 — Installing the Meteor Application</h2> | ||
Unchanged: First, we need to install Node.js. Since Meteor typically requires a version of Node.js newer than what's available in the standard repository, we will use a custom PPA <em>(at the time of writing, Ubuntu 14.04 provides nodejs=0.10.25~dfsg2-2ubuntu1 while Meteor 0.8.3 requires Node.js 0.10.29 or newer)</em>. | Unchanged: First, we need to install Node.js. Since Meteor typically requires a version of Node.js newer than what's available in the standard repository, we will use a custom PPA <em>(at the time of writing, Ubuntu 14.04 provides nodejs=0.10.25~dfsg2-2ubuntu1 while Meteor 0.8.3 requires Node.js 0.10.29 or newer)</em>. | ||
Unchanged: Execute the following to add a PPA with Node.js, and confirm by pressing Enter: | Unchanged: Execute the following to add a PPA with Node.js, and confirm by pressing Enter: | ||
Unchanged: <pre class="code-pre "><code>add-apt-repository ppa:chris-lea/node.js | Unchanged: <pre class="code-pre "><code>add-apt-repository ppa:chris-lea/node.js | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Output: | Unchanged: Output: | ||
Unchanged: <pre class="code-pre "><code> Evented I/O for V8 javascript. Node's goal is to provide an easy way to build scalable network programs | Unchanged: <pre class="code-pre "><code> Evented I/O for V8 javascript. Node's goal is to provide an easy way to build scalable network programs | ||
Unchanged: More info: https://launchpad.net/ ~chris-lea/+archive/ ubuntu/node.js | Unchanged: More info: https://launchpad.net/ ~chris-lea/+archive/ ubuntu/node.js | ||
Unchanged: Press [ENTER] to continue or ctrl-c to cancel adding it | Unchanged: Press [ENTER] to continue or ctrl-c to cancel adding it | ||
Unchanged: gpg: keyring `/tmp/tmphsbizg3u/ secring.gpg' created | Unchanged: gpg: keyring `/tmp/tmphsbizg3u/ secring.gpg' created | ||
Unchanged: gpg: keyring `/tmp/tmphsbizg3u/ pubring.gpg' created | Unchanged: gpg: keyring `/tmp/tmphsbizg3u/ pubring.gpg' created | ||
Unchanged: gpg: requesting key C7917B12 from hkp server keyserver.ubuntu.com | Unchanged: gpg: requesting key C7917B12 from hkp server keyserver.ubuntu.com | ||
Unchanged: gpg: /tmp/tmphsbizg3u/ trustdb.gpg: trustdb created | Unchanged: gpg: /tmp/tmphsbizg3u/ trustdb.gpg: trustdb created | ||
Unchanged: gpg: key C7917B12: public key "Launchpad chrislea" imported | Unchanged: gpg: key C7917B12: public key "Launchpad chrislea" imported | ||
Unchanged: gpg: Total number processed: 1 | Unchanged: gpg: Total number processed: 1 | ||
Unchanged: gpg: imported: 1 (RSA: 1) | Unchanged: gpg: imported: 1 (RSA: 1) | ||
Unchanged: OK | Unchanged: OK | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Now we must refresh the repository cache, and then we can install Node.js and npm (Node.js package manager): | Unchanged: Now we must refresh the repository cache, and then we can install Node.js and npm (Node.js package manager): | ||
Unchanged: <pre class="code-pre "><code>apt-get update | Unchanged: <pre class="code-pre "><code>apt-get update | ||
Unchanged: apt-get install nodejs | Unchanged: apt-get install nodejs | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: It's a good practice to run our Meteor application as a regular user. Therefore, we will create a new system user specifically for that purpose: | Unchanged: It's a good practice to run our Meteor application as a regular user. Therefore, we will create a new system user specifically for that purpose: | ||
Unchanged: <pre class="code-pre "><code>adduser --disabled-login <span class="highlight" >todos</span> | Unchanged: <pre class="code-pre "><code>adduser --disabled-login <span class="highlight" >todos</span> | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Output: | Unchanged: Output: | ||
Unchanged: <pre class="code-pre "><code>Adding user `todos' ... | Unchanged: <pre class="code-pre "><code>Adding user `todos' ... | ||
Unchanged: Adding new group `todos' (1001) ... | Unchanged: Adding new group `todos' (1001) ... | ||
Unchanged: Adding new user `todos' (1001) with group `todos' ... | Unchanged: Adding new user `todos' (1001) with group `todos' ... | ||
Unchanged: Creating home directory `/home/todos' ... | Unchanged: Creating home directory `/home/todos' ... | ||
Unchanged: Copying files from `/etc/skel' ... | Unchanged: Copying files from `/etc/skel' ... | ||
Unchanged: Changing the user information for todos | Unchanged: Changing the user information for todos | ||
Unchanged: Enter the new value, or press ENTER for the default | Unchanged: Enter the new value, or press ENTER for the default | ||
Unchanged: Full Name []: | Unchanged: Full Name []: | ||
Unchanged: Room Number []: | Unchanged: Room Number []: | ||
Unchanged: Work Phone []: | Unchanged: Work Phone []: | ||
Unchanged: Home Phone []: | Unchanged: Home Phone []: | ||
Unchanged: Other []: | Unchanged: Other []: | ||
Unchanged: Is the information correct? [Y/n] | Unchanged: Is the information correct? [Y/n] | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: <div data-unique=" step-four-—- configuring- upstart"></div> | Unchanged: <div data-unique=" step-four-—- configuring- upstart"></div> | ||
Unchanged: <h2 id="step-four- —-configuring- upstart">Step Four — Configuring Upstart</h2> | Unchanged: <h2 id="step-four- —-configuring- upstart">Step Four — Configuring Upstart</h2> | ||
Unchanged: Now we are ready to create the <a href="https:/ /www.digitalocean.com/community/ tutorials/the- upstart-event- system-what-it-is-and-how- to-use-it">Upstart< /a> service to manage our Meteor app. Upstart will automatically start the app on boot and restart Meteor in case it dies. You can read more about creating Upstart service files in <a href="https:/ /www.digitalocean.com/community/ tutorials/the- upstart-event- system-what-it-is-and-how- to-use-it">this tutorial</a>. | Unchanged: Now we are ready to create the <a href="https:/ /www.digitalocean.com/community/ tutorials/the- upstart-event- system-what-it-is-and-how- to-use-it">Upstart< /a> service to manage our Meteor app. Upstart will automatically start the app on boot and restart Meteor in case it dies. You can read more about creating Upstart service files in <a href="https:/ /www.digitalocean.com/community/ tutorials/the- upstart-event- system-what-it-is-and-how- to-use-it">this tutorial</a>. | ||
Unchanged: Create the file <code>/ etc/init/<span class="highlight" >todos</span> .conf</code>. Once again, it is annotated in-line: | Unchanged: Create the file <code>/ etc/init/<span class="highlight" >todos</span> .conf</code>. Once again, it is annotated in-line: | ||
Unchanged: <pre class="code-pre "><code># upstart service file at /etc/init/<span class="highlight" >todos.conf</span> | Unchanged: <pre class="code-pre "><code># upstart service file at /etc/init/<span class="highlight" >todos.conf</span> | ||
Unchanged: description "Meteor.js (NodeJS) application" | Unchanged: description "Meteor.js (NodeJS) application" | ||
Unchanged: author "Daniel Speichert <[email protected]>" | Unchanged: author "Daniel Speichert <[email protected]>" | ||
Unchanged: # When to start the service | Unchanged: # When to start the service | ||
Unchanged: start on started mongodb and runlevel [2345] | Unchanged: start on started mongodb and runlevel [2345] | ||
Unchanged: # When to stop the service | Unchanged: # When to stop the service | ||
Unchanged: stop on shutdown | Unchanged: stop on shutdown | ||
Unchanged: # Automatically restart process if crashed | Unchanged: # Automatically restart process if crashed | ||
Unchanged: respawn | Unchanged: respawn | ||
Unchanged: respawn limit 10 5 | Unchanged: respawn limit 10 5 | ||
Unchanged: # we don't use buil-in log because we use a script below | Unchanged: # we don't use buil-in log because we use a script below | ||
Unchanged: # console log | Unchanged: # console log | ||
Unchanged: # drop root proviliges and switch to mymetorapp user | Unchanged: # drop root proviliges and switch to mymetorapp user | ||
Unchanged: setuid <span class="highlight" >todos</span> | Unchanged: setuid <span class="highlight" >todos</span> | ||
Unchanged: setgid <span class="highlight" >todos</span> | Unchanged: setgid <span class="highlight" >todos</span> | ||
Unchanged: script | Unchanged: script | ||
Unchanged: export PATH=/opt/local/ bin:/opt/local/sbin:/usr/ local/sbin:/usr/local/bin: /usr/sbin:/usr/ bin:/sbin:/bin | Unchanged: export PATH=/opt/local/ bin:/opt/local/sbin:/usr/ local/sbin:/usr/local/bin: /usr/sbin:/usr/ bin:/sbin:/bin | ||
Unchanged: export NODE_PATH=/usr/ lib/nodejs:/usr/lib/node_ modules:/usr/ share/javascript | Unchanged: export NODE_PATH=/usr/ lib/nodejs:/usr/lib/node_ modules:/usr/ share/javascript | ||
Unchanged: # set to home directory of the user Meteor will be running as | Unchanged: # set to home directory of the user Meteor will be running as | ||
Unchanged: export PWD=/home/<span class="highlight" >todos</span> | Unchanged: export PWD=/home/<span class="highlight" >todos</span> | ||
Unchanged: export HOME=/home/<span class="highlight" >todos</span> | Unchanged: export HOME=/home/<span class="highlight" >todos</span> | ||
Unchanged: # leave as 127.0.0.1 for security | Unchanged: # leave as 127.0.0.1 for security | ||
Unchanged: export BIND_IP=127.0.0.1 | Unchanged: export BIND_IP=127.0.0.1 | ||
Unchanged: # the port nginx is proxying requests to | Unchanged: # the port nginx is proxying requests to | ||
Unchanged: export PORT=8080 | Unchanged: export PORT=8080 | ||
Unchanged: # this allows Meteor to figure out correct IP address of visitors | Unchanged: # this allows Meteor to figure out correct IP address of visitors | ||
Unchanged: export HTTP_FORWARDED_COUNT=1 | Unchanged: export HTTP_FORWARDED_COUNT=1 | ||
Unchanged: # MongoDB connection string using <span class="highlight" >todos</span> as database name | Unchanged: # MongoDB connection string using <span class="highlight" >todos</span> as database name | ||
Unchanged: export MONGO_URL=mongodb: //localhost:27017/<span class="highlight" >todos</span> | Unchanged: export MONGO_URL=mongodb: //localhost:27017/<span class="highlight" >todos</span> | ||
Unchanged: # The domain name as configured previously as server_name in nginx | Unchanged: # The domain name as configured previously as server_name in nginx | ||
Unchanged: export ROOT_URL=https://<span class="highlight" >todos.net</span> | Unchanged: export ROOT_URL=https://<span class="highlight" >todos.net</span> | ||
Unchanged: # optional JSON config - the contents of file specified by passing "--settings" parameter to meteor command in development mode | Unchanged: # optional JSON config - the contents of file specified by passing "--settings" parameter to meteor command in development mode | ||
Unchanged: export METEOR_SETTINGS='{ "somesetting": "someval", "public": { "othersetting": "anothervalue" } }' | Unchanged: export METEOR_SETTINGS='{ "somesetting": "someval", "public": { "othersetting": "anothervalue" } }' | ||
Unchanged: # this is optional: http://docs.meteor.com/#email | Unchanged: # this is optional: http://docs.meteor.com/#email | ||
Unchanged: # commented out will default to no email being sent | Unchanged: # commented out will default to no email being sent | ||
Unchanged: # you must register with MailGun to have a username and password there | Unchanged: # you must register with MailGun to have a username and password there | ||
Unchanged: # export MAIL_URL=smtp: //[email protected]: [email protected] | Unchanged: # export MAIL_URL=smtp: //[email protected]: [email protected] | ||
Unchanged: # alternatively install "apt-get install default-mta" and uncomment: | Unchanged: # alternatively install "apt-get install default-mta" and uncomment: | ||
Unchanged: # export MAIL_URL=smtp://localhost | Unchanged: # export MAIL_URL=smtp://localhost | ||
Unchanged: exec node /home/<span class="highlight" >todos</span> /bundle/main.js >> /home/<span class="highlight" >todos</span>/<span class="highlight" >todos</span>.log | Unchanged: exec node /home/<span class="highlight" >todos</span> /bundle/main.js >> /home/<span class="highlight" >todos</span>/<span class="highlight" >todos</span>.log | ||
Unchanged: end script | Unchanged: end script | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: One thing to notice in this configuration file is the <code>METEOR_ SETTINGS</code> parameter. If you use <code>meteor --settings config.json</code> when launching Meteor's development mode, then you should paste the contents of <code>config.json</code> as a variable in <code>METEOR_ SETTINGS</code>. | Unchanged: One thing to notice in this configuration file is the <code>METEOR_ SETTINGS</code> parameter. If you use <code>meteor --settings config.json</code> when launching Meteor's development mode, then you should paste the contents of <code>config.json</code> as a variable in <code>METEOR_ SETTINGS</code>. | ||
Unchanged: The <code>MAIL_ URL</code> must be a valid SMTP URL <strong> only</strong> if you plan to use Meteor's Email package. You can use MailGun (as recommended by Meteor), a local mail server, etc. | Unchanged: The <code>MAIL_ URL</code> must be a valid SMTP URL <strong> only</strong> if you plan to use Meteor's Email package. You can use MailGun (as recommended by Meteor), a local mail server, etc. | ||
Unchanged: As we can see in the file, the log will be saved to <code>/home/<span class="highlight" >todos</span>/<span class="highlight" >todos</span> .log.</code> This file will not rotate and <strong>WILL GROW</strong> over time. It's a good idea to keep an eye on it. Ideally, there should not be a lot of content (errors) in it. Optionally, you can set up <a href="https:/ /www.digitalocean.com/community/ tutorials/how- to-configure- logging-and-log-rotation- in-nginx-on-an- ubuntu-vps">log rotation</a> or replace <code> >></code> with <code> ></code> at the end of Upstart scripts in order to overwrite the whole file instead of appending to the end of it. | Unchanged: As we can see in the file, the log will be saved to <code>/home/<span class="highlight" >todos</span>/<span class="highlight" >todos</span> .log.</code> This file will not rotate and <strong>WILL GROW</strong> over time. It's a good idea to keep an eye on it. Ideally, there should not be a lot of content (errors) in it. Optionally, you can set up <a href="https:/ /www.digitalocean.com/community/ tutorials/how- to-configure- logging-and-log-rotation- in-nginx-on-an- ubuntu-vps">log rotation</a> or replace <code> >></code> with <code> ></code> at the end of Upstart scripts in order to overwrite the whole file instead of appending to the end of it. | ||
Unchanged: Don't start this service yet as we don't have the actual Meteor application files in place yet! | Unchanged: Don't start this service yet as we don't have the actual Meteor application files in place yet! | ||
Unchanged: <div data-unique=" step-five-—- deploying-the- meteor-application"></div> | Unchanged: <div data-unique=" step-five-—- deploying-the- meteor-application"></div> | ||
Unchanged: <h2 id="step-five- —-deploying-the-meteor- application">Step Five — Deploying the Meteor Application</h2> | Unchanged: <h2 id="step-five- —-deploying-the-meteor- application">Step Five — Deploying the Meteor Application</h2> | ||
Unchanged: <strong>Optional: If you do not have a Meteor project yet</strong> | Unchanged: <strong>Optional: If you do not have a Meteor project yet</strong> | ||
Unchanged: If you don't have a Meteor project yet and would like to use a demo app, that's not a problem! | Unchanged: If you don't have a Meteor project yet and would like to use a demo app, that's not a problem! | ||
Unchanged: <strong>Do this next step on your home computer or a development Linux server.</strong> Commands may vary based on your OS. Move to your home folder: | Unchanged: <strong>Do this next step on your home computer or a development Linux server.</strong> Commands may vary based on your OS. Move to your home folder: | ||
Unchanged: <pre class="code-pre "><code>cd ~ | Unchanged: <pre class="code-pre "><code>cd ~ | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: First, install Meteor's development version: | Unchanged: First, install Meteor's development version: | ||
Unchanged: <pre class="code-pre "><code>curl https://install.meteor.com | /bin/sh | Unchanged: <pre class="code-pre "><code>curl https://install.meteor.com | /bin/sh | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Then create an application from an example, called <a href="https:/ /www.meteor.com/ examples/todos">Todo List</a>: | Unchanged: Then create an application from an example, called <a href="https:/ /www.meteor.com/ examples/todos">Todo List</a>: | ||
Unchanged: <pre class="code-pre "><code>meteor create --example todos | Unchanged: <pre class="code-pre "><code>meteor create --example todos | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Now enter the directory of your application and you are ready to continue: | Unchanged: Now enter the directory of your application and you are ready to continue: | ||
Unchanged: <pre class="code-pre "><code>cd todos | Unchanged: <pre class="code-pre "><code>cd todos | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: <strong>All Meteor projects</strong> | Unchanged: <strong>All Meteor projects</strong> | ||
Unchanged: It's time to create a production-version bundle from our Meteor app. The following commands should be executed on your <strong>home computer or development Linux server</strong>, wherever your Meteor application lives. Go to your project directory: | Unchanged: It's time to create a production-version bundle from our Meteor app. The following commands should be executed on your <strong>home computer or development Linux server</strong>, wherever your Meteor application lives. Go to your project directory: | ||
Unchanged: <pre class="code-pre "><code>cd <span class="highlight" >/app/dir</span> | Unchanged: <pre class="code-pre "><code>cd <span class="highlight" >/app/dir</span> | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: And execute: | Unchanged: And execute: | ||
Unchanged: <pre class="code-pre "><code>meteor build . | Unchanged: <pre class="code-pre "><code>meteor build . | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: This will create an archive file like <code>todos.tar.gz</code> in the directory <code><span class="highlight" >/app/dir</span></code>. Copy this file to your <code>~< /code>directory on your Droplet. | Unchanged: This will create an archive file like <code>todos.tar.gz</code> in the directory <code><span class="highlight" >/app/dir</span></code>. Copy this file to your <code>~< /code>directory on your Droplet. | ||
Unchanged: <pre class="code-pre "><code>scp <span class="highlight" >todos</span>.tar.gz root@<span class="highlight" >todos.net</span>:~ | Unchanged: <pre class="code-pre "><code>scp <span class="highlight" >todos</span>.tar.gz root@<span class="highlight" >todos.net</span>:~ | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: <strong>Now go back to your Droplet.</strong> Create a project directory and move the archive project file into it. Note that this is the home folder for the project user that we created previously, < em>not</em> your root home folder: | Unchanged: <strong>Now go back to your Droplet.</strong> Create a project directory and move the archive project file into it. Note that this is the home folder for the project user that we created previously, < em>not</em> your root home folder: | ||
Unchanged: <pre class="code-pre "><code>mkdir /home/<span class="highlight" >todos</span> | Unchanged: <pre class="code-pre "><code>mkdir /home/<span class="highlight" >todos</span> | ||
Unchanged: mv <span class="highlight" >todos</span>.tar.gz /home/<span class="highlight" >todos</span> | Unchanged: mv <span class="highlight" >todos</span>.tar.gz /home/<span class="highlight" >todos</span> | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Move into the project directory and unpack it: | Unchanged: Move into the project directory and unpack it: | ||
Unchanged: <pre class="code-pre "><code>cd /home/<span class="highlight" >todos</span> | Unchanged: <pre class="code-pre "><code>cd /home/<span class="highlight" >todos</span> | ||
Unchanged: tar -zxf <span class="highlight" >todos</span>.tar.gz | Unchanged: tar -zxf <span class="highlight" >todos</span>.tar.gz | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Take a look at the project README: | Unchanged: Take a look at the project README: | ||
Unchanged: <pre class="code-pre "><code>cat /home/<span class="highlight" >todos</span> /bundle/README | Unchanged: <pre class="code-pre "><code>cat /home/<span class="highlight" >todos</span> /bundle/README | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: The bundle includes a <code>README< /code> file with contents: | Unchanged: The bundle includes a <code>README< /code> file with contents: | ||
Unchanged: <pre class="code-pre "><code>This is a Meteor application bundle. It has only one external dependency: | Unchanged: <pre class="code-pre "><code>This is a Meteor application bundle. It has only one external dependency: | ||
Unchanged: Node.js 0.10.29 or newer. To run the application: | Unchanged: Node.js 0.10.29 or newer. To run the application: | ||
Unchanged: $ (cd programs/server && npm install) | Unchanged: $ (cd programs/server && npm install) | ||
Unchanged: $ export MONGO_URL='mongodb: //user:password@host:port/ databasename' | Unchanged: $ export MONGO_URL='mongodb: //user:password@host:port/ databasename' | ||
Unchanged: $ export ROOT_URL='http: //example.com' | Unchanged: $ export ROOT_URL='http: //example.com' | ||
Unchanged: $ export MAIL_URL='smtp: //user:password@mailhost:port/' | Unchanged: $ export MAIL_URL='smtp: //user:password@mailhost:port/' | ||
Unchanged: $ node main.js | Unchanged: $ node main.js | ||
Unchanged: Use the PORT environment variable to set the port where the | Unchanged: Use the PORT environment variable to set the port where the | ||
Unchanged: application will listen. The default is 80, but that will require | Unchanged: application will listen. The default is 80, but that will require | ||
Unchanged: root on most systems. | Unchanged: root on most systems. | ||
Unchanged: Find out more about Meteor at meteor.com. | Unchanged: Find out more about Meteor at meteor.com. | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: This recipe is reflected in our <code>/etc/init/<span class="highlight" >todos</span> .conf</code> file. There is still one more thing mentioned in the README that we need to do. | Unchanged: This recipe is reflected in our <code>/etc/init/<span class="highlight" >todos</span> .conf</code> file. There is still one more thing mentioned in the README that we need to do. | ||
Unchanged: Now we need to install some required npm modules. To be able to build some of them, we also need to install g++ and make: | Unchanged: Now we need to install some required npm modules. To be able to build some of them, we also need to install g++ and make: | ||
Unchanged: <pre class="code-pre "><code>apt-get install g++ make | Unchanged: <pre class="code-pre "><code>apt-get install g++ make | ||
Unchanged: cd /home/<span class="highlight" >todos</span> /bundle/programs/server | Unchanged: cd /home/<span class="highlight" >todos</span> /bundle/programs/server | ||
Unchanged: npm install | Unchanged: npm install | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: You should see output like this: | Unchanged: You should see output like this: | ||
Unchanged: <pre class="code-pre "><code>npm WARN package.json [email protected] No description | Unchanged: <pre class="code-pre "><code>npm WARN package.json [email protected] No description | ||
Unchanged: npm WARN package.json [email protected] No repository field. | Unchanged: npm WARN package.json [email protected] No repository field. | ||
Unchanged: npm WARN package.json [email protected] No README data | Unchanged: npm WARN package.json [email protected] No README data | ||
Unchanged: > [email protected] install /home/todos/bundle/ programs/server/ node_modules/fibers | Unchanged: > [email protected] install /home/todos/bundle/ programs/server/ node_modules/fibers | ||
Unchanged: > node ./build.js | Unchanged: > node ./build.js | ||
Unchanged: `linux-x64-v8-3.14` exists; testing | Unchanged: `linux-x64-v8-3.14` exists; testing | ||
Unchanged: Binary is fine; exiting | Unchanged: Binary is fine; exiting | ||
Unchanged: [email protected] node_modules/underscore | Unchanged: [email protected] node_modules/underscore | ||
Unchanged: [email protected] node_modules/semver | Unchanged: [email protected] node_modules/semver | ||
Unchanged: [email protected] node_modules/ source-map-support | Unchanged: [email protected] node_modules/ source-map-support | ||
Unchanged: └── [email protected] ([email protected]) | Unchanged: └── [email protected] ([email protected]) | ||
Unchanged: [email protected] node_modules/fibers | Unchanged: [email protected] node_modules/fibers | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: The reason we need to do this is because our application bundle does not contain modules and libraries that are platform-dependent. | Unchanged: The reason we need to do this is because our application bundle does not contain modules and libraries that are platform-dependent. | ||
Unchanged: We are almost ready to run the application, but since we operated on files as root, and they should be owned by the <code><span class="highlight" >todos</span> </code> user, we need to update the ownership of the project directory: | Unchanged: We are almost ready to run the application, but since we operated on files as root, and they should be owned by the <code><span class="highlight" >todos</span> </code> user, we need to update the ownership of the project directory: | ||
Unchanged: <pre class="code-pre "><code>chown <span class="highlight" >todos</span>:<span class="highlight" >todos</span> /home/<span class="highlight" >todos</span> -R | Unchanged: <pre class="code-pre "><code>chown <span class="highlight" >todos</span>:<span class="highlight" >todos</span> /home/<span class="highlight" >todos</span> -R | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: <div data-unique=" step-six-—- showtime"></div> | Unchanged: <div data-unique=" step-six-—- showtime"></div> | ||
Unchanged: <h2 id="step-six- —-showtime">Step Six — Showtime</h2> | Unchanged: <h2 id="step-six- —-showtime">Step Six — Showtime</h2> | ||
Unchanged: At this point we have everything we need to run our Meteor application: | Unchanged: At this point we have everything we need to run our Meteor application: | ||
Unchanged: <ul> | Unchanged: <ul> | ||
Unchanged: <li>Node.js environment installed</li> | Unchanged: <li>Node.js environment installed</li> | ||
Unchanged: <li>Application installed in its project directory</li> | Unchanged: <li>Application installed in its project directory</li> | ||
Unchanged: <li>Upstart service configured to run the application</li> | Unchanged: <li>Upstart service configured to run the application</li> | ||
Unchanged: <li>MongoDB database</li> | Unchanged: <li>MongoDB database</li> | ||
Unchanged: <li>Nginx proxy server in front of our Meteor application to provide SSL encryption</li> | Unchanged: <li>Nginx proxy server in front of our Meteor application to provide SSL encryption</li> | ||
Unchanged: </ul> | Unchanged: </ul> | ||
Unchanged: To start our application, let's execute this command from the project directory: | Unchanged: To start our application, let's execute this command from the project directory: | ||
Unchanged: <pre class="code-pre "><code>start <span class="highlight" >todos</span> | Unchanged: <pre class="code-pre "><code>start <span class="highlight" >todos</span> | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Now you should be able to view your application in the browser at https://<span class="highlight" >todos.net</span>. | Unchanged: Now you should be able to view your application in the browser at https://<span class="highlight" >todos.net</span>. | ||
Unchanged: <div data-unique=" re-deploying- the-application"></div> | Unchanged: <div data-unique=" re-deploying- the-application"></div> | ||
Unchanged: <h2 id="re-deploying- the-application" >Re-deploying the Application</h2> | Unchanged: <h2 id="re-deploying- the-application" >Re-deploying the Application</h2> | ||
Unchanged: When you make changes in the development mode (and you will; we are developers after all!), you can simply repeat Step Five (starting from <code>meteor build</code>) and go through most of the steps until the <code>restart <span class="highlight" >todos</span> </code> command, which will reload your application via Upstart. | Unchanged: When you make changes in the development mode (and you will; we are developers after all!), you can simply repeat Step Five (starting from <code>meteor build</code>) and go through most of the steps until the <code>restart <span class="highlight" >todos</span> </code> command, which will reload your application via Upstart. | ||
Unchanged: This way you can push a new version with no downtime. Clients (visitors to your website) will automatically pull the new version of code and refresh their page - that is Meteor's magic! | Unchanged: This way you can push a new version with no downtime. Clients (visitors to your website) will automatically pull the new version of code and refresh their page - that is Meteor's magic! | ||
Unchanged: If you want to test this, you can make a simple change to the text in the <code>todos/ client/todos.html</code>page in your development copy of the app on your home computer or development server. | Unchanged: If you want to test this, you can make a simple change to the text in the <code>todos/ client/todos.html</code>page in your development copy of the app on your home computer or development server. | ||
Unchanged: <strong>Development server:</strong> | Unchanged: <strong>Development server:</strong> | ||
Unchanged: Build: | Unchanged: Build: | ||
Unchanged: <pre class="code-pre "><code>meteor build <span class="highlight" >/app/dir</span> | Unchanged: <pre class="code-pre "><code>meteor build <span class="highlight" >/app/dir</span> | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Upload: | Unchanged: Upload: | ||
Unchanged: <pre class="code-pre "><code>scp <span class="highlight" >todos</span>.tar.gz root@<span class="highlight" >todos.net</span> :/home/<span class="highlight" >todos</span> | Unchanged: <pre class="code-pre "><code>scp <span class="highlight" >todos</span>.tar.gz root@<span class="highlight" >todos.net</span> :/home/<span class="highlight" >todos</span> | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: <strong>Production server:</strong> | Unchanged: <strong>Production server:</strong> | ||
Unchanged: Expand: | Unchanged: Expand: | ||
Unchanged: <pre class="code-pre "><code>tar -zxf /home/<span class="highlight" >todos</span>/<span class="highlight" >todos</span>.tar.gz | Unchanged: <pre class="code-pre "><code>tar -zxf /home/<span class="highlight" >todos</span>/<span class="highlight" >todos</span>.tar.gz | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Move into the project folder: | Unchanged: Move into the project folder: | ||
Unchanged: <pre class="code-pre "><code>cd /home/<span class="highlight" >todos</span> /bundle/programs/server | Unchanged: <pre class="code-pre "><code>cd /home/<span class="highlight" >todos</span> /bundle/programs/server | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Update the npm modules (you may see a few warnings): | Unchanged: Update the npm modules (you may see a few warnings): | ||
Unchanged: <pre class="code-pre "><code>npm install | Unchanged: <pre class="code-pre "><code>npm install | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: Restart the app: | Unchanged: Restart the app: | ||
Unchanged: <pre class="code-pre "><code>restart <span class="highlight" >todos</span> | Unchanged: <pre class="code-pre "><code>restart <span class="highlight" >todos</span> | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: <h3 id="troubleshooting" >Troubleshooting</h3> | Unchanged: <h3 id="troubleshooting" >Troubleshooting</h3> | ||
Unchanged: If something goes wrong, here are a few hints on where to look for problems: | Unchanged: If something goes wrong, here are a few hints on where to look for problems: | ||
Unchanged: <ul> | Unchanged: <ul> | ||
Unchanged: <li>Check <code>/home/<span class="highlight" >todos</span>/<span class="highlight" >todos</span> .log</code> if your application starts and dies; it should throw an appropriate error message (e.g. in case of a programming error).</li> | Unchanged: <li>Check <code>/home/<span class="highlight" >todos</span>/<span class="highlight" >todos</span> .log</code> if your application starts and dies; it should throw an appropriate error message (e.g. in case of a programming error).</li> | ||
Unchanged: <li>Check <code> /var/log/nginx/ error.log</code> if you see an HTTP error in stead of your application.</li> | Unchanged: <li>Check <code> /var/log/nginx/ error.log</code> if you see an HTTP error in stead of your application.</li> | ||
Unchanged: <li>Check <code> /var/log/mongodb/ mongodb.log</code> if you think there might a problem with the database.</li> | Unchanged: <li>Check <code> /var/log/mongodb/ mongodb.log</code> if you think there might a problem with the database.</li> | ||
Unchanged: </ul> | Unchanged: </ul> | ||
Unchanged: Finally, check if all services are running: | Unchanged: Finally, check if all services are running: | ||
Unchanged: <pre class="code-pre "><code>status <span class="highlight" >todos</span> | Unchanged: <pre class="code-pre "><code>status <span class="highlight" >todos</span> | ||
Unchanged: service nginx status | Unchanged: service nginx status | ||
Unchanged: status mongodb | Unchanged: status mongodb | ||
Unchanged: </code></pre> | Unchanged: </code></pre> | ||
Unchanged: </div> | Unchanged: </div> | ||
Unchanged: </div> | Unchanged: </div> | ||
Unchanged: <div class="tutorial-footer"> | Unchanged: <div class="tutorial-footer"> | ||
Unchanged: <div class="tutorial- footer-details"> | Unchanged: <div class="tutorial- footer-details"> | ||
Unchanged: <div class="postable- info-bar-container"> | Unchanged: <div class="postable- info-bar-container"> | ||
Unchanged: <div class="postable-info-bar"> | Unchanged: <div class="postable-info-bar"> | ||
Unchanged: <div class="left-section"></div> | Unchanged: <div class="left-section"></div> | ||
Unchanged: </div> | Unchanged: </div> | ||
Unchanged: </div> | Unchanged: </div> | ||
Unchanged: </div> | Unchanged: </div> | ||
Unchanged: </div> | Unchanged: </div> |
Note: Spaces may be added to comparison text to allow better line wrapping.
No comments yet.