Dockerizing a Node.js Express API (Step-by-Step)
If you’re building a Node.js application and thinking about deployment, Docker should be your starting point.
Whether you deploy to AWS ECS, a virtual machine, or even Lambda, Docker gives you one critical benefit: consistent, portable environments across development, testing, and production.
In this guide, we’ll walk through a practical, production‑ready workflow to:
- Dockerize a Node.js Express API
- Use multi‑stage Docker builds
- Test everything locally with Docker Compose
🚀 Complete JavaScript Guide (Beginner + Advanced)
🚀 NodeJS – The Complete Guide (MVC, REST APIs, GraphQL, Deno)
Why Docker?
Docker allows you to package your application together with its runtime, dependencies, and configuration into a single container image.
Unlike virtual machines, containers share the host OS, which makes them:
- Faster to start
- More lightweight
- Easier to scale
Most importantly, Docker eliminates the classic “it works on my machine” problem.
Step 1: Create a .dockerignore File
Before writing a Dockerfile, start with .dockerignore.
It works just like .gitignore and prevents unnecessary files from being copied into your image.
Create a .dockerignore file in the project root:
node_modules dist .env .env.example .vscode .git .github .husky
Even though your app needs node_modules and dist to run, we’ll install dependencies and build the app inside Docker instead.
This keeps images smaller and builds faster.
Step 2: Create a Multi‑Stage Dockerfile
We’ll use Node 24 Alpine (LTS at the time of writing) and a multi‑stage build.
Multi‑stage builds let you:
- Build the app in one stage
- Ship only what’s needed in the final image
- Reduce image size and attack surface
Dockerfile
# ---------- Builder Stage ---------- FROM node:24-alpine AS builder RUN apk add --no-cache libc6-compat WORKDIR /app COPY . . RUN npm install RUN npm run build # ---------- Installer Stage ---------- FROM node:22-alpine AS installer RUN apk add --no-cache libc6-compat openssl WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/package-lock.json ./package-lock.json RUN npm ci --omit=dev RUN npm run prisma:generate # ---------- Runner Stage ---------- FROM node:22-alpine AS runner RUN apk add --no-cache libc6-compat openssl WORKDIR /app # Run as non-root user RUN addgroup -S expressjs && adduser -S expressjs -G expressjs USER expressjs COPY --from=installer /app . CMD ["npm", "run", "docker-start"]
Why This Matters
npm ci --omit=devinstalls only production dependencies- Dev dependencies are used only during build
- App runs as a non‑root user for better security
Step 3: Add a docker-start Script
Before deploying, your app needs to:
- Run database migrations
- Start the server
Add this script to your package.json:
{
"scripts": {
"docker-start": "npm run migrate && npm run start"
}
}
Step 4: Test Locally with Docker Compose
Before deploying anywhere, test locally.
Docker Compose lets you run your API and database together in a production‑like environment.
Create docker-compose.yml in the project root.
services:
app:
build:
context: .
dockerfile: Dockerfile
environment:
NODE_ENV: production
DATABASE_URL: mysql://root:secret@db:3306/task_manager
ports:
- "3000:3000"
depends_on:
- db
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: task_manager
ports:
- "33061:3306"
volumes:
- db-data:/var/lib/mysql
volumes:
db-data:
Important Gotcha 🚨
Inside Docker Compose, the database host is db, not localhost.
Step 5: Run Docker Compose
Start everything with:
docker-compose up -d
The first run may fail with:
P1001: Can't reach database server at db:3306
This happens because MySQL needs time to initialize.
Fix it by restarting once:
docker-compose down docker-compose up -d
Logging Inside Containers
Avoid writing logs to files inside containers ❌
- Hard to access
- Bloats images
- Not scalable
Instead:
- Log to
stdout - Use a logging service (CloudWatch, Datadog, etc.)
For local testing, comment out file‑based log transports.
Rebuild after changes:
docker-compose up -d --build
Verify Everything Works
With containers running, test the app:
curl http://localhost:3000/health
If you get a response, your app:
- Connected to the database
- Ran migrations
- Started successfully 🎉
Cleanup When You’re Done
If you no longer need containers, images, or volumes:
docker-compose down --rmi all --volumes
This will:
- Stop and remove containers
- Remove images
- Remove volumes
Final Thoughts
This workflow lets you:
- Dockerize once
- Test locally with confidence
- Deploy the same image anywhere
ECS, virtual machines, or Lambda—all with zero surprises.
If you want to take the next step and deploy this image to AWS ECS, that’s the perfect follow‑up.
Happy shipping 🚀