Using Docker for Nodejs development can be a pleasant experience! At least it is for me, using the setup described in this article. There are some issues I initially was bothered by, which I am solving in this post:
- How can I automatically restart my nodejs application inside a Docker container on code change?
- How can I debug my Dockerized Nodejs application?
- How can I install new packages in the docker container?
Prerequisites:
You should have the following software installed if you wish to use this setup:
- Docker
- Docker compose
- Nodejs
Your node app
The node app in this example is a simple expressjs hello-world server.
src/index.js
const express = require('express')
const app = express()
const port = 4000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
The application is currently only using express as a dependency in the package.json file.
Create the Dockerfile
I prefer calling my Dockerfile for development dev.Dockerfile
to separate it from the file used for production containers.
dev.Dockerfile
FROM node:alpine
WORKDIR /usr/src/app
RUN npm install pm2 -g
COPY package.json package-lock.json ./
RUN npm install
COPY . ./
CMD [ "pm2-runtime", "start", "./ecosystem.config.js" ]
I like using the Alpine version to avoid large image sizes, it’s only ~5MB compared to Ubuntu’s ~190MB.
Note that we are installing the package pm2
globally in our image. Pm2 is a process manager that allows us to watch our code directory and automatically restart the application when code changes
Use .dockerignore to prevent the copy of node_modules to the image
Our Dockerfile is copying everything in the project root to the image (The command COPY . ./
). We don’t want it to include the node_modules
directory. Because it can contain code built for our host operating system specfically - we want our image to it’s own copy of node_modules
To prevent this we create a .dockerignore
file. Not much different from a .gitignore
.dockerignore
node_modules
Create docker-compose.yaml
Our docker-compose.yaml
is using our new Dockerfile, dev.Dockerfile
Note that we are mounting the directory src
to the container using bind mount. This enables us to access our code in the docker container, while editing it on the host machine: the moment we save a file the changes are available in the container.
docker-compose.yaml
version: "3"
services:
my-nodejs-server:
build:
context: ./
dockerfile: ./dev.Dockerfile
ports:
- "4000:4000"
- "9229:9229"
volumes:
- ./src/:/usr/src/app/src/
We are exposing port 4000 to be able to access the application from our host machine. We are also exposing port 9229 to be able to debug our Dockerized nodejs application
Create an ecosystem.config file for PM2
The final piece of the puzzle is to create an ecosystem.config
file that pm2 can use to run our application in development.
ecosystem.config.js
module.exports = {
apps: [{
name: 'my-nodejs-server',
script: './src/index.js',
max_memory_restart: '1000M',
watch: ["src"],
node_args: ["--inspect=0.0.0.0:9229"],
watch_delay: 1000,
ignore_watch : ["node_modules"]
}],
};
By having the property watch: ['src']
we are telling pm2 to restart the process whenever a file changes within the directory “src”. I have seen that bind mounts and pm2 watching can be problematic on Windows. To solve this you should run everything within WSL2 (Windows subsystem for Linux 2). I recommend using WSL2 for all your Nodejs development on Windows.
Start developing
To run your development container and start developing, you simply run docker-compose up
.
Now your application inside the Docker container will automatically restart once you edit your code in your favorite code editor.
Installing new packages
When you need to install new packages, you do the normal npm install x
, which adds the package to package.json
.
You also need to run docker-compose build
to rebuild your image to install the new package in the development container as well.
Debugging our Dockerized Nodejs application from VS code
Create this file:
./.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Docker: Attach to Node",
"type": "node",
"request": "attach",
"port": 9229,
"address": "0.0.0.0",
"localRoot": "${workspaceFolder}/server/",
"remoteRoot": "/usr/src/app",
"protocol": "inspector",
"skipFiles": [
"<node_internals>/**"
],
}
]
}
Now you can simply press F5 in VS code and attach a debugger.
Full example
A full example of this is available in this GitHub repository.
Conclusion
This provides an easy to use setup for developing with Nodejs inside a docker container, without losing the ability to debug or having to manually restart the process after every code change.