Docker — Containerized Deployment
Docker packages applications into standardized units called containers that include everything needed to run: code, runtime, libraries, and system dependencies. Containers are lightweight, portable, and run consistently across any environment — your laptop, staging servers, or production clusters.
This guide covers writing Dockerfiles, multi-stage builds for optimized images, Docker Compose for multi-service environments, and production hardening techniques.
A Dockerfile is a text file containing instructions that Docker uses to build an image. Each instruction creates a layer in the image, and layers are cached to speed up subsequent builds.
| 1 | # Use an official lightweight Node.js image |
| 2 | FROM node:20-alpine |
| 3 | |
| 4 | # Set working directory inside the container |
| 5 | WORKDIR /app |
| 6 | |
| 7 | # Copy dependency manifests first (cached layer) |
| 8 | COPY package.json package-lock.json ./ |
| 9 | |
| 10 | # Install production dependencies only |
| 11 | RUN npm ci --omit=dev |
| 12 | |
| 13 | # Copy application source code |
| 14 | COPY . . |
| 15 | |
| 16 | # Expose the port the app listens on |
| 17 | EXPOSE 3000 |
| 18 | |
| 19 | # Set the default command to start the app |
| 20 | CMD ["node", "server.js"] |
info
Build and run the image:
| 1 | # Build the image with a tag |
| 2 | docker build -t my-app:1.0 . |
| 3 | |
| 4 | # Run the container in detached mode |
| 5 | docker run -d -p 3000:3000 --name my-app my-app:1.0 |
| 6 | |
| 7 | # Verify it's running |
| 8 | docker ps |
| 9 | |
| 10 | # View logs |
| 11 | docker logs my-app |
| 12 | |
| 13 | # Stop and remove |
| 14 | docker stop my-app && docker rm my-app |
Multi-stage builds let you separate the build environment from the runtime environment, dramatically reducing image size. The final image contains only production artifacts — no compilers, dev tools, or source maps.
| 1 | # Stage 1: Build |
| 2 | FROM node:20-alpine AS builder |
| 3 | WORKDIR /app |
| 4 | COPY package.json package-lock.json ./ |
| 5 | RUN npm ci |
| 6 | COPY . . |
| 7 | RUN npm run build |
| 8 | |
| 9 | # Stage 2: Production |
| 10 | FROM node:20-alpine AS runner |
| 11 | WORKDIR /app |
| 12 | ENV NODE_ENV=production |
| 13 | |
| 14 | # Copy only built artifacts from builder |
| 15 | COPY --from=builder /app/dist ./dist |
| 16 | COPY --from=builder /app/node_modules ./node_modules |
| 17 | COPY package.json ./ |
| 18 | |
| 19 | EXPOSE 3000 |
| 20 | CMD ["node", "dist/server.js"] |
For a Next.js application:
| 1 | # Build stage |
| 2 | FROM node:20-alpine AS builder |
| 3 | WORKDIR /app |
| 4 | COPY package.json package-lock.json ./ |
| 5 | RUN npm ci |
| 6 | COPY . . |
| 7 | RUN npm run build |
| 8 | |
| 9 | # Production stage |
| 10 | FROM node:20-alpine AS runner |
| 11 | WORKDIR /app |
| 12 | ENV NODE_ENV=production |
| 13 | RUN addgroup --system --gid 1001 nodejs |
| 14 | RUN adduser --system --uid 1001 nextjs |
| 15 | |
| 16 | COPY --from=builder /app/public ./public |
| 17 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ |
| 18 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static |
| 19 | |
| 20 | USER nextjs |
| 21 | EXPOSE 3000 |
| 22 | ENV PORT=3000 |
| 23 | CMD ["node", "server.js"] |
best practice
Each Dockerfile instruction creates a layer. Docker caches layers and reuses them on subsequent builds. Order instructions from least frequently changing to most frequently changing for optimal cache utilization.
| 1 | # BAD: Reinstalls dependencies on every code change |
| 2 | FROM node:20-alpine |
| 3 | WORKDIR /app |
| 4 | COPY . . |
| 5 | RUN npm ci |
| 6 | RUN npm run build |
| 7 | |
| 8 | # GOOD: Dependencies cached until package.json changes |
| 9 | FROM node:20-alpine |
| 10 | WORKDIR /app |
| 11 | COPY package.json package-lock.json ./ |
| 12 | RUN npm ci |
| 13 | COPY . . |
| 14 | RUN npm run build |
Use a .dockerignore file to exclude unnecessary files from the build context:
| 1 | node_modules |
| 2 | .git |
| 3 | .env |
| 4 | .env.* |
| 5 | dist |
| 6 | build |
| 7 | coverage |
| 8 | *.md |
| 9 | .github |
| 10 | .vscode |
| 11 | .DS_Store |
warning
Docker Compose defines and runs multi-container applications using a declarative YAML file. It handles networking, volumes, and service dependencies in a single configuration.
| 1 | services: |
| 2 | app: |
| 3 | build: . |
| 4 | ports: |
| 5 | - "3000:3000" |
| 6 | environment: |
| 7 | - DATABASE_URL=postgresql://user:pass@db:5432/mydb |
| 8 | - REDIS_URL=redis://cache:6379 |
| 9 | depends_on: |
| 10 | db: |
| 11 | condition: service_healthy |
| 12 | cache: |
| 13 | condition: service_started |
| 14 | restart: unless-stopped |
| 15 | |
| 16 | db: |
| 17 | image: postgres:16-alpine |
| 18 | volumes: |
| 19 | - postgres_data:/var/lib/postgresql/data |
| 20 | environment: |
| 21 | - POSTGRES_USER=user |
| 22 | - POSTGRES_PASSWORD=pass |
| 23 | - POSTGRES_DB=mydb |
| 24 | healthcheck: |
| 25 | test: ["CMD-SHELL", "pg_isready -U user -d mydb"] |
| 26 | interval: 10s |
| 27 | timeout: 5s |
| 28 | retries: 5 |
| 29 | |
| 30 | cache: |
| 31 | image: redis:7-alpine |
| 32 | volumes: |
| 33 | - redis_data:/data |
| 34 | |
| 35 | volumes: |
| 36 | postgres_data: |
| 37 | redis_data: |
Common Compose commands:
| 1 | # Build and start all services |
| 2 | docker compose up -d |
| 3 | |
| 4 | # View running services |
| 5 | docker compose ps |
| 6 | |
| 7 | # Follow logs for all services |
| 8 | docker compose logs -f |
| 9 | |
| 10 | # Stop and remove containers, networks |
| 11 | docker compose down |
| 12 | |
| 13 | # Rebuild images and start |
| 14 | docker compose up -d --build |
| 15 | |
| 16 | # Scale a service |
| 17 | docker compose up -d --scale worker=3 |
Health checks let Docker monitor container status and automatically restart unhealthy containers. They are essential for production reliability.
| 1 | # In Dockerfile |
| 2 | HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ |
| 3 | CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 |
| 1 | // Express health check endpoint |
| 2 | app.get("/health", (req, res) => { |
| 3 | const checks = { |
| 4 | database: checkDatabaseConnection(), |
| 5 | redis: checkRedisConnection(), |
| 6 | memory: process.memoryUsage().heapUsed < 512 * 1024 * 1024, |
| 7 | uptime: process.uptime(), |
| 8 | }; |
| 9 | |
| 10 | const healthy = Object.entries(checks).every( |
| 11 | ([key, val]) => key === "uptime" || val === true |
| 12 | ); |
| 13 | |
| 14 | res.status(healthy ? 200 : 503).json({ |
| 15 | status: healthy ? "healthy" : "unhealthy", |
| 16 | checks, |
| 17 | timestamp: new Date().toISOString(), |
| 18 | }); |
| 19 | }); |
best practice
Containers are ephemeral — all data is lost when they stop. Use volumes to persist data across container restarts and to share data between containers.
| 1 | services: |
| 2 | app: |
| 3 | image: my-app |
| 4 | volumes: |
| 5 | # Named volume for uploads |
| 6 | - uploads:/app/uploads |
| 7 | # Bind mount for development |
| 8 | - ./src:/app/src |
| 9 | # Read-only mount for config |
| 10 | - ./config.json:/app/config.json:ro |
| 11 | |
| 12 | db: |
| 13 | image: postgres:16-alpine |
| 14 | volumes: |
| 15 | # Named volume for database persistence |
| 16 | - pgdata:/var/lib/postgresql/data |
| 17 | |
| 18 | volumes: |
| 19 | uploads: |
| 20 | pgdata: |
| 1 | # List all volumes |
| 2 | docker volume ls |
| 3 | |
| 4 | # Inspect a volume |
| 5 | docker volume inspect pgdata |
| 6 | |
| 7 | # Remove unused volumes |
| 8 | docker volume prune |
| 9 | |
| 10 | # Backup a volume |
| 11 | docker run --rm -v pgdata:/data -v $(pwd):/backup \ |
| 12 | alpine tar czf /backup/pgdata-backup.tar.gz /data |
Docker creates isolated networks for container communication. Services on the same network can reach each other by service name, while remaining isolated from other networks.
| 1 | services: |
| 2 | frontend: |
| 3 | image: nginx:alpine |
| 4 | ports: |
| 5 | - "80:80" |
| 6 | networks: |
| 7 | - frontend |
| 8 | - backend |
| 9 | |
| 10 | api: |
| 11 | image: node:20-alpine |
| 12 | networks: |
| 13 | - backend |
| 14 | |
| 15 | db: |
| 16 | image: postgres:16-alpine |
| 17 | networks: |
| 18 | - backend |
| 19 | |
| 20 | networks: |
| 21 | frontend: |
| 22 | backend: |
| 23 | internal: true # No external access — DB stays isolated |
warning
Container security requires attention to user permissions, image provenance, secrets management, and runtime policies. These practices reduce the attack surface significantly.
| 1 | # Run as non-root user |
| 2 | FROM node:20-alpine |
| 3 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup |
| 4 | WORKDIR /app |
| 5 | COPY --chown=appuser:appgroup . . |
| 6 | USER appuser |
| 7 | |
| 8 | # Use specific image tags (never :latest in production) |
| 9 | # Use official images from Docker Hub |
| 10 | # Scan images for vulnerabilities regularly |
| 1 | services: |
| 2 | app: |
| 3 | image: my-app |
| 4 | security_opt: |
| 5 | - no-new-privileges:true |
| 6 | read_only: true |
| 7 | tmpfs: |
| 8 | - /tmp |
| 9 | cap_drop: |
| 10 | - ALL |
| 11 | cap_add: |
| 12 | - NET_BIND_SERVICE |
| 13 | secrets: |
| 14 | - db_password |
| 15 | |
| 16 | secrets: |
| 17 | db_password: |
| 18 | file: ./secrets/db_password.txt |
best practice
Before deploying containers to production, ensure every item on this checklist is addressed.
| Category | Requirement | Status |
|---|---|---|
| Images | Multi-stage builds, minimal base images, pinned tags | Required |
| Users | Non-root user, no unnecessary capabilities | Required |
| Secrets | No secrets in images, use Docker secrets or vaults | Required |
| Health | HEALTHCHECK in Dockerfile, proper start period | Required |
| Logging | Structured logs to stdout/stderr, no file logging | Required |
| Resource Limits | CPU and memory limits defined per service | Required |
| Networking | Internal networks for backend services, no exposed DB ports | Required |
| Scanning | Vulnerability scanning with Trivy or Snyk | Recommended |
| 1 | services: |
| 2 | app: |
| 3 | image: my-app:1.2.3 # Pinned version, never :latest |
| 4 | deploy: |
| 5 | resources: |
| 6 | limits: |
| 7 | cpus: "1.0" |
| 8 | memory: 512M |
| 9 | reservations: |
| 10 | cpus: "0.25" |
| 11 | memory: 128M |
| 12 | restart: unless-stopped |
| 13 | logging: |
| 14 | driver: json-file |
| 15 | options: |
| 16 | max-size: "10m" |
| 17 | max-file: "3" |
When containers misbehave, Docker provides several tools for inspecting state, logs, and networking without modifying the image.
| 1 | # Exec into a running container |
| 2 | docker exec -it my-app sh |
| 3 | |
| 4 | # View real-time logs |
| 5 | docker logs -f --tail 100 my-app |
| 6 | |
| 7 | # Inspect container configuration |
| 8 | docker inspect my-app |
| 9 | |
| 10 | # Check resource usage |
| 11 | docker stats my-app |
| 12 | |
| 13 | # Copy files from container |
| 14 | docker cp my-app:/app/logs/error.log ./error.log |
| 15 | |
| 16 | # Check network connectivity |
| 17 | docker exec -it my-app wget -qO- http://db:5432 |
| 18 | |
| 19 | # List running processes |
| 20 | docker top my-app |
info
Automate Docker image builds and deployments as part of your CI/CD pipeline. Build, test, scan, and push images on every commit.
| 1 | name: Build and Deploy |
| 2 | on: |
| 3 | push: |
| 4 | branches: [main] |
| 5 | |
| 6 | jobs: |
| 7 | build: |
| 8 | runs-on: ubuntu-latest |
| 9 | steps: |
| 10 | - uses: actions/checkout@v4 |
| 11 | |
| 12 | - name: Build image |
| 13 | run: docker build -t my-app:$\{{ github.sha }} . |
| 14 | |
| 15 | - name: Run tests |
| 16 | run: docker run --rm my-app:$\{{ github.sha }} npm test |
| 17 | |
| 18 | - name: Scan for vulnerabilities |
| 19 | uses: aquasecurity/trivy-action@master |
| 20 | with: |
| 21 | image-ref: my-app:$\{{ github.sha }} |
| 22 | severity: CRITICAL,HIGH |
| 23 | exit-code: 1 |
| 24 | |
| 25 | - name: Push to registry |
| 26 | run: | |
| 27 | echo "$\{{ secrets.DOCKER_PASSWORD }}" | docker login -u "$\{{ secrets.DOCKER_USERNAME }}" --password-stdin |
| 28 | docker tag my-app:$\{{ github.sha }} myregistry/my-app:latest |
| 29 | docker push myregistry/my-app:latest |
best practice
Blueprint — Engineering Documentation · Section ID: DEP-DCR-01 · Revision: 1.0