How to Containerize a Node.js Application with Docker: Step-by-Step Guide
To containerize a Node.js app: (1) create a Dockerfile using a slim Node base image (node:20-alpine); (2) copy package.json first and run npm ci before copying app code (enables Docker layer caching); (3) use a non-root user for security; (4) use a multi-stage build to keep the production image small; (5) add a .dockerignore file to exclude node_modules and .env; (6) use Docker Compose for local development with database and cache services. The resulting image should be under 200MB and start in under 3 seconds.
Commercial Expertise
Need help with Cloud & DevOps?
Ortem deploys dedicated Cloud Infrastructure squads in 72 hours.
Prerequisites
- Docker Desktop installed (docker.com)
- A Node.js application (Express, Fastify, NestJS, or similar)
- Basic terminal familiarity
Step 1: Create a .dockerignore File
Before writing the Dockerfile, tell Docker what to exclude from the build context:
node_modules
.env
.env.*
.git
.gitignore
README.md
dist
coverage
*.log
This prevents your local node_modules from being copied into the image (we install fresh inside) and keeps sensitive .env files out.
Step 2: Write a Production Dockerfile
# ---- Build stage ----
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependency files first (cache layer)
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Copy application code
COPY . .
# If you have a build step (TypeScript, etc.)
# RUN npm run build
# ---- Runtime stage ----
FROM node:20-alpine AS runtime
WORKDIR /app
# Create non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Copy only what we need from builder
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app .
# Own files as non-root user
RUN chown -R appuser:appgroup /app
USER appuser
# Expose port and define startup command
EXPOSE 3000
CMD ["node", "src/index.js"]
Why multi-stage? The builder stage installs all dev dependencies and runs build steps. The runtime stage only contains production dependencies — keeping the image lean.
Step 3: Build and Test the Image
# Build the image
docker build -t my-node-app:latest .
# Run it locally
docker run -p 3000:3000 --env-file .env my-node-app:latest
# Test it
curl http://localhost:3000/health
Step 4: Add Docker Compose for Local Development
# docker-compose.yml
version: '3.9'
services:
api:
build:
context: .
target: runtime
ports:
- "3000:3000"
environment:
NODE_ENV: development
DATABASE_URL: postgresql://postgres:password@db:5432/myapp
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
volumes:
- .:/app # Hot reload in development
- /app/node_modules # Don't overwrite container's node_modules
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
# Start all services
docker compose up
# Run in background
docker compose up -d
# View logs
docker compose logs -f api
# Stop
docker compose down
Step 5: Optimise Image Size
Check your image size:
docker images my-node-app
Target: under 200MB for a typical Node.js API. Common bloat sources:
- Using
node:20instead ofnode:20-alpine(adds ~600MB) - Including
devDependenciesin production (npm ci --only=productionfixes this) - Copying entire repo including test files (
.dockerignorefixes this)
Production Checklist
- Use specific image tags (
node:20.11-alpine), notlatest - Non-root user in production
- Health check endpoint (
GET /healthreturning 200) - Graceful shutdown handler (SIGTERM → drain connections → exit)
- No secrets in Dockerfile or docker-compose.yml (use environment injection)
- Image pushed to private registry (AWS ECR, GCP Artifact Registry)
Need help building a containerised deployment pipeline? Talk to our DevOps team → or contact us to discuss your deployment architecture.
Get the Ortem Tech Digest
Monthly insights on AI, mobile, and software strategy - straight to your inbox. No spam, ever.
About the Author
Digital Marketing Head, Ortem Technologies
Mehul Parmar is the Digital Marketing Head at Ortem Technologies, leading the marketing team under the direction of Praveen Jha. A seasoned digital marketing expert with 15 years of experience and 500+ projects delivered, he specialises in SEO, SEM, SMO, Affiliate Marketing, Google Ads, and Analytics. Certified in Google Ads & Analytics, he is proficient in CMS platforms including WordPress, Shopify, Magento, and Asp.net. Mehul writes about growth marketing, search strategies, and performance campaigns for technology brands.
Stay Ahead
Get engineering insights in your inbox
Practical guides on software development, AI, and cloud. No fluff — published when it's worth your time.
Ready to Start Your Project?
Let Ortem Technologies help you build innovative solutions for your business.
You Might Also Like
Cloud Cost Reduction: The 8 Optimisations That Actually Move the Needle

AI-Native Cloud & FinOps: Mastering Cost Optimization in the Generative AI Era

