Mark Pollmann's blog

Advanced Docker for NodeJS

· Mark Pollmann

The Dockerfile

  • add lots of comments and documentation
  • use copy not add. add has side-effects
  • use node not npm (problems with PID 1 signals)
  • workdir instead of mkdir (creates if not there and cd’s into it) (unless you need permissions)

FROM base images

https://news.ycombinator.com/item?id=10782897

  • basic, slim, alpine
  • use stable, even numbered, LTS
  • use alpine if you’re not migrating and think size matters. Smaller and more secure, even though debian gets smaller too and 100mb is not significant (caching). We had a problem with Alpine not supporting the newer Flow versions.
  • don’t use latest tag, pin your versions

change user

Docker runs as root in container (not the same as on host). Official node container has node user, not default. Switch to node for more security(?). Can run into permission problems. change user after apk and global npm installs. commmand USER node. Not all commands behave with this user, only run, entrypoint and cmd. workdir creates as root. -> RUN mkdir app && chown -R node:node. execs after creation will also be with this user. (change with docker-compose exec -u root)

efficient image building

-line order matters. often changing stuff below less often changing !! - copy packagejson and lock file first, then run npm install, then copy source files in. no cache busting if code was changed but deps not

WORKDIR /your-app-dir
COPY package.json package-lock.json* ./ #alternatively yarn.lock* instaed of package-lock.json. The * doesnt break the build if file is missing
RUN npm install # or yarn install
COPY . .

Multi-stage

FROM node as prod
ENV NODE_ENV=production
COPY package.json ./
run npm install --only=production
COPY . .
CMD ["node", "./app"]

FROM prod as dev
ENV NODE_ENV=development
run npm install --only=development
CMD ["nodemon", "./app", "--inspect=0.0.0.0:9229"]

FROM dev as test
CMD ["npm", "test"]
# or RUN npm test and know when the image built that it's good

docker build -t myapp:prod --target prod . docker build -t myapp:dev --target dev .

another stage for security scanning and auditing (RUN npm audit)

Docker-Compose

  • only use it for local dev
  • use named volumes (will stick around )
  • alias and container_name are unnecessary (use good server name)
  • legacy: expose and links no longer needed (all containers on same network can see each other. put on another network if you want to hide)
  • defaults are unnessary (bridge driver)
  • you can use env variables like this: ${MYVAR}

bind-mounts

use relative path like here:

volumes:
  - .:/var/lib/whatevs
  # not - /my/path/on/host:/var/lib/whatevs
  • don’t bind-mount database (slow on mac/windows as different file systems)

  • delegated flag on mac by default

  • don’t build images with node_modules from host (arch specific executables like node-gyp or bcrypt). add n_m to dockerignore

depends_on

-only in v2 what we want

services:
  myService:
    # [...]
    depends_on:
      otherService:
        condition: service_healthy #needs 2.3 < version < 3 and healthcheck in other service
  otherService:
    # [...]
    healthcheck:
      test: curl -f http://127.0.0.1
      # postgres has `pg_isready -h 127.0.0.1`
      # mongo has `echo 'db.runCommand("ping").ok' | mongo localhost:27017/test --quiet
      # mysql has `mysqladmin ping -h 127.0.0.1``

other healthceck example:

HEALTHCHECK --interval=5s --timeout=3s --start-period=15s \
CMD curl -f http://127.0.0.1/healthcheck || exit 1

Take it slow, don’t make it all or nothing, you’ll learn it while you’re in production. CI/CD, scaling, k8s…

Docker and 12-factor Apps

  • use env config
  • log to stdout/err

  • winston/bunyan/morgan (levels for dev, test, prod)

  • .dockerignore -> node modules, .git, compose file. keep dockerfile and readme in.

  • pin versions

  • graceful exit

Understanding the docker process

  • Before Docker -> pm2, nodemon
  • with docker no longer needed (?)
  • Docker manages start, stop, restart, - healthchecks, replicas
  • docker doesnt listen to termination signals correctly

PID 1 is first process and has two jobs, (1)reap zombie processes and (2)pass signals to sub-processes. (1) zombies in node no big problem. (2): SIGINT/SIGTERM/SIGKILL. kill never good, no time to respond. int when ctrl+c, term when docker stop.

  • docker waits 10s for container to respond to int or term before sending kill.
  • npm doesn’t respond to int or term (correct?). node doesnt by default but can with code. (research tini, entrypoint sbin/tini)

Orchestration

  • Kubernetes looks like clear winner
  • use one image, multi containers (because single-threaded)
  • healthchecks for readiness
  • no in-memory state
  • proper shutdown