Zero-downtime Deployments Using Supervisor and Nginx Load Balancer: How to Do It?

When you run a web application in a production environment, you should strive for its 100% availability. If you often deploy new versions and cause outages while doing so, it’s perceived as a big mistake. A few minutes “off” can result in a significant loss for the client.

We use the Supervisor tool to run and monitor processes on Unix and Unix-like systems. It has proven to be flexible, extensible and, moreover, it enables the automation and simplification of process management. Usually, it maintains Node.js and REST API services for us. Under normal circumstances, when restarting such a service, a few seconds of downtime occur. New source codes are uploaded to the server and the service must be reloaded.

We’re able to fix this problem by setting up Nginx and Supervisor. It should be noted that although it’s a quick, simple and relatively efficient solution, it isn’t a system that “thinks” of all the problems associated with deploying applications. That’s why it’s necessary to keep your eyes open and to fine-tune the deployment on the fly.

Initial configuration with outages

Suppose we use the following command to run the REST API:

$ npm run production 

The simplified related Supervisor configuration is as follows:

[program:mojweb-api]
 command=npm run production 
 directory=/var/www/mojweb.sk/www/api
 user=mojweb
 stopasgroup=true
 killasgroup=true
 autostart=true
 autorestart=true

This configuration results in the service being turned on as soon as the virtual machine is started. At the same time, the Supervisor shall ensure that the process is started whenever a failure occurs.

The Node.js itself “listens” to the process on the port or socket, which in our case was defined in the configuration file. So let’s assume that the API will listen on port 10000 when it starts.

The simplified NGINX configuration is as follows:

        location /api-eshop {
             rewrite ^/api-eshop/(.*) /$1  break;
             proxy_pass http://localhost:10000;
         }

Requests that arrive at the line /api-eshop will be routed to port 10000. 

To restart the service, we use the Supervisor:

$ supervisorctl restart mojweb-api 

New configuration

The principle of starting changes in the new configuration will work as follows.

  1. An API instance is running on the server 
  2. Changes to the source code will be uploaded to the web 
  3. The second instance of the API will be turned on on the server with the new source code 
  4. The old API instance will shut down 
  5. There’s only one updated application running on the server.

In order to achieve deployment of changes to the site without outages, we need to make a few minor adjustments. 

On the application side, we need to achieve the ability to configure the port on which we are listening directly when running from the command line. 

We’ll replace the original command with:

$ npm run production -p 10000

And we’ll modify the Supervisor configuration as follows:

[program:mojweb-api1]
 command=npm run production -p 10000
 directory=/var/www/mojweb.sk/www/api
 user=mojweb
 stopasgroup=true
 killasgroup=true
 autostart=true
 autorestart=true

 [program:mojweb-api2]
 command=npm run production -p 10001
 directory=/var/www/mojweb.sk/www/api
 user=mojweb
 stopasgroup=true
 killasgroup=true
 autostart=true
 autorestart=true

In this way, we’re able to run 2 API instances on different ports.

Nginx load balancer

If one instance is restarted, requests must be directed to the other. This step can be provided by NGINX load balancer. 

Modified configuration for NGINX:

   upstream magazin-ssr {
  server localhost:10000;
  server localhost:10001;
     }
location /api-eshop {
             rewrite ^/api-eshop/(.*) /$1  break;
             proxy_pass http://magazin-ssr;
         }

Nginx therefore decides which API instance to use. If one is off, traffic is directed to the other, and vice versa.

We then use a simple SHELL script to restart the service.

supervisorctl start ssr-edsi1
 sleep 10s
 supervisorctl restart ssr-edsi
 sleep 10s
 supervisorctl stop ssr-edsi1

All done!