Tutorial: How to Deploy a Node.js App to DigitalOcean with SSL

Learn how to use DigitalOcean, Let’s Encrypt, and your big, sexy brain to deploy an SSL-enabled Node.js app for $5/month — in 30 minutes.

In this post, we’ll walk through the process, from start to finish, of creating a new server, deploying a Node.js app, securing it (for free!) with an SSL certificate, and pointing a domain name to it.

Watch the video above to see the whole process live — with clever commentary, of course — or jump to just the bits you need in the write-up below.

Prerequisites

  • A domain name that you can modify DNS records for.
  • A sense of adventure.

Set Up and Configure Your Server

Before we can do anything, we need a server that can be accessed publicly. There are lots of options out there, so don’t feel locked into DigitalOcean — however, for this tutorial it’ll probably be easiest to follow if you’re using exactly the same setup.

Create a new droplet on DigitalOcean.

To start create an account on DigitalOcean, or log into your existing account.

Once you’re logged in, click the “Create Droplet” button at the top of your screen.

The DigitalOcean dashboard

Choose the $5/month option with Ubuntu 16.04.1 x64. Select a region closest to your users.

Creating a $5/month DigitalOcean droplet with Ubuntu 16.04.1

Finally, add your SSH key and

How to find your SSH key

First, open Terminal1 and check for existing SSH keys:

    ls -la ~/.ssh

If you already have SSH keys set up, you should see a file called id_rsa.pub. (If there’s a file ending in .pub, it’s very likely an SSH key.)

To copy your SSH key, use one of the following commands:

# This copies the key so you can paste with command + V
pbcopy < ~/.ssh/id_rsa.pub

# This prints it in the command line for manual copying
cat ~/.ssh/id_rsa.pub

Add your SSH key to the droplet

Back on the DigitalOcean droplet creation screen, click the “New SSH Key” button and paste your SSH key into the field that opens.

Paste your SSH key into this field

Click “Add SSH Key” to save it, then make sure it’s selected, name your droplet, and hit the big “Create” button to get your server online.

It takes a minute or so for the droplet to finish spinning up

Your new droplet will display its IP address once it’s set up. You can click on it to copy the IP to your clipboard.

Click the IP address to copy it to your clipboard

Connect to the server using SSH

DigitalOcean droplets are created with a root user, and since we added our SSH keys, we can now log in without a password. Like magic!

# Make sure to replace the IP below with your server's IP address
ssh root@192.168.1.1

You will most likely be asked if you want to continue connecting the first time you log in. Type yes to continue, and you’ll see something similar to the following:

$ ssh root@138.68.11.65
The authenticity of host '138.68.11.65 (138.68.11.65)' can't be established.
ECDSA key fingerprint is SHA256:f1qsLkumkNyRNfDVgjJk2R7kRlonuce1IMoEVTL2sfE.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '138.68.11.65' (ECDSA) to the list of known hosts.
Welcome to Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-31-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

0 packages can be updated.
0 updates are security updates.



The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@nodejs-ssl-deploy:~#

Configure the server with basic security.

Once we’re logged into the server, we need to get a few things configured to keep it secure.2

Create an SSH user

First, we’re going to add a new user with sudo privileges. To do this, run the following command while logged into your droplet:

# You can choose any username you want here.
adduser jason

This command prompts us for a password, and then for some additional, optional details.

Afterward, we can see that our user has been created by running id <your_username>, which should output something like the following:

root@nodejs-ssl-deploy:~# id jason
uid=1000(jason) gid=1000(jason) groups=1000(jason)

In order to run some of the commands on the server, such as restarting services, we need to add our new user to the sudo group. Do this by running the following command:

# Don't forget: use your own username here
usermod -aG sudo jason

Now if we run id jason we can see the sudo group has been applied.

root@nodejs-ssl-deploy:~# id jason
uid=1000(jason) gid=1000(jason) groups=1000(jason),27(sudo)

Add your SSH key for the new user

Next, we need to add our SSH key to the new user. This allows us to log in without a password, which is important because we’re planning to disable password logins for this server.

# Become the new user
su - jason

# Create a new directory for SSH stuff
mkdir ~/.ssh

# Set the permissions to only allow this user into it
chmod 700 ~/.ssh

# Create a file for SSH keys
nano ~/.ssh/authorized_keys

The nano editor allows us to copy-paste your SSH key — the same one we copied to DigitalOcean when we created the droplet — into the new file, then press control + X to exit. Type Y to save the file, and press enter to confirm the file name.

We can make sure the SSH key is saved by running cat ~/.ssh/authorized_keys; if the SSH key is printed in the terminal, it’s been saved.

# Set the permissions to only allow this user to access it
chmod 600 ~/.ssh/authorized_keys

# Stop acting as the new user and become root again
exit

Disable password login

Since every server has a default root account that’s a target for automated server attacks — and because that account has unlimited power inside the server — it’s a good idea to make sure no one can use it.

After the previous step, you should be logged into your server as root. Let’s make sure the new account works and has sudo access:

# Log out of the server as root
exit

# Log into your server as the new user
ssh jason@138.68.11.65

Inside, we need to update the SSH configuration to disable password logins, and to disable logging in as root altogether.

To do this, use the following command to open the SSH configuration file for editing:

sudo nano /etc/ssh/sshd_config

Inside, you need to update two settings:

  1. Find PermitRootLogin yes and change it to PermitRootLogin no
  2. Find #PasswordAuthentication yes and change it to PasswordAuthentication no

Save the file by pressing control + X, then Y, then enter.

Finally, restart the SSH service with this command:

# Reloads the configuration we just changed
sudo systemctl reload sshd

Test your login by opening a new tab in Terminal (command + T on Mac) and logging into your server again.

If we log in as our new user, everything works as expected. However, if we try to log in as root, we get an error:

$ ssh root@138.68.11.65
Permission denied (publickey).

Set up a basic firewall

Next, we’re going to configure a simple firewall. We’re going to configure it to deny all traffic except through standard web traffic ports (80 for HTTP, and 443 for HTTPS), and to allow SSH logins.

This, in theory at least, should eliminate a lot of security risks on our server. (But again — this is not a security article; these are just basic precautions.)

We’re going to run three commands to configure the firewall — called ufw — and then we’ll enable it. Enter the following while logged into the server:

# Enable OpenSSH connections
sudo ufw allow OpenSSH

# Enable HTTP traffic
sudo ufw allow http

# Enable HTTPS traffic
sudo ufw allow https

# Turn the firewall on
sudo ufw enable

To check the status of the firewall, run sudo ufw status, which will give you the following:

jason@nodejs-ssl-deploy:~$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere                  
80                         ALLOW       Anywhere                  
443                        ALLOW       Anywhere                  
OpenSSH (v6)               ALLOW       Anywhere (v6)             
80 (v6)                    ALLOW       Anywhere (v6)             
443 (v6)                   ALLOW       Anywhere (v6)  

Get Your App Up and Running

Now that the server is set up, we can get our app installed.

Install Git.

In order to get a copy of our app to this server, we’re going to use Git. Fortunately, Ubuntu makes it really easy to install common tools, so all we need to do is run this command:

sudo apt-get install git

We can validate that Git was installed properly by running git --version:

jason@nodejs-ssl-deploy:~$ git --version
git version 2.7.4

Set up Node.js.

Node.js is a little more complex than Git, because there are several different versions of Node that are used in production environments. Therefore, we need to update apt-get with the right version for our app before we install it.

Tell apt-get which Node.js version to download

The folks at NodeSource have made it really easy to install our desired Node version. For this tutorial, we’ll be using the latest 6.x release.

Run the following commands to download and execute the setup script:

curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -

This takes a few seconds to complete.

Install Node.js v6.x

With the NodeSource script complete, we can simply use apt-get to install Node.js:

sudo apt-get install nodejs

Once it’s complete, we can verify that node is available by running node --version:

jason@nodejs-ssl-deploy:~$ node --version
v6.3.1

Clone the app

Now we can actually clone a copy of our app to the server — things are really getting exciting now.

It doesn’t matter where you install the app, so let’s create an apps dir in our user’s home folder and clone the app into a folder named after our domain — this makes it really easy to remember which app is which.

# Make sure you’re in your home folder
cd ~

# Create the new directory and move into it
mkdir apps
cd apps/

# Clone your app into a new directory named for your domain
git clone https://github.com/jlengstorf/tutorial-deploy-nodejs-ssl-digitalocean-app.git app.example.com

Test the app

To make sure your app is installed and working, move into the new folder and start it:

# Move into the app directory
cd app.example.com

# Start the app
node app

The example app listens at http://localhost:5000, so we can test if it’s working by opening a new Terminal session, logging into our server, and making a curl request to the app.

jason@nodejs-ssl-deploy:~$ curl http://localhost:5000/
<h1>I’m a Node app!</h1><p>And I’m <em>sooooo</em> secure.</p>

Awesome — we have a running app. Now we just need to make it accessible to the outside world.

We can exit from the test session (the one we just ran the curl command in), and we can stop the app in our other session using control + C.

Start Your App Using a Process Manager

Simply starting the app manually is technically enough to get the app deployed, but if the server restarts, that means we have to manually start the app again.

And in production apps, we want to eliminate as many — if not all — manual steps to get the app deployed. So we’re going to use a process manager called PM2 to run our app. This also gives us benefits like easy-to-access logs, and a simple way to start, stop, and restart the app.

PM2 also allows us to start the app automatically when the server restarts, which means one less thing we need to worry about.

Install PM2

Unlike the other tools we’ve installed, pm2 is a Node package. We install it using the npm command, which is the default package manager for Node.js.

sudo npm install -g pm2

Start your app using PM2

With PM2 installed, we can now start the app like this:

# Make sure you're in the app directory
cd ~/apps/app.example.com

# Start the app with PM2
pm2 start app

Once the app is started, we see the status displayed:

jason@nodejs-ssl-deploy:~/apps/app.example.com$ pm2 start app
[PM2] Starting app in fork_mode (1 instance)
[PM2] Done.
┌──────────┬────┬──────┬───────┬────────┬─────────┬────────┬─────────────┬──────────┐
│ App name │ id │ mode │ pid   │ status │ restart │ uptime │ memory      │ watching │
├──────────┼────┼──────┼───────┼────────┼─────────┼────────┼─────────────┼──────────┤
│ app      │ 0  │ fork │ 21229 │ online │ 0       │ 0s     │ 21.840 MB   │ disabled │
└──────────┴────┴──────┴───────┴────────┴─────────┴────────┴─────────────┴──────────┘
 Use `pm2 show <id|name>` to get more details about an app

And, conveniently, the app is running without locking up our session. In the same session we’re able to run curl http://localhost:5000 to make sure it’s running properly:

jason@nodejs-ssl-deploy:~/apps/app.example.com$ curl http://localhost:5000/
<h1>I’m a Node app!</h1><p>And I’m <em>sooooo</em> secure.</p>

Start your app automatically when the server restarts

We’re almost done with getting the app running — just one more step.

The last thing to do is to make sure that when the server restarts, PM2 starts our app again.

This is a two-step process, which we kick off by running pm2 startup systemd:

jason@nodejs-ssl-deploy:~/app.example.com$ pm2 startup 
[PM2] You have to run this command as root. Execute the following command:
      sudo su -c "env PATH=$PATH:/usr/bin pm2 startup systemd -u jason --hp /home/jason"

PM2 prints out a command that we need to run using sudo. Copy-paste that to finish the process.

sudo su -c "env PATH=$PATH:/usr/bin pm2 startup systemd -u jason --hp /home/jason"

This will step through the process of updating the server to run PM2 on startup, and then you’re all set.

Now we’ve got a running app — in the next section, we’ll make the app securely accessible to the rest of the world!

Get a Free SSL Certificate With Let’s Encrypt

SSL was a big hurdle for a long time for two reasons:

  1. It was expensive.
  2. It was hard.

Fortunately, some very smart, very kind-hearted people created Let’s Encrypt, which is:

  1. Free
  2. Easy

So now there’s really no excuse not to set up SSL for our domains.

Install Let’s Encrypt

To start, we need to install some tools that Let’s Encrypt depends on, then clone the letsencrypt repository to our server.

# Install tools that Let’s Encrypt requires
sudo apt-get install bc

# Clone the Let’s Encrypt repository to your server
sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt

Configure your domain to point to the server

Log into your DNS provider. I use CloudFlare; you’ll want to log into whichever service you either bought your domain name through (Namecheap and GoDaddy, for example), or the service you use to manage your DNS (such as CloudFlare or DNSimple).

Add an A record for your domain that points to your droplet’s IP address.

To check that the domain is pointing to your droplet, run the following (make sure to replace app.example.com with the domain you just configured):

dig +short app.example.com
# output should be your droplet’s IP address, e.g. 138.68.11.65

Generate the SSL certificate.

Now that the domain is pointed to our server, we can generate the SSL certificate:

# Move into the Let’s Encrypt directory
cd /opt/letsencrypt

# Create the SSL certificate
./certbot-auto certonly --standalone

The tool will run for a while to initialize itself, and then we’ll be asked for an admin email address, to agree to the terms, and to specify our domain name or names. Once that’s done, the certificate will be stored on the server for use with our app.

For now, that’s all we need. We’ll come back to these in a minute when we configure the domain.

Setup auto-renewal for the SSL certificate

For security, Let’s Encrypt certificates expire every 90 days, which seems pretty short. (By contrast, most paid SSL certificates are valid for at least a year.)

It turns out, though, that Let’s Encrypt has an one-step command to renew certificates:

/opt/letsencrypt/certbot-auto renew

This command checks if the certificate is near its expiration date and, when necessary, it generates an updated certificate that’s good for another 90 days.

Now, it would be a huge pain in the ass if we had to manually log into the server and renew the certificate four times a year — and most likely we’d end up forgetting at least once — so we’re going to use a built-in tool called cron to handle the renewal automatically.

To set this up, run the following command in the terminal to edit the server’s cron jobs:

sudo crontab -e

We get an option for which editor to use here. Since nano is easier than the others, we’ll stick with that.

When the editor opens, head to the bottom of the file and add the following two lines:

00 1 * * 1 /opt/letsencrypt/certbot-auto renew >> /var/log/letsencrypt-renewal.log
30 1 * * 1 /bin/systemctl reload nginx

The first line tells cron to run the renewal command, with the output logged so we can check on it when necessary, every Monday at 1 in the morning.

The second restarts NGINX — which we haven’t set up yet, so don’t worry — at 1:30 to make sure the new cert is being used.

Save and exit by pressing control + X, then Y, then enter.

That’s it for the SSL cert. The last thing left is to make your app accessible by visiting our domain name in a browser.

Point Your Domain to the App

In order to make our app accessible, we need to send visitors to our domain to our app. To do this, we’ll be using NGINX as a reverse proxy because it’s faster and less painful than handling it through Node.js.

Install NGINX

Installing NGINX is no different from most of the other tools we’ve downloaded so far. Use apt-get to download and install it:

sudo apt-get install nginx

Make sure all traffic is secure

Next, we need to make sure that all traffic is served over SSL. To do this, we’ll add a redirect for any non-SSL traffic to the SSL version. That way, if someone visits http://app.example.com, they’ll be automatically redirected to https://app.example.com.

To do this, we need to edit NGINX’s configuration files. Run the following command to open the file for editing:

sudo nano /etc/nginx/sites-enabled/default

Inside, delete everything and add the following:

# HTTP — redirect all traffic to HTTPS
server {
    listen 80;
    listen [::]:80 default_server ipv6only=on;
    return 301 https://$host$request_uri;
}

Save and exit by pressing control + X, then Y, then enter.

Create a secure Diffie-Hellman Group

It only takes a couple extra minutes to create a really secure SSL setup, so we might as well do it. One of the ways to do that is to use a strong Diffie-Hellman group, which helps ensure that our secure app stays secure.

Run the following command on your server:

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

This takes a minute or two — encryption should be hard for computers — and when it’s done we can move on for now. We’ll use this file in the next section.

Create a configuration file for SSL

Since I’m not a security expert, we’re going to defer to an actual security expert for NGINX’s SSL settings.

We need to create a new file on our server to hold these settings — if we add another domain to this server, we can reuse them this way — which we’ll do with the following command:

sudo nano /etc/nginx/snippets/ssl-params.conf

Inside, we can copy-paste the following settings.

# See https://cipherli.st/ for details on this configuration
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

# Add our strong Diffie-Hellman group
ssl_dhparam /etc/ssl/certs/dhparam.pem;

Save and exit by pressing control + X, then Y, then enter.

Configure your domain to use SSL

This is the last configuration step, I promise.

Now that we’ve got a certificate, a strong Diffie-Hellman group, and a secure SSL configuration, all that’s left to do is actually set up the reverse proxy.

Open the site configuration again:

sudo nano /etc/nginx/sites-enabled/default

Inside, add the following below the block we added earlier:

# HTTPS — proxy all requests to the Node app
server {
    # Enable HTTP/2
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name app.example.com;

    # Use the Let’s Encrypt certificates
    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    # Include the SSL configuration from cipherli.st
    include snippets/ssl-params.conf;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://localhost:5000/;
        proxy_ssl_session_reuse off;
        proxy_set_header Host $http_host;
        proxy_cache_bypass $http_upgrade;
        proxy_redirect off;
    }
}

Save and exit by pressing control + X, then Y, then enter.

This configuration listens for connections to our domain on port 443 (the HTTPS port), uses the certificate we generated to secure the connection, and then proxies our app’s output out to the browser.

Before we start the server, we should test the NGINX configuration with sudo nginx -t. If we didn’t make any typos and everything looks good, we’ll get the following:

jason@nodejs-ssl-deploy:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Enable NGINX

The very last step in this process is to start NGINX.

sudo systemctl start nginx

Test Your App

And now: the big moment. We can now visit our domain in a browser, and we’ll see our app.

Look at that sexy green lock!

The configuration steps get pretty mind-numbing toward the end, but there’s a huge payoff: we can now bask in the glory of a server that took about 30 minutes to set up, costs $5/month, and — as a bonus — gets an A+ for SSL security.

Our app's SSL rating.

Not bad for 30 minutes’ worth of setup, right?

Additional Resources


  1. If you’re on Windows, check out Git Bash for a way to run these commands on your computer.
  2. Keep in mind that this is not a security tutorial, and these are bare minimum security measures. For a production app, consult a security specialist.

Questions? Ideas? Spotted a Bug?

The code in this article is hosted on GitHub. You can fork the repo to modify or test it, open an issue to report bugs, or create a pull request to suggest improvements or modifications.