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.
On this page · 6 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
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#
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#
# ---- 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 /app/node_modules ./node_modules
COPY package.json ./
COPY src ./src
USER node
EXPOSE 3000
HEALTHCHECK \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]
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#
- The deps stage installs only production dependencies from the lock file.
- The runtime stage copies node_modules and the source — no npm cache, tests or .env files.
- NODE_ENV=production enables production behaviour in Express and libraries.
- USER node drops root privileges inside the container.
- 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#
Secrets end up in every layer and registry; local node_modules may contain wrong native binaries. Use .dockerignore.
A vulnerability in the app then gives root inside the container. Use USER node.
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?
Show solution
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#
Why use a multi-stage Dockerfile?
Want more? Practise all Node.js REST API MCQs · Node.js REST API interview questions
My notes#
Saved in this browser.