Docker & NodeJS
Recommendations for dealing with NodeJS in Docker environments
Compose YAML v2 vs v3
v3 does not replace v2
v2 focus: single-node dev / test
v3 focus: multi-node orchestration
If not using Swarm / Kubernetes, stick to v2
Some best practices
Use COPY to copy files, use ADD only if there is a reason to do so
Always cleanup after npm install
RUN npm install && npm cache clean --force
CMD 'node', instead of 'npm'. Reasons:
requires another application to run (npm launches, and then starts node)
not as literal in Dockerfiles
'npm' does not work well as an 'init' or 'PID 1' process, container will not be terminated correctly as npm might not send SIGTERM to node process
Use WORKDIR not RUN mkdir (unless you need to 'chown')
FROM base image guidelines
Stick to even numbered major releases
Don't use
:latest
tag, be specific about the versionStart with Debian if migrating
Move to Alpine later (or start with Alpine, if it's a new project)
Don't use :slim (it's just a smaller version of Debian, and using Alpine is preferred)
Don't use :onbuild
Least privilege: Using 'node' User
Official node images have a '
node
' user, but it's not used by defaultDo this after 'apt/apk' and 'npm install -g'
Do this before 'npm install'
This may cause permissions issues with write access
May require 'chown node:node'
Change user from 'root' to 'node': USER node
Only the CMD, ENTRYPOINT and RUN will use a user set by user, and everything else will use root, and that can lead to problems (like installing packages, writing to files, bind mounts permissions, etc)
Set permissions on application directory
RUN mkdir app && chown -R node:node .
Run a command as a root in a container
docker-compose exec -u root
since docker will assume 'node' as default user otherwise
Making Images Efficiently
Pick proper FROM (be as specific as possible with image tags)
Line order matters (layers that change more often should be close to the bottom, and vice versa, layers that don't change that often should be close to the top )
COPY package.json (and lock) first, run npm install and then copy rest of the code
COPY package*.json ./
RUN npm install && npm cache clean --force
COPY . .
One 'apt-get' (or other package manager) line per Dockerfile
RUN apt-get update && apt-get install curl && apt-get clean
(or apt-get autoclean)
Node process management in containers
No need for nodemon, forever or pm2 on server
Use nodemon in dev for watching file changes
Docker manages app start, stop, restart, healthcheck
Node multi-threaded: Docker manages multiple "replicas"
One npm / node problem: they don't listen for proper shutdown signal by default
PID 1 (process identifier) is the first process in a system (or container), AKA init
Init process in the container has two jobs:
reap zombie processes
pass signals to subprocesses
Zombie processes is not a big Node issue
Focus on proper Node shutdown
Docker uses Linux signals to stop app (SIGINT / SIGTERM / SIGKILL)
SIGINT / SIGTERM allow graceful stop
NPM doesn't respond to SIGINT / SIGTERM
Node doesn't respond by default, but a code can be added to handle it
Docker provides a init PID 1 replacement option (tini, for instance)
Proper Node shutdown options
Temporary: use --init to fix Ctrl+C for now
docker run --init -d nodeapp
Workaround: add 'tini' to your image
RUN apk add --no-cache tini
ENTRYPOINT ["sbin/tini", "--"]
CMD ["node", "./bin/www"]
Production: Your app captures SIGINT / SIGTERM for proper exit
Multi-stage builds
New feature in Docker 17.06 (mid-2017)
Build multiple images from one Dockerfile
Those images can FROM each other
COPY files between them
Space and security benefits (included only what is needed for the app, nothing else)
Great for "artifact only" (see above)
Great for dev + test + prod
Example:
To build image from specific stage
docker build -t myapp:prod --target prod .
Cloud Native application guidelines
Follow 12factor.net principles, especially:
Use environment variables for config. Docker and Compose are great at this with multiple options
Log into stdout / stderr
Pin all versions: images, packages installed locally and globally
Graceful exit with SIGINT / SIGTERM
Create a .dockerignore
prevent bloat and unneeded files
.git/
node_modules/
npm-debug/
docker-compose*.yml
any other file(s) or folder(s) that should not be a part of the image
not needed but useful in image
Dockerfile
README.md
Compose Project Tips: Do's
Compose project tips: Don'ts
node_modules in Images
Images should not be build with node_modules from host, some NPM packages are build when they're installed for specific architecture (for example, node-gyp).
Add .gitignore to exclude node_modules. This will have added benefit of not sending unnecessary payload as context when image builds.
Startup order and dependencies
Problem: multi-service apps start out of order, node might exit or cycle
Multi container apps need:
dependency awareness
name resolution (DNS)
connection failure handling
'depends_on' (from compose.v2): when "up X", start Y first
Fixes name resolution issues with "can't resolve "
Only for Compose, not for orchestration
compose V2: works with healthchecks like a "wait for script"
Connection failure handling
restart: on-failure
good: helps slow db startup and NodeJS failing. Better: 'depends_on'
bad: could spike CPU with restart cycling
Solution: build connection timeout, buffering, retries and exponential backoffs in your app
Example of the compose with healthchecks
Templating
A relatively new and lesser-known feature is Extension Fields, which lets you define a block of text in Compose files that is reused throughout the file itself. This is mostly used when you need to set the same environment objects for a bunch of microservices, and you want to keep the file DRY:
You'll notice a new section starting with an x-, which is the template, that you can then name with a preceding & and call it from anywhere in your Compose file with * and the name. Once you start to use microservices and have hundreds or more lines in your Compose file, this will likely save you considerable time and ensure consistency of options throughout.
Avoiding devDependencies in Production
Multi-stage Dockerfile can solve this
prod stages:
npm i --only=production
dev stages:
npm i --only=development
Use
npm ci
to speed up buildsEnsure NODE_ENV is set
Dockerfile documentation
Document every line that isn't obvious
FROM stage, document why it's needed
COPY = don't document
RUN - maybe document
Add LABELs
LABEL has OCI standard now
LABEL org.opencontainers.image.
Use ARG to add info to labels like build date or git commit
Docker Hub has built-in env.vars for use with ARGs
Inlcude "RUN npm config list" to get information about NPM / Node in the logs
Dockerfile Healthchecks
Always include HEALTHCHECK
Docker run and docker-compose: info only
Docker Swarm: Key for uptime and rolling updates
Kubernetes: Not used, but helps in others making readiness / liveness probes
Last updated