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
  2. Theory: Docker Architecture
  3. Basic: Dockerfile Optimization
  4. Theory: Multi-stage Builds
  5. Theory: Docker Compose
  6. Advanced: Production Best Practices
  7. Theory: Orchestration & Kubernetes
  8. Best Practices Checklist
  9. Interview Q&A + MCQ
  10. Contextual Learning Links

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                        │
└─────────────────────────────────────────────────────────────┘
AspectContainersVirtual Machines
Guest OSNone (shared kernel)Full OS per VM
Startup TimeMillisecondsSeconds to minutes
SizeMB (10-100MB)GB (1-10GB)
Resource Overhead1-5%10-20%
IsolationProcess-levelHardware-level
PortabilityHighMedium
PerformanceNear-nativeSome 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"]
StageBase ImageIncludesSize
Buildernode:18TypeScript, dev deps, source code~800MB
Productionnode:18-alpineCompiled code, production deps~150MB
Slimnode:18-slimMinimal dependencies~200MB
Distrolessgcr.io/distroless/nodejsOnly 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
ImageSizeUse Case
node:latest~950MBDevelopment only
node:18-slim~200MBProduction with system deps
node:18-alpine~150MBProduction recommended
node:18-alpine + multi-stage~120MBProduction optimized
gcr.io/distroless/nodejs~80MBSecurity-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:

AMilliseconds
BHours
CAlways minutes
DUnpredictable
Explanation: Containers start quickly compared to VMs.
2

`npm ci` is preferred because:

ADeterministic lockfile install
BIt ignores lockfile
CIt installs globally only
DIt skips dependencies
Explanation: CI builds should be repeatable and lockfile-driven.
3

`.dockerignore` helps by:

AReducing build context size
BIncreasing image size
CDisabling Docker cache
DRunning tests
Explanation: Fewer files sent to daemon means faster/cleaner builds.
4

Best runtime security default:

ARun as root
BRun as non-root user
CDisable health checks
DExpose all ports
Explanation: Non-root execution lowers risk exposure.
5

Multi-stage builds primarily reduce:

AFinal image size/attack surface
BNeed for health endpoint
CNetwork ports
DNode.js event loop
Explanation: Build tooling stays in builder stage, not runtime image.
6

Compose is best for:

ASingle-host stacks and development
BGlobal edge routing
CKernel patching
DContainer image scanning
Explanation: Compose excels at local/managed single-host orchestration.
7

Kubernetes adds:

AAutoscaling + rolling updates + self-healing
BNo networking
CNo YAML
DNo health checks
Explanation: K8s provides production orchestration capabilities.
8

Health checks should verify:

AService responsiveness/readiness
BOnly container name
CGit status
DDisk partition labels
Explanation: Probe endpoints should reflect real app health.
9

Best place to log in containers:

Astdout/stderr
BRandom temp file only
CHidden directory only
DNowhere
Explanation: Container platforms capture stdout/stderr streams.
10

Graceful shutdown handles:

ASIGTERM and in-flight requests
BOnly build-time args
COnly Docker daemon crash
DOnly static file serving
Explanation: Shutdown hooks drain connections before exit.