Skip to content
intermediate Phase 12 · Docker & Applications

Dockerizing Node.js Apps

Create optimized Dockerfiles for Node.js applications.

1h
0 problems
Topic Progress 0%

A Production Multi-Stage Dockerfile

A Production Multi-Stage Dockerfile

FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

npm ci installs exactly what the lockfile says and fails on drift. The two COPY package* steps keep the install layer cached across source edits, and USER node stops the app from running as root inside the container.

Runtime Behavior

Runtime Behavior

Containers have no init process, so signal and zombie handling are your problem:

docker run --init -e NODE_ENV=production app:1.4
# or: CMD ["tini", "--", "node", "dist/server.js"]

Tini forwards SIGTERM so graceful shutdown hooks run — without it, docker stop SIGKILLs after the grace period and in-flight requests die. Handle SIGTERM explicitly: stop accepting connections, drain, exit 0. Memory: Node sizes its heap from the cgroup limit, but set --max-old-space-size (and a matching --memory) so the OOM boundary and GC behavior are predictable.

Pitfalls and Interview Angles

Pitfalls and Interview Angles

Host node_modules. Copying them in drags platform-specific binaries and dev tooling; always install inside the image.

Dev servers in prod. npm run dev with hot reload does not belong in the runtime stage — run the built output with node.

Wrong CMD after a bundler change. A CMD pointing at a missing file exits with code 1 in a restart loop; check docker logs and docker inspect --format '{{.State.ExitCode}}'.

Port mismatch. If Express listens on 3000 but the app binds process.env.PORT, set -e PORT=3000 or EXPOSE and map consistently.

Interview: defend each Dockerfile line, explain the init/signal story, and describe how you would set a memory limit together with Node's heap flag.

Quiz

1. Why add --init (or tini) when running a Node container?

Question 1 options

2. Why npm ci instead of npm install in Docker builds?

Question 2 options

3. What should CMD execute in a production image?

Question 3 options

Flashcards

Question

npm ci

Answer

Clean lockfile-exact install; the only install command that belongs in a Dockerfile.

Question

USER node

Answer

Runs the process as the unprivileged user shipped in the node image instead of root.

Question

Container init

Answer

--init/tini as PID 1: forwards SIGTERM and reaps orphaned children.

Question

Heap vs cgroup

Answer

Node reads the container memory limit; pair --memory with --max-old-space-size for a known OOM point.

Revision Notes

Key Takeaways

  • 1. Multi-stage: build with dev deps, run with prod deps only
  • 2. Signals need an init; graceful drain needs a SIGTERM handler
  • 3. Install inside the image — never copy host node_modules

Interview Tips

  • • Defend every line of the Dockerfile as if reviewing a PR
  • • Explain what happens on docker stop without tini versus with it

Cheat Sheet

node:20-slim multi-stage; npm ci → build → npm ci --omit=dev; USER node; --init + SIGTERM drain; --memory with --max-old-space-size.

Continue Learning

Before You Start

  • No prerequisites listed.