CodePark/ questions / Node apps in Docker
START PRACTICINGPRACTICE
Backend question · Docker › Node apps in Docker

Review this production Dockerfile for a plain JavaScript Node API. Explain why each choice is there.

FROM node:22-slim
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]

Each line has a job: a small, pinned base image, cached dependencies, no root user, and clean shutdown.

Base image and environment:

  • node:22-slim pins the Node major version. It uses a small base image built on glibc (the standard C library on most Linux systems).
  • NODE_ENV=production tells frameworks and libraries to turn on their production behaviour.

Dependencies and caching:

  • The file copies only package*.json before it runs npm ci. Docker caches each step as a layer. So the dependency layer stays cached until the lock file changes.
  • npm ci installs exactly the versions in the lock file.
  • --omit=dev skips dev dependencies.
  • COPY . . comes last, because the source code changes most often.
  • .dockerignore keeps node_modules, .git and .env out of the image.

Security and running:

  • USER node stops the app from running as root. It uses the node user that the official image already has.
  • EXPOSE 3000 only documents the port. You still need -p to publish it.
  • The exec-form CMD (the JSON array form) runs node directly as PID 1, the first process in the container. So node receives SIGTERM, the stop signal.

The app should still handle SIGTERM itself. It should also listen on 0.0.0.0, so traffic from outside the container can reach it.

  • The base image is slim and pinned to a Node version.
  • Dependencies are copied and installed before the source code, so Docker can cache them.
  • npm ci --omit=dev gives the same production install every time.
  • USER node stops the app from running as root.
  • The exec-form CMD runs node directly, so it receives signals.