Node.js Docker: Complete Theory & Practice Guide
Learn containerization, Docker builds, compose stacks, production hardening, and Kubernetes transition.
DockerfileComposeKubernetes
Table of Contents
1. Theory: Containerization Fundamentals
Containerization is operating-system-level virtualization that packages an application with its dependencies into an isolated, portable unit called a container.
┌─────────────────────────────────────────────────────────────┐
│ Traditional VMs │
├─────────────────────────────────────────────────────────────┤
│ App A │ App B │ App C │
│ Guest │ Guest │ Guest │
│ OS │ OS │ OS │
├────────┴───────┴────────────────────────────────────────────┤
│ Hypervisor (VirtualBox, VMware) │
├─────────────────────────────────────────────────────────────┤
│ Host Operating System │
├─────────────────────────────────────────────────────────────┤
│ Physical Hardware │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Containers │
├─────────────────────────────────────────────────────────────┤
│ App A │ App B │ App C │
├────────┴───────┴────────────────────────────────────────────┤
│ Docker Engine │
├─────────────────────────────────────────────────────────────┤
│ Host Operating System │
├─────────────────────────────────────────────────────────────┤
│ Physical Hardware │
└─────────────────────────────────────────────────────────────┘
| Aspect | Containers | Virtual Machines |
|---|---|---|
| Guest OS | None (shared kernel) | Full OS per VM |
| Startup Time | Milliseconds | Seconds to minutes |
| Size | MB (10-100MB) | GB (1-10GB) |
| Resource Overhead | 1-5% | 10-20% |
| Isolation | Process-level | Hardware-level |
| Portability | High | Medium |
| Performance | Near-native | Some overhead |
const dockerBenefits = {
consistency: {
description: 'Eliminates "works on my machine" problems',
explanation: 'Same environment from development to production'
},
isolation: {
description: 'Separate Node.js versions, dependencies per service',
explanation: 'Run Node 14, 16, 18 on same host'
},
scalability: {
description: 'Easy horizontal scaling',
explanation: 'docker compose up --scale api=10'
},
dependencyManagement: {
description: 'Database, Redis, queues as containers',
explanation: 'Complete stack in docker-compose.yml'
},
ciCd: {
description: 'Consistent build pipelines',
explanation: 'Build once, deploy anywhere'
}
};
2. Theory: Docker Architecture
const dockerArchitecture = {
client: {
purpose: 'CLI interface for user commands',
commands: ['docker build', 'docker run', 'docker push']
},
daemon: {
purpose: 'Manages containers, images, networks',
responsibilities: [
'Build images from Dockerfiles',
'Run containers',
'Manage container lifecycle',
'Handle networking and volumes'
]
},
registry: {
purpose: 'Store and distribute images',
public: 'Docker Hub (docker.io)',
private: 'Amazon ECR, Google GCR, Azure ACR'
},
imageVsContainer: {
image: 'Read-only template with code, runtime, libraries',
container: 'Running instance of an image',
analogy: 'Image = Class, Container = Instance'
},
layers: {
concept: 'Each instruction in Dockerfile creates a layer',
benefits: ['Caching', 'Reuse across images', 'Smaller transfers']
}
};
# Dockerfile instruction reference
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
ARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENV
ENV PORT=3000
EXPOSE 3000
USER node
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node health.js
CMD ["node", "server.js"]
3. Basic: Dockerfile Optimization
# Dockerfile - Production-optimized
FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --chown=node:node . .
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {r.statusCode === 200 ? process.exit(0) : process.exit(1)})"
CMD ["node", "src/server.js"]
# Layer caching optimization
# ❌ Bad
COPY . .
RUN npm install
RUN npm run build
# ✅ Good
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# .dockerignore
node_modules
npm-debug.log
.env
.env.*
!.env.example
.git
.github
test
coverage
logs
*.log
dist
build
.vscode
.idea
Dockerfile
.dockerignore
docker-compose*.yml
4. Theory: Multi-stage Builds
Multi-stage builds separate build environment from runtime environment, reducing final image size and attack surface.
# Stage 1: Builder
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY prisma ./prisma/
RUN npm ci
COPY . .
RUN npm run build
RUN npx prisma generate
RUN npm prune --production
# Stage 2: Production
FROM node:18-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
USER nodejs
EXPOSE 3000
CMD ["node", "dist/server.js"]
| Stage | Base Image | Includes | Size |
|---|---|---|---|
| Builder | node:18 | TypeScript, dev deps, source code | ~800MB |
| Production | node:18-alpine | Compiled code, production deps | ~150MB |
| Slim | node:18-slim | Minimal dependencies | ~200MB |
| Distroless | gcr.io/distroless/nodejs | Only app + runtime | ~80MB |
# docker-compose.override.yml (development)
version: '3.8'
services:
api:
build:
context: .
target: development
volumes:
- .:/app
- /app/node_modules
environment:
NODE_ENV: development
NODE_OPTIONS: --inspect=0.0.0.0:9229
ports:
- "3000:3000"
- "9229:9229"
command: npm run dev
5. Theory: Docker Compose
# docker-compose.yml
version: '3.8'
services:
api:
build:
context: .
dockerfile: Dockerfile
target: production
image: myapp/api:latest
container_name: myapp-api
restart: unless-stopped
ports: ["3000:3000"]
environment:
NODE_ENV: production
PORT: 3000
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: myapp
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
REDIS_URL: redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks: [app-network]
postgres:
image: postgres:15-alpine
container_name: myapp-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes: [postgres_data:/var/lib/postgresql/data]
networks: [app-network]
redis:
image: redis:7-alpine
container_name: myapp-redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes: [redis_data:/data]
networks: [app-network]
networks:
app-network:
driver: bridge
volumes:
postgres_data:
redis_data:
# Environment variables management
version: '3.8'
services:
api:
build: .
env_file:
- .env
- .env.${NODE_ENV}
environment:
- NODE_ENV=${NODE_ENV}
- PORT=3000
ports:
- "${PORT:-3000}:3000"
# Resource limits
services:
api:
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
6. Advanced: Production Best Practices
# Hardened production Dockerfile
FROM node:18-alpine AS base
RUN apk update && apk upgrade && apk add --no-cache dumb-init
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
WORKDIR /app
COPY --chown=nodejs:nodejs package*.json ./
RUN npm ci --only=production --ignore-scripts && npm cache clean --force
COPY --chown=nodejs:nodejs . .
RUN rm -rf tests docs .git .github && find . -name "*.md" -type f -delete
USER nodejs
ENTRYPOINT ["dumb-init", "--"]
EXPOSE 3000
CMD ["node", "server.js"]
// health.js
const http = require('http');
const options = { host: 'localhost', port: 3000, path: '/health', timeout: 2000 };
const request = http.request(options, (res) => {
process.exit(res.statusCode === 200 ? 0 : 1);
});
request.on('error', () => process.exit(1));
request.end();
// logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [new winston.transports.Console()]
});
// Graceful shutdown
const express = require('express');
const app = express();
const server = app.listen(3000);
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10000);
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
const containerOptimizations = {
imageSize: {
alpine: 'Use Alpine variants',
distroless: 'Consider distroless for security',
multiStage: 'Exclude dev dependencies from runtime image'
},
security: {
nonRoot: 'USER node',
readOnly: 'Mount filesystem read-only where possible',
secrets: 'Use Docker secrets/env vars, not hardcoded values',
updates: 'Weekly rebuilds for base image patches'
},
observability: {
logging: 'JSON logs to stdout',
metrics: 'Prometheus endpoint',
tracing: 'OpenTelemetry'
}
};
7. Theory: Orchestration & Kubernetes
const orchestrationComparison = {
'Docker Compose': {
useCase: 'Development/small production/single host',
features: ['Single-host orchestration', 'Simple YAML', 'Local dev friendly'],
limits: ['No auto-failover', 'No zero-downtime rolling updates', 'Manual scaling']
},
'Kubernetes': {
useCase: 'Production/multi-host/microservices',
features: [
'Multi-host cluster',
'Self-healing',
'Rolling updates',
'Service discovery',
'Autoscaling'
],
complexity: ['Steeper learning curve', 'More YAML', 'Cluster operations']
}
};
# deployment.yaml (excerpt)
apiVersion: apps/v1
kind: Deployment
metadata:
name: nodejs-api
spec:
replicas: 3
strategy:
type: RollingUpdate
template:
spec:
containers:
- name: api
image: myregistry/nodejs-api:latest
ports:
- containerPort: 3000
resources:
requests: { memory: "256Mi", cpu: "250m" }
limits: { memory: "512Mi", cpu: "500m" }
8. Best Practices Checklist
# Best-practice Dockerfile
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
FROM dependencies AS build
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
COPY --from=dependencies --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=build --chown=nodejs:nodejs /app/dist ./dist
COPY --chown=nodejs:nodejs package*.json ./
RUN chmod -R 555 /app && chmod 777 /app/tmp
USER nodejs
EXPOSE 3000
CMD ["node", "dist/server.js"]
const productionChecklist = {
build: [
'✓ Use specific base image tags (not :latest)',
'✓ Implement multi-stage builds',
'✓ Copy only necessary files (.dockerignore)',
'✓ Run npm ci instead of npm install'
],
security: [
'✓ Run as non-root user',
'✓ Scan images for vulnerabilities',
'✓ Use secrets management',
'✓ Keep base images updated'
],
runtime: [
'✓ Implement health checks',
'✓ Set memory limits',
'✓ Use restart policy',
'✓ Handle SIGTERM gracefully'
],
monitoring: [
'✓ Expose /health endpoint',
'✓ Expose /metrics endpoint',
'✓ Structured JSON logs'
]
};
# Docker commands reference
docker build -t myapp:latest .
docker run -d --name myapp -p 3000:3000 myapp:latest
docker logs -f myapp
docker exec -it myapp sh
docker stats myapp
docker-compose up -d
docker-compose down -v
docker system prune -a --volumes
docker scan myapp:latest
| Image | Size | Use Case |
|---|---|---|
| node:latest | ~950MB | Development only |
| node:18-slim | ~200MB | Production with system deps |
| node:18-alpine | ~150MB | Production recommended |
| node:18-alpine + multi-stage | ~120MB | Production optimized |
| gcr.io/distroless/nodejs | ~80MB | Security-critical production |
# Environment-specific Compose
version: '3.8'
services:
api:
build:
target: production
image: myregistry/myapp:${TAG:-latest}
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1'
memory: 512M
10 Interview Questions + 10 MCQs
1Why use Docker with Node.js?easy
Answer: Consistent environments, isolated dependencies, and easier deployment/scaling.
2What is a multi-stage build?easy
Answer: A Docker build pattern using multiple stages to keep final runtime image small and secure.
3Why prefer `npm ci` in containers?medium
Answer: Deterministic installs from lockfile and generally faster CI builds.
4What does `.dockerignore` improve?easy
Answer: Smaller build context, faster builds, less accidental secret leakage.
5Why run container as non-root?medium
Answer: Reduces privilege and impact if the container is compromised.
6When choose Kubernetes over Compose?medium
Answer: Multi-host production needing auto-healing, rolling updates, and autoscaling.
7Why graceful shutdown in containers?hard
Answer: To finish in-flight requests and close resources when SIGTERM arrives.
8How do Docker layers help performance?medium
Answer: Cached layers avoid rebuilding unchanged steps and reduce push/pull data.
9What is container healthcheck for?easy
Answer: To report app readiness/liveness so orchestration can restart unhealthy containers.
10Why structured JSON logs for Dockerized apps?hard
Answer: Easier aggregation/search in centralized logging systems.
10 Docker MCQs
1
Container startup is usually:
Explanation: Containers start quickly compared to VMs.
2
`npm ci` is preferred because:
Explanation: CI builds should be repeatable and lockfile-driven.
3
`.dockerignore` helps by:
Explanation: Fewer files sent to daemon means faster/cleaner builds.
4
Best runtime security default:
Explanation: Non-root execution lowers risk exposure.
5
Multi-stage builds primarily reduce:
Explanation: Build tooling stays in builder stage, not runtime image.
6
Compose is best for:
Explanation: Compose excels at local/managed single-host orchestration.
7
Kubernetes adds:
Explanation: K8s provides production orchestration capabilities.
8
Health checks should verify:
Explanation: Probe endpoints should reflect real app health.
9
Best place to log in containers:
Explanation: Container platforms capture stdout/stderr streams.
10
Graceful shutdown handles:
Explanation: Shutdown hooks drain connections before exit.