|$ curl https://forge-ai.dev/api/markdown?path=docs/deploy/docker
$cat docs/docker-—-containerized-deployment.md
updated Recently·50 min read·published

Docker — Containerized Deployment

DevOpsContainersBeginner to Advanced🎯Free Tools
Introduction

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.

Dockerfile Basics

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.

Dockerfile
Dockerfile
1# Use an official lightweight Node.js image
2FROM node:20-alpine
3
4# Set working directory inside the container
5WORKDIR /app
6
7# Copy dependency manifests first (cached layer)
8COPY package.json package-lock.json ./
9
10# Install production dependencies only
11RUN npm ci --omit=dev
12
13# Copy application source code
14COPY . .
15
16# Expose the port the app listens on
17EXPOSE 3000
18
19# Set the default command to start the app
20CMD ["node", "server.js"]

info

Always use npm ci instead of npm install in Dockerfiles. It installs from the lockfile exactly, ensuring deterministic builds and faster execution.

Build and run the image:

terminal
Bash
1# Build the image with a tag
2docker build -t my-app:1.0 .
3
4# Run the container in detached mode
5docker run -d -p 3000:3000 --name my-app my-app:1.0
6
7# Verify it's running
8docker ps
9
10# View logs
11docker logs my-app
12
13# Stop and remove
14docker stop my-app && docker rm my-app
Multi-Stage Builds

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.

Dockerfile
Dockerfile
1# Stage 1: Build
2FROM node:20-alpine AS builder
3WORKDIR /app
4COPY package.json package-lock.json ./
5RUN npm ci
6COPY . .
7RUN npm run build
8
9# Stage 2: Production
10FROM node:20-alpine AS runner
11WORKDIR /app
12ENV NODE_ENV=production
13
14# Copy only built artifacts from builder
15COPY --from=builder /app/dist ./dist
16COPY --from=builder /app/node_modules ./node_modules
17COPY package.json ./
18
19EXPOSE 3000
20CMD ["node", "dist/server.js"]

For a Next.js application:

Dockerfile
Dockerfile
1# Build stage
2FROM node:20-alpine AS builder
3WORKDIR /app
4COPY package.json package-lock.json ./
5RUN npm ci
6COPY . .
7RUN npm run build
8
9# Production stage
10FROM node:20-alpine AS runner
11WORKDIR /app
12ENV NODE_ENV=production
13RUN addgroup --system --gid 1001 nodejs
14RUN adduser --system --uid 1001 nextjs
15
16COPY --from=builder /app/public ./public
17COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
18COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
19
20USER nextjs
21EXPOSE 3000
22ENV PORT=3000
23CMD ["node", "server.js"]

best practice

Next.js standalone output mode reduces the final image by up to 80%. Enable it in next.config.js with output: "standalone".
Layer Optimization

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.

Dockerfile
Dockerfile
1# BAD: Reinstalls dependencies on every code change
2FROM node:20-alpine
3WORKDIR /app
4COPY . .
5RUN npm ci
6RUN npm run build
7
8# GOOD: Dependencies cached until package.json changes
9FROM node:20-alpine
10WORKDIR /app
11COPY package.json package-lock.json ./
12RUN npm ci
13COPY . .
14RUN npm run build

Use a .dockerignore file to exclude unnecessary files from the build context:

.dockerignore
Bash
1node_modules
2.git
3.env
4.env.*
5dist
6build
7coverage
8*.md
9.github
10.vscode
11.DS_Store

warning

Never copy node_modules into your Docker image. Always run npm ci inside the container. Copying node_modules from your host can introduce platform-specific binaries that break in Linux containers.
Docker Compose

Docker Compose defines and runs multi-container applications using a declarative YAML file. It handles networking, volumes, and service dependencies in a single configuration.

docker-compose.yml
YAML
1services:
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
35volumes:
36 postgres_data:
37 redis_data:

Common Compose commands:

terminal
Bash
1# Build and start all services
2docker compose up -d
3
4# View running services
5docker compose ps
6
7# Follow logs for all services
8docker compose logs -f
9
10# Stop and remove containers, networks
11docker compose down
12
13# Rebuild images and start
14docker compose up -d --build
15
16# Scale a service
17docker compose up -d --scale worker=3
Health Checks

Health checks let Docker monitor container status and automatically restart unhealthy containers. They are essential for production reliability.

Dockerfile
Dockerfile
1# In Dockerfile
2HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
3 CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
health.js
JavaScript
1// Express health check endpoint
2app.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

Always include a start_period in your health check configuration. It gives your application time to initialize before Docker starts reporting health status, preventing false negative restarts.
Volumes & Persistence

Containers are ephemeral — all data is lost when they stop. Use volumes to persist data across container restarts and to share data between containers.

docker-compose.yml
YAML
1services:
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
18volumes:
19 uploads:
20 pgdata:
terminal
Bash
1# List all volumes
2docker volume ls
3
4# Inspect a volume
5docker volume inspect pgdata
6
7# Remove unused volumes
8docker volume prune
9
10# Backup a volume
11docker run --rm -v pgdata:/data -v $(pwd):/backup \
12 alpine tar czf /backup/pgdata-backup.tar.gz /data
Networking

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.

docker-compose.yml
YAML
1services:
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
20networks:
21 frontend:
22 backend:
23 internal: true # No external access — DB stays isolated

warning

Never expose database ports to the host in production. Use internal networks and only expose the services that need external access (like your API or frontend).
Security Hardening

Container security requires attention to user permissions, image provenance, secrets management, and runtime policies. These practices reduce the attack surface significantly.

Dockerfile
Dockerfile
1# Run as non-root user
2FROM node:20-alpine
3RUN addgroup -S appgroup && adduser -S appuser -G appgroup
4WORKDIR /app
5COPY --chown=appuser:appgroup . .
6USER appuser
7
8# Use specific image tags (never :latest in production)
9# Use official images from Docker Hub
10# Scan images for vulnerabilities regularly
docker-compose.yml
YAML
1services:
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
16secrets:
17 db_password:
18 file: ./secrets/db_password.txt

best practice

Use Docker secrets or environment files (not inline environment variables) to manage sensitive data. Never commit secrets to version control or embed them in Dockerfiles.
Production Checklist

Before deploying containers to production, ensure every item on this checklist is addressed.

CategoryRequirementStatus
ImagesMulti-stage builds, minimal base images, pinned tagsRequired
UsersNon-root user, no unnecessary capabilitiesRequired
SecretsNo secrets in images, use Docker secrets or vaultsRequired
HealthHEALTHCHECK in Dockerfile, proper start periodRequired
LoggingStructured logs to stdout/stderr, no file loggingRequired
Resource LimitsCPU and memory limits defined per serviceRequired
NetworkingInternal networks for backend services, no exposed DB portsRequired
ScanningVulnerability scanning with Trivy or SnykRecommended
docker-compose.yml
YAML
1services:
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"
Debugging Containers

When containers misbehave, Docker provides several tools for inspecting state, logs, and networking without modifying the image.

terminal
Bash
1# Exec into a running container
2docker exec -it my-app sh
3
4# View real-time logs
5docker logs -f --tail 100 my-app
6
7# Inspect container configuration
8docker inspect my-app
9
10# Check resource usage
11docker stats my-app
12
13# Copy files from container
14docker cp my-app:/app/logs/error.log ./error.log
15
16# Check network connectivity
17docker exec -it my-app wget -qO- http://db:5432
18
19# List running processes
20docker top my-app

info

Use docker exec -it to get a shell inside a running container for debugging. This is invaluable for checking file contents, environment variables, and network connectivity without rebuilding the image.
CI/CD Integration

Automate Docker image builds and deployments as part of your CI/CD pipeline. Build, test, scan, and push images on every commit.

.github/workflows/docker.yml
YAML
1name: Build and Deploy
2on:
3 push:
4 branches: [main]
5
6jobs:
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

Tag images with the Git SHA for traceability, and keep a latest tag for convenience. Scan every image before pushing — never deploy unscanned images to production.

Blueprint — Engineering Documentation · Section ID: DEP-DCR-01 · Revision: 1.0