Deploying Node.js applications across different environments often introduces the classic "it works on my machine" dilemma. Differences in Node versions, operating system dependencies, and system configurations lead to unpredictable production bugs. Docker solves this by packaging your code, runtime, dependencies, and environment variables into a single lightweight, isolated, and immutable container image.
This guide covers how to write a production-ready, secure, multi-stage Dockerfile and orchestrate multi-container setups (Node.js + MongoDB + Redis) using Docker Compose.
Production Multi-Stage Dockerfile Benefits
| Metric / Aspect | Single-Stage Build | Multi-Stage Build (Production) |
|---|---|---|
| Image Size | Large (~800MB - 1.2GB) including devDependencies | Minimal (~100MB - 150MB) using node:20-alpine |
| Security Attack Surface | High (Includes build tools, compilers, npm cache) | Low (Contains only production dependencies & compiled output) |
| Process Privileges | Defaults to root user (High vulnerability risk) |
Runs as non-root node user |
| Layer Caching | Slower rebuild times on code changes | Optimal layer caching for lightning-fast deployments |
1. Optimized Multi-Stage Dockerfile
Create a Dockerfile in your project root using build stages to isolate dev tools from production execution:
# ==========================================
# Stage 1: Build & Dependency Resolution
# ==========================================
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files separately to leverage Docker layer caching
COPY package*.json ./
# Install ALL dependencies (including devDependencies)
RUN npm ci
# Copy application source code
COPY . .
# Remove devDependencies for production image
RUN npm prune --production
# ==========================================
# Stage 2: Production Execution
# ==========================================
FROM node:20-alpine AS runner
WORKDIR /app
# Set production environment
ENV NODE_ENV=production
# Create a non-root user for security compliance
USER node
# Copy production dependencies and application code from builder stage
COPY --chown=node:node --from=builder /app/package*.json ./
COPY --chown=node:node --from=builder /app/node_modules ./node_modules
COPY --chown=node:node --from=builder /app/src ./src
# Expose server port
EXPOSE 3000
# Health check to ensure service vitality
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1
# Start server
CMD ["node", "src/server.js"]
Step 2: Add .dockerignore File
Prevent copying bloated local binaries, secrets, and git history into the build context:
node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
.env
dist
coverage
2. Multi-Container Orchestration: Docker Compose
Use docker-compose.yml to spin up the Node.js service alongside isolated **MongoDB** and **Redis** containers with automated networking and health checks.
version: '3.8'
services:
# Node.js API Service
api:
build:
context: .
dockerfile: Dockerfile
container_name: mern_api
restart: unless-stopped
ports:
- "3000:3000"
environment:
- PORT=3000
- NODE_ENV=production
- MONGO_URI=mongodb://mongo:27017/prod_db
- REDIS_URL=redis://redis:6379
depends_on:
mongo:
condition: service_healthy
redis:
condition: service_healthy
networks:
- app-network
# MongoDB Database
mongo:
image: mongo:7.0
container_name: mern_mongo
restart: always
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
# Redis Cache
redis:
image: redis:7-alpine
container_name: mern_redis
restart: always
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
volumes:
mongo_data:
redis_data:
networks:
app-network:
driver: bridge
Docker Deployment CLI Commands
# Build images and start all containers in detached mode
docker compose up -d --build
# View real-time aggregated container logs
docker compose logs -f api
# Check status of running services
docker compose ps
# Gracefully stop and clean up containers and networks
docker compose down
Production Container Best Practices Checklist
- Never Run Containers as Root: Always switch to a unprivileged user (`USER node`) inside the Dockerfile to prevent potential container breakout attacks.
- Use Exact Semantic Versioning Tagging: Avoid using `node:latest` in production environments. Stick to specific Alpine tags (e.g., `node:20-alpine`) to prevent breaking changes during automated CI/CD builds.
- Use Named Volumes for Persistent Storage: Ensure database services (MongoDB, PostgreSQL) store data on persistent Docker volumes so information survives container destructions and updates.