In this step-by-step guide, I will walk you through the process of dockerizing a Node.js application with NGINX, Let's Encrypt, and Docker Compose.

Docker Compose allows you to define, configure, and manage multi-container applications in a single YAML configuration file. Developers can define the set of services and dependencies needed for an application and run the entire application on different operating systems.

NGINX is a multifunctional HTTP and reverse proxy server, mail proxy server, and generic TCP/UDP proxy server that is widely used in the DevOps world.

Let's Encrypt is a free, automated, and open certificate authority that provides TLS certificates to over 300 million websites worldwide. It is operated by the nonprofit Internet Security Research Group.

Prerequisites

To follow this guide, the following prerequisites are required:

  • An Ubuntu server must be installed on your system.
  • A root user or a user with sudo privileges must be configured.

Installing Docker and Docker Compose

Before starting, the Docker and Docker Compose packages must be installed on your server. If they are not installed, follow the steps below to install both packages.

First, install all the packages required to allow apt to install the packages over HTTPS:

apt install apt-transport-https ca-certificates curl software-properties-common -y

Next, download and add Docker's official GPG key:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

Then, add the Docker repository to the apt source file:

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

After that, update the apt package index:

apt update -y

Once the package index is updated, install both the Docker and Docker Compose packages on your server.

apt install docker-ce docker-compose -y

Verify the status of the Docker service after successful installation:

systemctl status docker

You will see the active status of the Docker service on the following screen:

Checking Docker status

Checking Docker status

Installing Node.js

You can use the NodeSource repository to install the latest version of Node.js. Here are the steps:

First, add the NodeSource repository to apt:

curl -sL https://deb.nodesource.com/setup_18.x | bash -

Next, update the package index:

apt update -y

Now, install the Node.js from the NodeSource repository:

apt install nodejs -y

Verify that Node.js has been installed by checking its version:

node --version

Creating the Node.js application

In this section, we will create a simple Node.js application and dockerize it in the next section.

First, create a directory for your Node.js application.

mkdir ~/node-app

Then, navigate inside your created directory and run the following command to initialize a new project:

cd ~/node-app
npm init -y

This command will create a package.json file in your current working directory. The package.json holds the information about your application.

Initializing the Node.js application

Initializing the Node.js application

Then, install the Express.js framework using the npm command. Express is a lightweight framework that provides a set of features for web applications.

npm install express --save

Next, create a server.js file for your application.

nano server.js

Add the following code:

// server.js
const express = require('express');

//Create an app
const app = express();
app.get('/', (req, res) => {
    res.send('Node.js application is working!\n');
});

//Listen port
const PORT = 8080;
app.listen(PORT);
console.log(`Running on port ${PORT}`);

Save and close the file. Then, run your Node.js application:

node server.js

The above command will start an Express server on port 8080, as shown below.

http://your-server-ip:8080

Now, open your web browser and verify your application using URL http://your-server-ip:8080. You should receive a message that the Node:js application is working.

Press CTRL+C to stop the Express server.

Building the Node.js application container

To build a container for the Node.js application, we will use Docker to package the application with its dependencies and configuration into a single image. You can deploy and run this image on different platforms easily, without any additional dependencies.

First, create a Dockerfile inside your Node.js application directory.

nano Dockerfile

Add the following code:

# Define Node.js version
FROM node:18

# Create app directory
WORKDIR /usr/src/app

# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
RUN npm install

# Bundle app source
COPY . .

# Define port to expose application for the outside world.
EXPOSE 8080
CMD [ "node", "server.js" ]

Next, change the directory to your Node.js application directory and build the Docker image:

cd /root/node-app
docker build -t node-app .

This will build the Docker image from your Node.js application, as shown below:

Building node application Docker image

Building node application Docker image

Here is an explanation of the above command:

  • -t: Define the name of your image.
  • .: Specify the current working directory.

You can now verify your built Docker image using the following command:

docker images

This will show you your Node.js application image, as shown below:

REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
node-app     latest    628798797f8b   19 seconds ago   1.1GB

Finally, run your Node.js application container using the Node.js image:

docker run --name node-app-container -p 80:8080 -d node-app

Here is an explanation of the above command:

  • --name: Specify the name of your Node.js application container.
  • -p: Map Port 80 from your server machine to Port 8080 in the container.
  • -d: Run the container in the background.

You can check the status of your Node.js application container using the following command:

docker ps

You will see the Node.js container status in the following output:

CONTAINER ID   IMAGE      COMMAND                  CREATED          STATUS         PORTS                                   NAMES
e8639110ed17   node-app   "docker-entrypoint.s…"   10 seconds ago   Up 8 seconds   0.0.0.0:80->8080/tcp, :::80->8080/tcp   node-app-container

You can now access your Node.js application running inside the Docker container using the URL http://your-server-ip:80.

After successful verification, stop and remove your Node.js application container using the following commands:

docker stop node-app-container
docker container rm node-app-container

Next, remove all unused images using the following command:

docker system prune -a

Setting up NGINX as a reverse proxy

At this point, the Dockerfile for your Node.js application is tested and working properly. Now you will need to create an NGINX configuration file to build and run the NGINX container. Using NGINX as a reverse proxy allows you to bind your application with the domain name and secure it with SSL.

First, create a directory in the node-app directory.

mkdir nginx-conf

Then, create an NGINX configuration file:

nano nginx-conf/nginx.conf

Add the following configuration:

server {
        listen 80;
        listen [::]:80;

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;

        server_name node.exampledomain.com www.node.exampledomain.com;

        location / {
                proxy_pass http://nodejs:8080;
        }

        location ~ /.well-known/acme-challenge {
                allow all;
                root /var/www/html;
        }
}

Save and close the file when you're done.

Here is a brief explanation of the NGINX configuration file:

  • server_name: Specify your domain name.
  • proxy_pass: Specify the Node.js application port running inside the container.
  • listen: Specify the NGINX listening port.

Creating the Docker Compose file for the Node.js and NGINX containers

Here, we will use docker-compose.yml to define and run multiple containers, such as NGINX, Node.js, and Certbot, using a single resource and working them together.

First, create a docker-compose.yml file in your node-app directory:

nano docker-compose.yml

Add the following configuration to define the Node.js service:

version: '3'
services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped
    networks:
      - app-network

A brief explanation of the Node.js service component is shown below:

  • build: Specify the Dockerfile to build the Node.js application image.
  • image: Specify the name of your application image.
  • container_name: Specify the name of your application container.
  • restart: Define the container start behavior.
  • networks: Define the network name to interconnect all containers.

Next, add the following configuration for the webserver service:

  webserver:
    image: nginx:latest
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network

A brief explanation of the webserver service component is shown below:

  • image: Specify the version of your NGINX web server image.
  • ports: Specify the NGINX listening port.
  • web-root:/var/www/html: Specify the Docker volume name to be mounted in the /var/www/html directory in the NGINX container.
  • ./nginx-conf:/etc/nginx/conf.d: Mount the NGINX configuration file from the Docker host to the NGINX container.
  • certbot-etc:/etc/letsencrypt: Mount the Let's Encrypt certificates to the NGINX container.

Next, add the following configuration for the Certbot service:

  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email user@email-domain.com --agree-tos --no-eff-email --staging -d node.exampledomain.com 

A brief explanation of the Certbot service component is shown below:

  • image: Specify the Certbot image name.
  • volumes: Specify the volumes used to share resources with the NGINX container, including application code, certificates, and the Let's Encrypt working directory.
  • depends_on: Used to start the Certbot container only after the webserver container.
  • command: Run the certbot command to download Let's Encrypt certificates for your domain.

Note: Replace node.exampledomain.com with your domain name and user@email-domain.com with your real email address.

Next, add the following configuration for volume and network definitions:

volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /root/node-app
      o: bind

networks:
  app-network:
    driver: bridge

Note: Replace the /root/node-app with the path of your Node.js application.

Save and close the file when you're finished.

Deploying the application using Docker Compose

At this point, your Docker Compose file is ready to start all containers and test Let's Encrypt certificate requests.

Now, run the docker-compose command to create and run all containers in the background, which you specified in the docker-compose.yml file.

docker-compose up -d

If everything is OK, you will see the following screen:

Building and starting node.js app using Docker Compose

Building and starting node.js app using Docker Compose

The above command will start all containers, download the Let's Encrypt certificates, and mount them to the /etc/letsencrypt/live folder on the webserver container.

You can now check the status of all containers using the following command:

docker-compose ps

You will see the service status in the following output:

  Name                 Command               State                 Ports              
--------------------------------------------------------------------------------------
certbot     certbot certonly --webroot ...   Exit 0                                   
nodejs      docker-entrypoint.sh node  ...   Up       8080/tcp                        
webserver   /docker-entrypoint.sh ngin ...   Up       0.0.0.0:80->80/tcp,:::80->80/tcp

As you can see, the nodejs and webserver containers are in the Up state, while the certbot container is in the Exit 0 state. To identify the issue of the certbot container, check the Certbot log using the following command:

docker-compose logs certbot

You will see the Certbot log on the following screen:

Showing the certbot logs

Showing the certbot logs

Now, verify that your Let's Encrypt certificates are mounted to the webserver container:

docker-compose exec webserver ls -la /etc/letsencrypt/live

If everything is OK, you will see the following screen:

Verifying the Lets Encrypt certificate mount

Verifying the Lets Encrypt certificate mount

As you can see, the Let's Encrypt certificate request is successful. The problem is with the Verifying the Let's Encrypt certificate mount flag in the command: section of your docker-compose.yml file.

Let's edit the Docker Compose file and replace the --staging flag with the --force-renewal flag:

nano docker-compose.yml

Change the following line:

    command: certonly --webroot --webroot-path=/var/www/html --email user@email-domain.com --agree-tos --no-eff-email --force-renewal -d node.exampledomain.com 

Save and close the file, and then run the docker-compose command again to recreate the Certbot container.

docker-compose up --force-recreate --no-deps certbot

If the certificate request is successful, you will see the following screen:

Verifying the Lets Encrypt certificate request

Verifying the Lets Encrypt certificate request

Modifying NGINX configuration and service definition

At this point, you have Let's Encrypt certificates in your hand. Now, you will need to edit your NGINX configuration file and Docker Compose file to define the SSL ports and certificate path.

First, stop the webserver container using the following command:

docker-compose stop webserver

Next, edit the NGINX configuration file:

nano nginx-conf/nginx.conf

Remove the default configuration, and add the following configuration:

server {
        listen 80;
        listen [::]:80;
        server_name node.exampledomain.com www.node.exampledomain.com;

        location ~ /.well-known/acme-challenge {
          allow all;
          root /var/www/html;
        }

        location / {
                rewrite ^ https://$host$request_uri? permanent;
        }
}

server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name node.exampledomain.com www.node.exampledomain.com;

        server_tokens off;

        ssl_certificate /etc/letsencrypt/live/node.exampledomain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/node.exampledomain.com/privkey.pem;

        ssl_buffer_size 8k;

        ssl_protocols TLSv1.2;
        ssl_prefer_server_ciphers on;

        ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

        ssl_ecdh_curve secp384r1;
        ssl_session_tickets off;

        ssl_stapling on;
        ssl_stapling_verify on;
        resolver 8.8.8.8;

        location / {
                try_files $uri @nodejs;
        }

        location @nodejs {
                proxy_pass http://nodejs:8080;
                add_header X-Frame-Options "SAMEORIGIN" always;
                add_header X-XSS-Protection "1; mode=block" always;
                add_header X-Content-Type-Options "nosniff" always;
                add_header Referrer-Policy "no-referrer-when-downgrade" always;
                add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
                #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
                # enable strict transport security only if you understand the implications
        }

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;
}

Save and close the file when you're done.

Next, edit the Docker Compose file:

nano docker-compose.yml

Find the webserver service section and modify it with the following configuration:

  webserver:
    image: nginx:latest
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network

Save and close the file, and then recreate the webserver container using the following command.

docker-compose up -d --force-recreate --no-deps webserver

Verifying Node.js application

At this point, your Node.js application is hosted inside the Docker container with NGINX as a reverse proxy. It's time to test the Node.js application by using a web browser.

First, check the NGINX and Node.js containers to identify the listening ports.

docker-compose ps

You will see that the NGINX container is started and listening on Ports 80 and 443.

CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                                                                      NAMES
02779b7297e1   nginx:latest   "/docker-entrypoint.…"   6 seconds ago   Up 5 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp   webserver
3a33075c6381   nodejs         "docker-entrypoint.s…"   8 minutes ago   Up 8 minutes   8080/tcp                                                                   nodejs

Now, open your web browser, and access the Node.js application securely using the URL https://node.exampledomain.com.

Subscribe to 4sysops newsletter!

Verifying that Jode.js application is working

Verifying that Jode.js application is working

Conclusion

Congratulations! You have successfully deployed a Node.js application with NGINX as a reverse proxy in a containerized environment. This way, you can deploy your application in a scalable, secure, and easily maintainable environment. Using Docker Compose makes it easier to manage multiple containers and services.

avatar
2 Comments
  1. cesar ramirez 2 weeks ago

    Would it be possible to do a video of it….??

  2. Remember, docker-compose is now deprecated so you can install the Docker Compose plugin to start using the newer version of docker compose.

Leave a reply

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2023

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account