Node.js REST API · Delivery

Production Deployment

The LearnHub API is ready to ship. Packaging it as a Docker image gives you the same, reproducible artifact on a laptop, a VPS, AWS ECS or Azure Container Apps — with health checks and safe restarts built in.

Advanced 6 min Lesson 23 of 25
0/25 in Node.js REST API
On this page · 6 min · Advanced

Learning objectives#

  • Write a small, secure multi-stage Dockerfile for Node.js
  • Run the API and MySQL together with Docker Compose
  • Wire health checks, migrations and configuration
  • Plan zero-downtime releases

Explanation#

Multi-stage build: one stage installs production dependencies with npm ci --omit=dev; the final stage copies only node_modules and src onto a slim base image. Smaller images build, pull and scan faster.

Security: run as the built-in non-root node user, keep secrets out of the image (pass them as environment variables or mounted secrets), add a .dockerignore (node_modules, .env, .git).

Health: the HEALTHCHECK (or the orchestrator’s probe) calls /health; /ready checks MySQL. The API handles SIGTERM gracefully so containers stop cleanly.

Docker Compose runs the API and MySQL locally or on a single VPS, with depends_on: condition: service_healthy so the API starts after the database is ready.

Releases: run database migrations as a separate step before the new version starts; make migrations backwards-compatible so old and new containers can run side by side during a rolling update.

Syntax#

Shell Illustrative
docker build -t learnhub-api:1.4.0 .
docker run -p 3000:3000 --env-file .env.production learnhub-api:1.4.0
docker compose up -d --build
docker compose logs -f api

Code example: A production Dockerfile for the API#

DockerfileDockerfile Illustrative
# ---- deps: install production dependencies only ----
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev


# ---- runtime: small image, non-root, no build tools ----
FROM node:22-alpine
ENV NODE_ENV=production PORT=3000
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY package.json ./
COPY src ./src
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
  CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]
Expected output
docker build produces an image of roughly 150–200 MB that runs as the unprivileged node user. docker ps shows the container as “healthy” once /health answers, and docker stop sends SIGTERM so in-flight requests finish before the container exits.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. The deps stage installs only production dependencies from the lock file.
  2. The runtime stage copies node_modules and the source — no npm cache, tests or .env files.
  3. NODE_ENV=production enables production behaviour in Express and libraries.
  4. USER node drops root privileges inside the container.
  5. HEALTHCHECK lets Docker (and Compose) know when the API is really ready; CMD runs Node directly so it receives SIGTERM.

Real-world use case#

LearnHub’s GitHub Actions pipeline builds this image, tags it with the commit SHA, pushes it to a registry (ECR or ACR), runs migrations, and then updates the service. AWS ECS and Azure Container Apps replace containers one by one using the health check, so users never see downtime.

Common mistakes#

Copying .env or node_modules into the image

Secrets end up in every layer and registry; local node_modules may contain wrong native binaries. Use .dockerignore.

Running as root

A vulnerability in the app then gives root inside the container. Use USER node.

CMD ["npm", "start"]

npm may not forward SIGTERM to Node, so graceful shutdown never runs. Start node directly.

Best practices#

  • Multi-stage build with npm ci --omit=dev
  • Non-root user and .dockerignore
  • Health checks + graceful shutdown
  • Migrations as a separate, backwards-compatible step

Remember this#

deps stage → slim runtime stage.

USER node. No secrets in the image.

CMD ["node", "src/server.js"] — not npm start.

Practice exercise#

Your task: Write a docker-compose.yml that runs MySQL 8 (with a health check and a volume) and the API, starting the API only after MySQL is healthy.

Need a hint?
depends_on: { db: { condition: service_healthy } } and a mysqladmin ping health check.
Show solution
YAML Illustrative
services:
  db:
    image: mysql:8.4
    environment:
      MYSQL_DATABASE: learnhub
      MYSQL_USER: learnhub
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      retries: 5


  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: mysql://learnhub:${DB_PASSWORD}@db:3306/learnhub
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped


volumes:
  db-data:

Mini quiz#

Production Deployment quizMultiple choice Question 1 of 3

Why use a multi-stage Dockerfile?

Score so far: 0/0

Want more? Practise all Node.js REST API MCQs · Node.js REST API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.