DEV Community

Cover image for Step-by-Step Guide to Setup Node With Docker
Tommy May III
Tommy May III

Posted on

Step-by-Step Guide to Setup Node With Docker

Learn how to setup Node JS inside of a Docker container

Goals of this article

  • Have a working NodeJS application
  • Make the Node app resilient by ensuring the process does not exit on error
  • Make the Node app easy to work with by auto restarting the server when code changes
  • Utilize Docker to:
    • Quickly set up a development environment that is identical to production.
    • Easily be able to switch Node versions both locally and on a server
    • All the other benefits of Docker

Prerequisites

  • Docker already installed
  • At least entry level Knowledge of Node and NPM

If you are the type of person that just wants to see the end result then maybe the github repo will suit you better


1. Get a simple Node app in place

We are going to use Express because of how easy it is to set up and the popularity of the framework.

In a clean directory let's start by initializing NPM, go ahead and run this command and follow the prompts (what you put in the prompts is not that important for this guide)

npm init
Enter fullscreen mode Exit fullscreen mode

Install Express

npm install --save-prod express
Enter fullscreen mode Exit fullscreen mode

Setup basic express server. The file below simply says to start a Node process that listens to port 3000 and responds with Hello World! to the / route.

src/index.js

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => res.send('Hello World!'))

app.listen(port, () => {console.log(`Example app listening on port ${port}!`))
Enter fullscreen mode Exit fullscreen mode

2. Setup Docker to run our Node app

We will be using a docker-compose.yml file in order to start and stop our Docker containers as opposed to typing long Docker commands. You can think of this file as a config file for multiple Docker containers.

docker-compose.yml

version: "3"
services:
  app:
    container_name: app # How the container will appear when listing containers from the CLI
    image: node:10 # The <container-name>:<tag-version> of the container, in this case the tag version aligns with the version of node
    user: node # The user to run as in the container
    working_dir: "/app" # Where to container will assume it should run commands and where you will start out if you go inside the container
    networks:
    - app # Networking can get complex, but for all intents and purposes just know that containers on the same network can speak to each other
    ports:
    - "3000:3000" # <host-port>:<container-port> to listen to, so anything running on port 3000 of the container will map to port 3000 on our localhost
    volumes:
    - ./:/app # <host-directory>:<container-directory> this says map the current directory from your system to the /app directory in the docker container
    command: "node src/index.js" # The command docker will execute when starting the container, this command is not allowed to exit, if it does your container will stop

networks:
  app:

Enter fullscreen mode Exit fullscreen mode

Now we have our config in place, let's start the docker container with this command. This just means start the containers defined in our config file and run them in the background (-d)

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Now you should be able to go to localhost:3000 in your browser and see Hello World!
hello world example shown in the browser
You should also be able to verify that your container is running by running

docker ps
Enter fullscreen mode Exit fullscreen mode

which should output the list of your running docker containers, something like
list of running containers from the docker ps command

Useful docker commands for managing this container

List all running containers

docker ps
Enter fullscreen mode Exit fullscreen mode

List all containers regardless of if they are running

docker ps -a
Enter fullscreen mode Exit fullscreen mode

Start containers from a docker-compose.yml file in the same directory

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Stop containers from a docker-compose.yml file in the same directory

docker-compose stop
Enter fullscreen mode Exit fullscreen mode

Restart containers from a docker-compose.yml file in the same directory

docker-compose restart
Enter fullscreen mode Exit fullscreen mode

See the log files from your docker container

docker-compose logs -f
Enter fullscreen mode Exit fullscreen mode

3. Make our application resilient

If you have worked with Node before then you probably know that if an error occurs in your application, like an uncaught exception, then it will shut down that Node process. That is *really bad news for us because we are bound to have a bug in our code and can not ever guarantee our code is 100% error free. The solution to this problem is usually another process that watches our Node app and restarts it if it quits. With so many solutions out there like linux's supervisord, the NPM package forever and PM2, etc... we will just need to pick one for this guide.

I am going to focus on PM2 since I am most familiar with it and it also comes with some other features besides process management such as file watching which will come in handy for our next section.

Install PM2

npm install --save-prod pm2
Enter fullscreen mode Exit fullscreen mode

PM2 can be used through the command line but we are going to set up a simple config file much like we did with the docker-compose.yml file in order to prevent us from typing long commands repeatedly

ecosystem.config.js

const path = require('path')

module.exports = {
  apps: [{
    name: 'app',
    script: 'src/index.js', // Your entry point
    instances: 1,
    autorestart: true, // THIS is the important part, this will tell PM2 to restart your app if it falls over
    max_memory_restart: '1G'
  }]
}
Enter fullscreen mode Exit fullscreen mode

Now we should change our docker-compose.yml file to use PM2 to start our app instead of starting it directly from index.js.

docker-compose.yml (Only changed the command option)

version: "3"
services:
  app:
    container_name: app # How the container will appear when listing containers from the CLI
    image: node:10 # The <container-name>:<tag-version> of the container, in this case the tag version aligns with the version of node
    user: node # The user to run as in the container
    working_dir: "/app" # Where to container will assume it should run commands and where you will start out if you go inside the container
    networks:
    - app # Networking can get complex, but for all intents and purposes just know that containers on the same network can speak to each other
    ports:
    - "3000:3000" # <host-port>:<container-port> to listen to, so anything running on port 3000 of the container will map to port 3000 on our localhost
    volumes:
    - ./:/app # <host-directory>:<container-directory> this says map the current directory from your system to the /app directory in the docker container
    command: "npx pm2 start ecosystem.config.js --no-daemon" # The command docker will execute when starting the container, this command is not allowed to exit, if it does your container will stop

networks:
  app:
Enter fullscreen mode Exit fullscreen mode

It should be noted that changing your docker-compose.yml file will not affect already running containers. In order for your changes to take place you should restart your containers

docker-compose restart
Enter fullscreen mode Exit fullscreen mode

Great we should now be back to a working app at locahost:3000 but now our app will not fall over when we have errors.

4. Make our application easy to develop on

You may have noticed that once a Node process has started then changing the code does not actually do anything until you restart that Node process, and for us that would involve restarting our Docker containers every time we make a change. Ewwwwwwwww that sounds awful. It would be ideal if we could have our Node process restarting for us automatically when we make a code change. In the past I have done things like bring in a file watching utility and using that file watching utility to restart Docker on file changes, or I would use Nodemon but that comes with some caveats when using Docker. Recently I have been using PM2 to restart my Node process when a file changes, and since we already have it pulled in from the previous step we won't have to install another dependency.

ecosystem.config.js (only added the watch option)**

const path = require('path')

module.exports = {
    apps: [{
        name: 'app',
        script: 'src/index.js',
        instances: 1,
        autorestart: true,
        watch: process.env.NODE_ENV !== 'production' ? path.resolve(__dirname, 'src') : false,
        max_memory_restart: '1G'
    }]
}
Enter fullscreen mode Exit fullscreen mode

The config file above will now watch the src directory if we do not have the NODE_ENV environment variable set to production. You can test it out by changing your index.js file to print something else to the browser besides Hello World!. Again before this can work you need to restart your Docker containers, since you changed how PM2 is running the containers

docker-compose restart
Enter fullscreen mode Exit fullscreen mode

It should be noted that restarting the Node process may take a second to finish up, if you want to watch to see when it is finished you could watch your Docker logs to tell when PM2 is done restarting your Node Process.

docker-compose logs -f
Enter fullscreen mode Exit fullscreen mode

You will see something like this when your process has restarted
Example of PM2 restarting node process

Wrapping Up

  • One of our goals was to be able to easily change Node versions, you can do this by changing the image option in the docker-compose.yml file.

  • Installing dependencies locally is done with your local NPM and Node version which can cause conflicts sometimes if your local versions are different than Dockers. It is safer to use the same Docker container to install your dependencies. You can use this command which will use that container to install dependencies and then remove it

docker run --rm -i -v <absolute-path-to-your-project-locally>:/app -w /app node:10 npm install 
Enter fullscreen mode Exit fullscreen mode
  • As mentioned above having a different local version of Node than what Docker is running could be problematic. It is best to run commands inside of your container for consistency. You can go inside a container with
docker exec -it app bash
Enter fullscreen mode Exit fullscreen mode

The above command will put you inside the container so you can continue to run your commands from there i.e. npm run start or npm run test

If you prefer not to go inside the container you could run the commands like this

docker exec -t app bash -c "npm run start"
Enter fullscreen mode Exit fullscreen mode

Top comments (5)

Collapse
 
jarielht profile image
José Ariel Herrera

Thanks for this article.

Collapse
 
podguzovvasily profile image
podguzovvasily

Great Article and thanks, but these mistakes must be resolved

Collapse
 
channprj profile image
Park Hee Chan

Thank you for saving my time. 👍

Collapse
 
myciek profile image
myciek

I have little problem. Pm2 didnt see changes. I checked with 'docker-compose up" and nodeamon is waiting but do not response to my changes in "index.js".

Collapse
 
eddardomeka profile image
eddard omeka

is there a real in running your dockerized nodejs app with pm2 in production since most container orchestrators compose, swarm or k8s seem to be able to provide everything that pm2 provifes?