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 version

  • Start 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 default

  • Do 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 builds

  • Ensure 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

Was this helpful?