Deploy a production Node.js API application in Nginx with HTTPS

This article illustrates the process to deploy a production Node.js application on a linux Nginx with HTTPS.

Node.js Application

A node.js application could be used as a restful API service for client applications. It could be as simple as:

// app.js
var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(3030);
console.log('Server running at http://localhost:3030/');

Or a more complicated app such as FeathersJS.

Run node app.js and then the terminal hangs there to wait for coming requests. Open a browser and visit http://localhost:3030. You will see Hello World on the screen, which means it's running correctly.

PM2

Now we will install PM2, which is a process manager for Node.js applications. PM2 provides an easy way to manage and daemonize applications (run them as a service).

$ sudo npm install pm2@latest -g

Then you can start a daemon of your app.js using PM2:

$ pm2 start app.js

The output is like:

Selection 026

Note that the command line exits (instead of hanging there), but the application has been running at the background (named app, with pid 15306, type in top to find the process). You can view existing PM2 applications by pm2 ls.

To stop it, run pm2 stop app.

Now you can visit http://localhost:3030/ to see your lovely Hello World.

Nginx: set up reverse proxy server

A reverse proxy server acts as a proxy for internal web apps to serve requests from outside. For example, a user wants to view your Hello World, while port 3030 is not public. Set up nginx serve as a reverse proxy to access port 3030. So the user just talks to Nginx.

Note: reverse lies in that instead of a proxy linking to somewhere outside, nginx connects to something inside (within the same network/server/computer).

To install and start Nginx, on CentOS 7:

$ sudo yum install epel-release
$ sudo yum install nginx
$ sudo systemctl start nginx 

On Ubuntu, use sudo apt-get install nginx instead.

Note: do backup the configuration files such as /etc/nginx/nginx.conf before editing it. Such as sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.

Then, configure the reverse proxy to serve app.js. Ensure you have sudo permission on the server, locate the location / section in file /etc/nginx/nginx.conf and add proxy_ directives:

location / {
    proxy_pass http://127.0.0.1:3030;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
}

Save it, then restart the Nginx: sudo systemctl restart nginx. Now you have successfully serve app.js at port 80 (the default web port number). Nginx helps to redirect all requests to 80 to port 3030 internally, while the user does not even know.

To validate it's setup correctly, visit http://YOURSERVERIP/, you should be able to see Hello World again. Cheers!

Domain name

OK, you do not always want to send an API endpoint to your developers/consumers with IP in it. So you go and buy a cool domain name on AWS (or NameCheap or GoDaddy, whichever you like). I bought the domain name isvg.io from AWS for the purpose of testing.

For any domain name service provider, same as AWS, there is somewhere to setup the DNS records. For AWS, go to Route53 -> Hosted Zones -> isvg.io. Click Create Record Set:

Selection 028

Type in your server IP address to the Value field. Then hit the Create button. Then do the same thing for www.isvg.io, i.e., create another Record Set, type in www in the Name field and SERVER IP in Value field, Create.

That's it. You should be able to visit http://isvg.io very soon after you create this Record Set. Phew :)

HTTPS Certificate

OK ok, you want to enable HTTPS to protect your application from bad guys. That's also not troublesome. First, buy some HTTPS certificate. Some say that this service LetsEncrypt gives you free certificates. You can check it out. In this article, I go to NameCheap to purchase a certificate. The prices vary from $8.88 to $50 per year. I bought one from PositiveSSL.

A HTTPS certificate goes valid after they verify that a domain belongs to you. So there is a validation process. First, create a private key and CSR file locally:

$ openssl req -newkey rsa:2048 -nodes -keyout isvg.io.key -out isvg.io.csr

You will prompted to these questions:

Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:New York
Locality Name (eg, city) []:New York
Organization Name (eg, company) [Internet Widgits Pty Ltd]:My Company
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:isvg.io
Email Address []:sammy@example.com

Important: put your DOMAIN NAME to the field Common Name. NameCheap validates this.

This command will create two files isvg.io.key and isvg.io.csr on your server, show the content of isvg.io.csr and copy the content:

$ cat isvg.io.csr

It's a long sequences of messy characters. No worries, machine knows what it means:

-----BEGIN CERTIFICATE REQUEST-----
MIIC6DCCAdACAQAwgYkxCzAJBgNVBAYTAkNOMQ8wDQYDVQQIDAZGdWppYW4xCzAJ
BgNVBAcMAlpaMRUwEwYDVQQKDAxzaGFvamlhbmcuaW8xDDAKBgNVBAsMA0VuZzEQ
......
M35199hCJxG9aLnAY8W2PprrXhg7ZhdQT3e0xPnI/kqffH81+gx+H2XsA6orl0nm
HfYbstSNqouuYcUkA4AvEeIQwWVa92+Inzq/yAfC+8vfA4rG6/l91hHj0bEP0f0X
A8h+M9UhqjjNOPhbQw7iQydE9hNXy2S9rw45bw==
-----END CERTIFICATE REQUEST-----

Go to the NameCheap dashboard of Product List, click the ACTIVATE button at the new SSL certificate you just bought. It shows a form where you can paste the CSR string:

Selection 029

Paste the CSR string there, the form will intepret it and fill isvg.io to the field Primary domain for you. Click Next. Since we will use Nginx, at step 2, we choose Any other server (cPanel, Apache, NGINX, etc.) and Next:

Selection 031

On this step, you can choose one of three ways to confirm that you own the domain:

  1. Email
  2. HTTP based
  3. DNS based.

In this article, I choose DNS based. I will show you how to do it later.

Click Next, fill in your email address, then finally submit the request. Now your SSL certificate's status is IN PROGRESS. One action more to take to validate it. Click the link go to the Certificate Details page:

Selection 032

On the next screen, expand the dropdown besides EDIT METHODS, select Get Record:

Selection 033

Alright, we have got Host and Target for our DNS validation. Since our domain name isvg.io is bought on AWS, go to its DNS records page, create a CNAME record:

Selection 034

Fill the Name field with Host, and Value field with Target. Hit the Create button.

That's all we should configure for validation. Some minutes later (10 minutes for me), you will get an email to notify that the SSL certificate has been activated:

Selection 035

Download the certificate file isvg_io.zip from the email attatchment or from the NameCheap dashboard. Extract the zip file and get file isvg_io.crt (rename it to isvg.io.crt). Together with the previous file isvg.io.key, they are the files you need to tell Nginx for your HTTPS connection.

Now we are ready to go back to Nginx to setup the secure HTTPS connection.

Nginx: HTTPS

Copy files isvg.io.key and isvg.io.crt to your web server (the one Nginx runs on). Modify the Nginx configuration file /etc/nginx/nginx.conf, go to section server {, change the listening port number to 443:

listen       443 ssl http2 default_server;
listen       [::]:443 ssl http2 default_server;

Then add the following lines below root directive:

ssl_certificate "/your/path/to/isvg.io.crt";
ssl_certificate_key "/your/path/to/isvg.io.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout  10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

Save the file, restart Nginx: sudo systemctl restart nginx.

Hooray, visit https://isvg.io, you will see Hello World (again)!

Great, we are done.

References