This is how you build a NodeJS App in Docker

Table of Contents

Docker rocks! With this virtualization technology you can run practically anything (with enough tinkering) without having to muddy up your machine with a bunch of dependencies, or any of the other crap that comes with running apps directly on a server. Think of it as a protective bubble around your app! Containers run in their own sandbox without crashing and burning the host they are running on. Sounds great right? I must say docker has changed my server game tremendously, so much that running apps on bare metal just feel dirty now. Having converted all my other home lab services over to docker, its now time to do my web server! In today’s tutorial we will cover how to build a docker image for a node app and how to host it with nginx on a web server. Let’s dive right into installation on a Ubuntu\Debian server.

Installing docker & docker-compose

For Debian/Ubuntu

The following commands will install docker’s needed dependencies, the docker keyring, updating system repos and installing docker and docker-compose.

				
					sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings
curl -fsSL <https://download.docker.com/linux/ubuntu/gpg> | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \\
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] <https://download.docker.com/linux/ubuntu> \\
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose docker-compose-plugin
				
			

The quickest way of doing this is to insert the above into a bash script. Copy & paste the above into a text file using your favorite text editor. If you are using vim use :wq! to save and exit.

				
					vim install-docker.sh
				
			

After the file has been saved make the script executable with chmod.

				
					chmod +x install-docker.sh
				
			

The node app

Now that docker and its related components are installed let’s get our node app that we’ll be serving. This is essentially a stand-in for my actual site, I do this because its always better to be safe than sorry. Always do a dry run and document your process, you’ll never know when it’ll come in handy! We will be using a pre-built express app to host a simple single page website. It won’t be winning any contests but it will do for our purposes. Clone the repo to your target machine, linked here.

				
					git clone <https://github.com/cutratefuture/Dockerized-node-app.git>
				
			

Next you’ll want to navigate into that directory and get started on dockerizing the app.

				
					cd Dockerized-node-app
				
			

Building a docker image

We will need to build a container for our app, this is done via the Dockerfile. The Dockerfile is a set of instructions that will build the image container and environment our node app needs, the resulting image can then be ran via the docker CLI or docker-compose.

Creating a Dockerfile

Create this file inside of the Dockerized-node-app directory with your favorite text editor.

				
					vim Dockerfile
				
			

Next you’ll want to copy and paste the following into that file:

				
					
FROM node:lts-bullseye
EXPOSE 5400
WORKDIR /data
COPY ./app /data
RUN npm install
CMD [ "yarn", "start" ]
				
			

Here’s the line by line breakdown:

Syntax Description
FROM node:lts-bullseye
Pulls the lts-bullseye node container
EXPOSE 5400
Expose container ports
WORKDIR /data
Define working directory in container
COPY ./app /data
Copies the contents of the app dir to the /data dir in the container
RUN npm install
Install node app dependencies from package.json
CMD [ "yarn", "start" ]
Runs the node app inside the container.

Now that we have our image built file, we need to run the docker build command to create the container image file.

				
					sudo docker build -t demo-app .
				
			

This command will build out our image and name it “demo-app”

We could run the container off the bat using docker CLI, like so:

				
					sudo docker run -p 5400:5400 demo-app
				
			

Running a container this way gets the job done but real docker champions use docker-compose as it is tailored for use with multi-container apps (which this isn’t, but its a good start) and makes containers built this way easier to reuse (which is what we want). I don’t know about you but I find it annoying parsing through my command history for a build/run command.

Writing docker-compose.yml

Creating a docker-compose file is a simple operation, especially after what we’ve done so far.

Create the file using the command below:

				
					vim docker-compose.yml #or demo-app.yml
				
			

You may also name the file whatever you please, I typically do this as I have multiple containers running alongside each other and its just easier for me to keep track of the containers if I name the YAML files semantically. YML or YAML files are a very particular about their indentations so make sure you copy and paste the following carefully and try not to break the formatting otherwise docker-compose will yell at you.

				
					#docker-compose.yml
version: '3.9'
services:
    demo-app:
        ports:
            - '5400:5400'
        image: 'demo-app:latest'
        restart: on-failure
				
			

If you compare the docker-compose file contents with the docker CLI, you can pretty much see whats going on. Line-by-line the break down is as follows:

  •  
    • At the top you have the version property that is purely informative and serves only to specify backward compatibility.
    • Services are a abstract computing resource that the application will run in. This top level element will hold all the containers and their configurations.
    • demo-app this is our app under it we have the following services lined up:
      •  
        • the ports section is mapping the host port 5400 on the left, to the container’s port on the right, in this format: HOST:CONTAINER
        • image is specifying the image we built using the Dockerfile
        • restart: on-failure sets the container’s restart policy to on-failure this means that our container will restart only if the container exits with an error.

Running our app with docker-compose

In order to run our newly minted docker app, we will use the following docker-compose command while in the root of the Dockerized-node-app directory:

				
					sudo docker-compose up -d
				
			

or if you opted to name the file simply specify it with the -f flag:

				
					sudo docker-compose -f demo-app.yml up -d
				
			

This will start the container in the background. You can verify this by running docker ps

				
					sudo docker ps
				
			

You should see the container running along with the port its running at, check this image for successful output:

With that our node app should be running on our machine’s IP using port 5400, typically http://192.168.XX.XX:5400. Upon visiting your machines URL you should see the following:
do-3
Notice! If you don’t see the above make sure that you’ve allowed the correct port through your firewall.

Like I said, not the prettiest gal at the ball but it sure is a site for sore eyes. We aren’t done yet however, as the app is running on our machine but in its current state we won’t be able to point a domain to it.

Using nginx

Our docker container is running on our machines IP on port 5400, if you want to point a domain to the node app, you’ll need to create and edit a nginx conf file. In my case I still have nginx installed on the host machine so I’ll opt for the vanilla nginx experience instead of adding another layer of complexity to this, so we’ll save that for another day.

Installing nginx

On Ubuntu\Debian systems this is pretty straight forward.

				
					sudo apt install nginx
				
			

Once installed, started, and enabled at boot, navigate to the following path and create a nginx conf file:

				
					cd /etc/nginx/sites-available/ && vim demo-app.conf
				
			

Once inside this file, copy and paste the following into that file and save it. As you can see in this file, nginx will serve the app running on port 5400 to the specified domain.

				
					#demo-app.conf
server {
  server_name example.com www.example.com;

  access_log /var/log/nginx/example.com.access.log;

  location / {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header Host $host;
      proxy_pass <http://localhost:5400>;
  }
}
				
			

Its not over yet as once that is done you’ll need to enable the nginx conf file. This is done through the use of the ln command. With this command you are creating a shortcut (symlinking) for the file in the destination directory; /etc/nginx/sites-enabled.

				
					sudo ln -s /etc/nginx/sites-available/demo-app.conf /etc/nginx/sites-enabled/

				
			
In order for the file to be registered with nginx , the nginx service needs to be restarted. Before you do that, you’ll want to run sudo nginx -t before you restart NGINX, as this will test your conf for errors.
				
					sudo nginx -t
				
			

You’ll then need to restart nginx so that the changes can take effect.

				
					sudo nginx -s reload
				
			

From here your app should be running off of your domain, and you can apply a SSL certificate via certbot normally. As a bonus here’s how to do that:

BONUS: Using Certbot for SSL

Installing certbot

The first thing you’ll need to do is install certbot and its dependencies.

				
					sudo apt install certbot python3-certbot-nginx -y
				
			

Then run this command:

				
					sudo certbot --nginx -d your_domain.com -d www.your_domain.com
				
			

certbot will ask you to redirect to https, and it’ll add that section to your nginx conf file. With that you’ll now have that SSL cert on your domain, and your app will be ready for production.

Conclusion

Congratulations are in order as we successfully converted a plain node app into a docker container, learned about Dockerfiles, docker-compose, and covered a bit on nginx too! As a side note, during my site’s migration I realized that pm2 was giving me all kinds of grief. For those not in the know, pm2 is production process manager w/ load-balancer. Once it was in the container pm2 bugged out and prevented my node app from running. After doing some research I found out that docker is providing the same functionality so pm2 got axed with extreme prejudice. Just throwing it out there in case anyone else has issues with pm2. Thanks for reading and keep on docking!

Meet the Author

2 Responses

Leave a Reply