Bablu Kumar Singh
Back to Blog
DevOps
4 min read
June 6, 2026

Docker Deployment for Node.js Applications: A Production Guide

Docker Deployment for Node.js Applications: A Production Guide

"Works on my machine" mostly stops being a problem once the machine is the same everywhere, which is what a container actually buys you. Here's how I containerize Node.js apps for production, past the tutorial-level Dockerfile.

The Naive Dockerfile (and Why It Is Bad)

Most tutorials start here:

FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "dist/index.js"]

Problems:

  • Copies node_modules, .git, and other junk into the image.
  • Installs devDependencies in production.
  • Uses the full Node.js image (1 GB+).
  • No layer caching — every code change reinstalls all dependencies.

Multi-Stage Build (Production-Ready)

A multi-stage build separates the build environment from the production runtime:

# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Stage 2: Build TypeScript
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json tsconfig.json ./
COPY --from=deps /app/node_modules ./node_modules
COPY src ./src
RUN npm run build

# Stage 3: Production runtime
FROM node:20-alpine AS runner
WORKDIR /app

# Create non-root user
RUN addgroup --system appgroup && adduser --system appuser --ingroup appgroup

COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./

USER appuser
EXPOSE 3000
ENV NODE_ENV=production

CMD ["node", "dist/index.js"]

Results:

  • Final image size: ~150 MB (vs. 1 GB+ with the naive approach).
  • No devDependencies, no source code, no build tools in production.
  • Runs as a non-root user for security.

The .dockerignore File

Just as critical as the Dockerfile. Prevent unnecessary files from entering the build context:

node_modules
.git
.env
.env.*
dist
*.md
.vscode
coverage
tests

A smaller build context means faster docker build commands.

Environment Variables

Never bake secrets into the image. Pass them at runtime:

docker run -d \
  -e DATABASE_URL="mongodb://..." \
  -e REDIS_URL="redis://..." \
  -e JWT_SECRET="super-secret" \
  -p 3000:3000 \
  my-app:latest

For complex setups, use a .env file with Docker Compose (never commit it to Git).

Docker Compose for Development

Docker Compose orchestrates multi-container setups. A typical backend stack:

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - '3000:3000'
    environment:
      - DATABASE_URL=mongodb://mongo:27017/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      - mongo
      - redis
    volumes:
      - ./src:/app/src  # hot reload in dev

  mongo:
    image: mongo:7
    ports:
      - '27017:27017'
    volumes:
      - mongo_data:/data/db

  redis:
    image: redis:7-alpine
    ports:
      - '6379:6379'

volumes:
  mongo_data:

Start everything with one command:

docker compose up -d

Health Checks in Docker

Add a health check so Docker and orchestrators know when your app is ready:

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget --quiet --tries=1 --spider http://localhost:3000/api/health || exit 1

Layer Caching Strategy

Docker caches layers from top to bottom. Structure your Dockerfile so frequently changing layers are at the bottom:

  1. Base image (rarely changes)
  2. COPY package.json and npm ci (changes when dependencies change)
  3. COPY src and npm run build (changes on every code push)

This way, a code change does not trigger a full npm ci — only the build step runs.

Production Deployment Checklist

  • Use multi-stage builds to minimize image size.
  • Run as a non-root user.
  • Never bake secrets into images.
  • Add a health check endpoint and Docker health check.
  • Set NODE_ENV=production explicitly.
  • Use npm ci instead of npm install for deterministic builds.
  • Tag images with Git SHA, not latest, for traceability.
  • Scan images for vulnerabilities with docker scout or Trivy.

Wrapping Up

The multi-stage build is the change that matters most — going from a 1GB+ image with dev tools and source code baked in, to something closer to 150MB that's just the runtime and the build output.

#Docker#Deployment#DevOps#Node.js
Bablu Kumar Singh
Written by

Bablu Kumar Singh

Backend-Focused Full Stack Developer

Writes about backend systems and databases based on what comes up while building with Node.js, MongoDB, PostgreSQL, and Redis.

You May Also Like

Building SaaS Platforms with Node.js: Architecture to Payment Integration
Backend Development
4 min read

Building SaaS Platforms with Node.js: Architecture to Payment Integration

End-to-end guide for building multi-tenant SaaS applications with Node.js — covering tenant isolation, subscription billing with Stripe, webhook handling, and production deployment.

May 19, 2026Read
AWS for Backend Developers: Essential Services and Architecture Patterns
DevOps
4 min read

AWS for Backend Developers: Essential Services and Architecture Patterns

A backend developer's guide to essential AWS services — covering EC2, RDS, S3, SQS, CloudFront, and building a production-ready deployment architecture with Auto Scaling and load balancing.

Jun 9, 2026Read
Authentication Systems in Node.js: JWT, Refresh Tokens, and Security
Backend Development
4 min read

Authentication Systems in Node.js: JWT, Refresh Tokens, and Security

Build a production-grade authentication system in Node.js — covering JWT access tokens, refresh token rotation, password hashing with bcrypt, and common security pitfalls to avoid.

May 28, 2026Read