Skip to content
AstroPaper
Go back

Docker & Docker Compose — Complete Deep-Dive Engineering Guide

Updated:
Edit page

Docker & Docker Compose — Complete Deep-Dive Engineering Guide

For a frontend engineer (React / Next.js / Astro / TypeScript) moving toward backend, DevOps, infrastructure, deployment, and platform engineering.


Table of Contents

Open Table of Contents

1. Big Picture

1.1 What Docker Is

Docker is a platform for building, shipping, and running applications inside containers — lightweight, isolated environments that package your code together with everything it needs to run: runtime, libraries, system tools, and configuration.

The React analogy: Think of a React component as a self-contained unit — it declares its own props, state, and rendering logic. A Docker container is the same idea applied to an entire application: it declares its own OS dependencies, runtime, files, and network ports. Just as you can render the same React component anywhere (browser, SSR, tests), you can run the same Docker container anywhere (your laptop, CI, staging, production).

1.2 Why Containers Exist

Before containers, deploying software meant:

Containers solve all of this. A container is a promise: “This application will run identically everywhere.”

Frontend analogy: You already understand this problem. When a teammate clones your repo and npm install gives different results because of OS differences or Node version mismatches — that’s the problem containers solve at the infrastructure level. Docker is like nix develop or a lockfile for your entire operating environment, not just your npm packages.

1.3 Key Distinctions

ConceptWhat it isAnalogy
ProcessA running program on your OSA single function executing
ContainerAn isolated process with its own filesystem, network, and resource limitsA sandboxed iframe — isolated from the host but sharing the kernel
Virtual MachineA full OS running on emulated hardwareA completely separate computer inside your computer
Docker EngineThe daemon (background service) that builds and runs containersLike the Node.js runtime — it interprets and executes
Docker DesktopGUI + Docker Engine + extras (VM on Mac/Windows)Like VS Code wrapping a language server
Docker ComposeA tool for defining and running multi-container apps from a YAML fileLike package.json scripts but for containers — declares which services to run together
KubernetesA container orchestration platform for running containers at scale across many machinesLike Vercel’s infrastructure — manages scaling, routing, and failover, but you control it

Container vs. VM — the critical difference:

Virtual Machine:                    Container:
┌─────────────────────┐            ┌─────────────────────┐
│     Your App        │            │     Your App        │
│     Libraries       │            │     Libraries       │
│     Guest OS        │  ← full OS │     (no guest OS)   │
│     Hypervisor      │            │     Docker Engine    │
│     Host OS         │            │     Host OS         │
│     Hardware        │            │     Hardware        │
└─────────────────────┘            └─────────────────────┘

VMs: heavy, slow to start, full isolation
Containers: lightweight, instant start, process-level isolation

1.4 Core Concepts

Image

A read-only template containing the application code, runtime, libraries, and filesystem. Images are built from a Dockerfile and stored in registries.

Analogy: An image is like a built dist/ folder from npm run build — it’s the immutable artifact. You don’t modify it; you create a new build.

Container

A running instance of an image. You can run multiple containers from the same image.

Analogy: An image is the class; a container is the instance. Or: the image is the Docker equivalent of your built Next.js app, and the container is that app actually running on a server.

Volume

Persistent storage that survives container restarts and removal. Used for databases, uploads, and any data that must persist.

Analogy: Like localStorage in the browser — the container (page) can be refreshed or destroyed, but the volume (localStorage) persists.

Network

Docker creates isolated networks for containers to communicate. Containers on the same network can reach each other by service name.

Analogy: Like a private LAN — containers on the same Docker network can talk to each other using hostnames, but they’re isolated from the outside by default.

Layer

Each instruction in a Dockerfile creates a filesystem layer. Layers are cached and shared between images, making builds fast and storage efficient.

Analogy: Like Git commits — each layer is a diff on top of the previous one. Docker caches unchanged layers so rebuilds only reprocess what changed.

Registry

A storage and distribution service for Docker images. Docker Hub is the public default. GitHub Container Registry (ghcr.io), AWS ECR, and Google Artifact Registry are common alternatives.

Analogy: Like npm registry for packages, but for container images.

Dockerfile

A text file with instructions to build an image. Each instruction (FROM, COPY, RUN, etc.) creates a layer.

Analogy: Like a next.config.js + build script — it declares how to construct the application artifact.

Build Context

The set of files sent to the Docker daemon when building an image. Controlled by what you pass to docker build and filtered by .dockerignore.

Analogy: Like the files Webpack/Vite processes — everything in the build context is available during the build. A .dockerignore is like a .gitignore for builds.

1.5 The Lifecycle: Source Code → Deployment

┌──────────────────────────────────────────────────────────────┐
│  SOURCE CODE                                                  │
│                                                              │
│  src/  package.json  Dockerfile  docker-compose.yml          │
│                                                              │
└──────────────────────────┬───────────────────────────────────┘
                           │ docker build

┌──────────────────────────────────────────────────────────────┐
│  IMAGE                                                        │
│                                                              │
│  Immutable artifact containing app + runtime + dependencies  │
│  Tagged: myapp:1.0.0  myapp:latest                           │
│                                                              │
└──────────────────────────┬───────────────────────────────────┘
                           │ docker push

┌──────────────────────────────────────────────────────────────┐
│  REGISTRY                                                     │
│                                                              │
│  Docker Hub / GHCR / ECR / Artifact Registry                 │
│  Stores and distributes images                               │
│                                                              │
└──────────────────────────┬───────────────────────────────────┘
                           │ docker pull + docker run

┌──────────────────────────────────────────────────────────────┐
│  CONTAINER(S)                                                 │
│                                                              │
│  Running instance(s) of the image                            │
│  Mapped ports, mounted volumes, connected networks           │
│  On: local machine / CI / staging / production               │
│                                                              │
└──────────────────────────────────────────────────────────────┘

1.6 Docker vs. Your Current Local Setup

DimensionLocal Node.jsDocker
RuntimeWhatever Node version is installedExact Node version declared in Dockerfile
Dependenciesnode_modules on your filesystemIsolated inside the container
OS librariesWhatever your OS hasDeclared and installed in the image
DatabaseInstall PostgreSQL/Redis locallyRun as a container alongside your app
Sharing setupREADME with “install these things”docker compose up — done
Reproducibility”Works on my machine”Guaranteed identical everywhere
CleanupLeftover processes, global installsdocker compose down — everything gone

1.7 Why Docker Is Useful for React / Next.js / Astro

ScenarioWithout DockerWith Docker
Local developmentInstall Node, npm, DB, Redis, etc.docker compose up
Team onboarding2-hour setup guidegit clone && docker compose up
CI/CDConfigure runner with exact toolchainBuild image, run tests inside it
Staging/productionConfigure server, install deps, prayDeploy the same image you tested
Running Next.js SSRInstall Node on server, manage processContainer with Node + Next.js, portable
Full-stack localFrontend + API + DB + Redis all manualOne docker-compose.yml defines everything

1.8 Mental Model Diagram

┌─────────────────────────────────────────────────┐
│               YOUR PROJECT                       │
│                                                 │
│  Dockerfile          docker-compose.yml         │
│  .dockerignore       .env                       │
│  src/  package.json  ...                        │
│                                                 │
└──────────────┬──────────────────────────────────┘

               │ docker compose up

┌─────────────────────────────────────────────────┐
│           DOCKER ENGINE                          │
│                                                 │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐     │
│  │ frontend │  │   api    │  │ postgres │     │
│  │ :3000    │  │ :4000    │  │ :5432    │     │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘     │
│       │              │              │           │
│       └──────────────┼──────────────┘           │
│                      │                          │
│              Docker Network                     │
│              (services find each other by name) │
│                                                 │
│  ┌──────────────────────────┐                   │
│  │    Volumes               │                   │
│  │    (persistent data)     │                   │
│  └──────────────────────────┘                   │
│                                                 │
└─────────────────────────────────────────────────┘

2. Learning Roadmap by Skill Level

Level 1 — Newbie

Goal: Run your first container, write your first Dockerfile, understand the basics.

Installing Docker

Verify installation:

docker --version
docker compose version
docker run hello-world

Understanding image vs. container

ImageContainer
Blueprint / recipeRunning instance
Read-onlyRead-write layer on top
Built from DockerfileCreated from image
Stored on diskRunning as a process
Can exist without runningExists only while alive (or stopped)

Basic Docker commands

# Run a container
docker run -it node:20-alpine node -e "console.log('Hello')"

# Run and expose a port
docker run -p 3000:3000 my-app

# List running containers
docker ps

# List all containers (including stopped)
docker ps -a

# Stop a container
docker stop <container-id>

# Remove a container
docker rm <container-id>

# List images
docker images

# Remove an image
docker rmi <image-id>

# Build an image
docker build -t my-app .

# View logs
docker logs <container-id>

# Execute a command inside a running container
docker exec -it <container-id> sh

Your first Dockerfile — a Node.js app

# Use an official Node.js runtime as the base image
FROM node:20-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy package files first (for layer caching)
COPY package.json package-lock.json ./

# Install dependencies
RUN npm ci

# Copy the rest of the source code
COPY . .

# Expose the port the app runs on
EXPOSE 3000

# Define the command to run the app
CMD ["node", "server.js"]

Build and run it:

docker build -t my-node-app .
docker run -p 3000:3000 my-node-app

Exposing ports

# Map host port 8080 to container port 3000
docker run -p 8080:3000 my-app

# Map multiple ports
docker run -p 3000:3000 -p 9229:9229 my-app

The rule: -p HOST:CONTAINER. The left side is your machine, the right side is inside the container.

Mounting files (bind mounts)

# Mount current directory into the container
docker run -v $(pwd):/app -p 3000:3000 my-app

# Read-only mount
docker run -v $(pwd)/config:/app/config:ro my-app

Common mistakes at this level

MistakeWhat happensFix
Forgetting to expose portsApp runs but you can’t reach itAdd -p flag or EXPOSE + -p
Not using .dockerignorenode_modules copied into image, slow build, huge imageCreate .dockerignore with node_modules, .git, dist
Using npm install instead of npm ciNon-deterministic installs, lockfile driftAlways use npm ci in Dockerfiles
COPY . . before npm ciBreaks layer cache — every code change reinstalls depsCopy package.json + lockfile first, then npm ci, then COPY . .
Running as rootSecurity riskAdd USER node (or create a non-root user)
Using node:20 instead of node:20-alpineImage is 1GB+ instead of ~180MBUse alpine or slim variants

5 beginner exercises

  1. Run a container: Pull and run nginx:alpine, visit it in your browser on port 8080.
  2. Build a Dockerfile: Create a simple Express server, Dockerize it, and run it.
  3. Layer caching: Build an image twice without changing code. Observe “CACHED” in the output. Then change one source file and rebuild — see which layers rebuild.
  4. Inspect a container: Run a container, use docker exec -it <id> sh to explore the filesystem inside it.
  5. Clean up: List all containers and images, stop everything, remove everything. (docker system prune)

Level 1 success criteria


Level 2 — Junior

Goal: Build multi-container applications, use Docker Compose, and optimize images.

Multi-stage Dockerfile

Multi-stage builds let you use one stage for building and another for running. This drastically reduces image size.

# Stage 1: Build
FROM node:20-alpine AS builder

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:20-alpine AS runner

WORKDIR /app
ENV NODE_ENV=production

# Copy only what's needed from the builder
COPY --from=builder /app/package.json /app/package-lock.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist

EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]

Why multi-stage?

Environment variables

# In Dockerfile
ENV NODE_ENV=production
ENV PORT=3000

# Override at runtime
docker run -e PORT=8080 -e DATABASE_URL=postgres://... my-app
# In docker-compose.yml
services:
  api:
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://db:5432/mydb
    env_file:
      - .env

Volumes

TypeSyntaxUse case
Named volumemy-data:/var/lib/postgresql/dataDatabase persistence
Bind mount./src:/app/srcLocal development — code syncing
tmpfstmpfs: /tmpTemporary data that shouldn’t persist
# docker-compose.yml
services:
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data # Named volume — persists
    environment:
      POSTGRES_PASSWORD: secret

volumes:
  pgdata: # Declare the named volume

Networks

Docker Compose creates a default network for all services. Services can reach each other by name.

services:
  web:
    build: ./frontend
    ports:
      - '3000:3000'
    depends_on:
      - api

  api:
    build: ./backend
    ports:
      - '4000:4000'
    environment:
      DATABASE_URL: postgres://db:5432/mydb # ← "db" is the service name
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: secret

Key insight: Inside the Docker network, db resolves to the PostgreSQL container’s IP. You never use localhost to reach another container — you use the service name.

Docker Compose basics

# docker-compose.yml
services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - '3000:3000'
    volumes:
      - ./frontend/src:/app/src # Hot reload
    environment:
      - NEXT_PUBLIC_API_URL=http://localhost:4000

  api:
    build: ./api
    ports:
      - '4000:4000'
    environment:
      - DATABASE_URL=postgres://postgres:secret@db:5432/mydb
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    ports:
      - '5432:5432'
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

Commands:

# Start all services
docker compose up

# Start in background
docker compose up -d

# Rebuild images and start
docker compose up --build

# Stop all services
docker compose down

# Stop and remove volumes (careful — deletes data!)
docker compose down -v

# View logs
docker compose logs -f api

# Run a one-off command
docker compose exec api sh

# Restart a single service
docker compose restart api

Debugging containers

# View logs
docker compose logs -f <service>

# Shell into a running container
docker compose exec <service> sh

# Inspect a container
docker inspect <container-id>

# Check resource usage
docker stats

# View networks
docker network ls
docker network inspect <network>

# Check if a port is actually exposed
docker port <container-id>

Optimizing image size

TechniqueImpactExample
Use alpine base~900MB → ~180MBFROM node:20-alpine
Multi-stage buildsRemove build tools from final imageSee multi-stage example above
.dockerignoreExclude unnecessary files from build contextSee .dockerignore section
npm ci --omit=devSkip devDependencies in productionRUN npm ci --omit=dev
Minimize layersFewer layers = smaller imageCombine RUN commands with &&
Use specific tagsAvoid pulling unexpected updatesnode:20.11-alpine not node:latest

5 mini project ideas

  1. React + Express + PostgreSQL: Full-stack app with Docker Compose. Frontend proxies to API, API talks to DB.
  2. Next.js with hot reload: Development Compose file with bind mounts for instant feedback.
  3. Multi-stage React build: Build React app, serve with Nginx in a tiny production image.
  4. API with Redis cache: Express API + Redis container, cached responses.
  5. Database migration workflow: Run migrations as a one-off command in Compose before starting the app.

Common mistakes and anti-patterns

Anti-patternWhy it’s badBetter approach
One giant container for everythingHard to scale, debug, and updateOne process per container
Not using multi-stage buildsImages are 1GB+ with dev toolsSeparate build and runtime stages
Bind-mounting node_modulesOverwrites container’s node_modules with host’s (OS mismatch)Use anonymous volume: - /app/node_modules
Hardcoding secrets in DockerfileSecrets baked into image layers permanentlyUse env vars or secrets at runtime
Using latest tagNon-reproducible — image changes silentlyPin to specific version tags
No .dockerignoreSlow builds, bloated context, accidental secret inclusionAlways create .dockerignore
depends_on without health checkService starts before dependency is readyUse condition: service_healthy
Storing data inside containersData lost on container removalUse volumes for persistence

Level 2 success criteria


Level 3 — Senior

Goal: Build production-ready Docker setups — secure, optimized, observable, and integrated with CI/CD.

Production Dockerfile strategy

# ============================================
# Production Dockerfile for Next.js
# ============================================

# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

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

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Create non-root user
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs

# Copy only production artifacts
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

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

CMD ["node", "server.js"]

Next.js standalone output requires output: 'standalone' in next.config.js. This produces a minimal server without node_modules — image size drops dramatically.

Security hardening

PracticeWhyHow
Non-root userPrevent container escape escalationUSER node or create a custom user
Read-only filesystemPrevent runtime modificationread_only: true in Compose
No secrets in imageSecrets persist in layers foreverUse runtime env vars or Docker secrets
Minimal base imageFewer packages = fewer vulnerabilitiesUse alpine or distroless
Pin image digestsPrevent supply chain attacksFROM node:20-alpine@sha256:abc123...
Scan imagesDetect known CVEsdocker scout, trivy, snyk container
Drop capabilitiesReduce kernel surfacecap_drop: [ALL] in Compose
No package managers in productionPrevent installing malware at runtimeRemove apk/apt in final stage or use distroless
# Security-hardened Compose service
services:
  api:
    build: .
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    user: '1001:1001'

Build cache optimization

Layer order matters enormously:

# ❌ BAD: Any source change invalidates npm ci cache
COPY . .
RUN npm ci

# ✅ GOOD: npm ci only reruns when package files change
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

Advanced: BuildKit cache mounts:

# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

This keeps the npm cache across builds even when the layer is invalidated.

Docker Compose for production-like environments

# docker-compose.prod.yml
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      target: runner
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=production
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
    healthcheck:
      test:
        ['CMD', 'wget', '--spider', '-q', 'http://localhost:3000/api/health']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '3'

Monorepo Docker strategy

monorepo/
├── apps/
│   ├── web/
│   │   └── Dockerfile
│   └── api/
│       └── Dockerfile
├── packages/
│   ├── ui/
│   └── shared/
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── docker-compose.yml
# apps/web/Dockerfile (monorepo-aware)
FROM node:20-alpine AS builder
WORKDIR /app

# Install pnpm
RUN corepack enable && corepack prepare pnpm@9 --activate

# Copy workspace config
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/web/package.json ./apps/web/
COPY packages/ui/package.json ./packages/ui/
COPY packages/shared/package.json ./packages/shared/

# Install all workspace dependencies
RUN pnpm install --frozen-lockfile

# Copy source
COPY apps/web ./apps/web
COPY packages/ui ./packages/ui
COPY packages/shared ./packages/shared

# Build
RUN pnpm --filter web build

# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./.next/static
COPY --from=builder /app/apps/web/public ./public

USER node
EXPOSE 3000
CMD ["node", "server.js"]

CI/CD integration

# GitHub Actions: Build and push Docker image
name: Docker Build

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:${{ github.sha }}
            ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Health checks

# In Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
# In docker-compose.yml
services:
  api:
    healthcheck:
      test: ['CMD-SHELL', 'curl -f http://localhost:4000/health || exit 1']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 15s

  db:
    image: postgres:16-alpine
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 5s
      timeout: 5s
      retries: 5

Logging and observability

services:
  api:
    logging:
      driver: json-file
      options:
        max-size: '10m' # Rotate at 10MB
        max-file: '3' # Keep 3 files


    # Alternatively, use a logging driver for centralized logging
    # logging:
    #   driver: fluentd
    #   options:
    #     fluentd-address: localhost:24224

Structured logging inside your app is critical:

// Use JSON logging in production
console.log(
  JSON.stringify({
    level: 'info',
    message: 'Request handled',
    method: 'GET',
    path: '/api/users',
    duration_ms: 42,
    timestamp: new Date().toISOString(),
  }),
);

Secret management

MethodSecurity levelUse case
Environment variablesLow-MediumNon-sensitive config, development
.env fileLowLocal development only
Docker secrets (Compose)MediumProduction Compose deployments
External secrets managerHighProduction (Vault, AWS Secrets Manager)
# Docker Compose secrets
services:
  api:
    secrets:
      - db_password
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt # Development
    # external: true                   # Production (pre-created)

Reverse proxy with Nginx / Traefik

# Nginx reverse proxy
services:
  nginx:
    image: nginx:alpine
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - web
      - api

  web:
    build: ./frontend
    expose:
      - '3000' # Not published to host — only accessible via nginx

  api:
    build: ./backend
    expose:
      - '4000'
# nginx/nginx.conf
events {}

http {
    upstream frontend {
        server web:3000;
    }

    upstream backend {
        server api:4000;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://frontend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        location /api/ {
            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

Traefik alternative (auto-discovers services via labels):

services:
  traefik:
    image: traefik:v3.0
    command:
      - '--providers.docker=true'
      - '--entrypoints.web.address=:80'
    ports:
      - '80:80'
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

  web:
    build: ./frontend
    labels:
      - 'traefik.http.routers.web.rule=Host(`myapp.localhost`)'
      - 'traefik.http.services.web.loadbalancer.server.port=3000'

5 production-grade project examples

  1. Next.js SSR + PostgreSQL + Redis: Three-stage Dockerfile for Next.js standalone, health checks, volume persistence, Nginx reverse proxy.
  2. Monorepo with pnpm: Workspace-aware Dockerfiles for multiple apps sharing packages, CI/CD pipeline building only changed services.
  3. Full-stack with background workers: Web app + API + database + worker service for async jobs (e.g., email sending, image processing).
  4. Multi-environment Compose: docker-compose.yml (base) + docker-compose.dev.yml (overrides for local dev) + docker-compose.prod.yml (production settings).
  5. Automated image pipeline: GitHub Actions builds, scans for vulnerabilities, pushes to GHCR, deploys to a VPS with docker compose pull + up.

Level 3 success criteria


Level 4 — Expert

Goal: Design container strategy at platform scale — reusable base images, orchestration decisions, supply chain security, multi-architecture, disaster recovery.

Designing reusable base images

Create organization-wide base images that embed your standards:

# base-images/node-base/Dockerfile
FROM node:20-alpine

# Security: non-root user
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 appuser

# Standard tools
RUN apk add --no-cache tini wget

# Healthcheck utility
COPY healthcheck.sh /usr/local/bin/healthcheck
RUN chmod +x /usr/local/bin/healthcheck

# Use tini as init system (proper signal handling)
ENTRYPOINT ["/sbin/tini", "--"]

USER appuser
WORKDIR /app

Teams then use:

FROM ghcr.io/my-org/node-base:20-1.0.0

COPY --chown=appuser:appgroup package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=appuser:appgroup . .
CMD ["node", "server.js"]

Benefits:

Docker vs. Kubernetes — decision strategy

DimensionDocker ComposeKubernetes
ComplexityLowHigh
Learning curveDaysWeeks to months
Best forSingle host, small teams, dev environmentsMulti-host, auto-scaling, large teams
ScalingManual (add replicas)Automatic (HPA)
NetworkingSimple service discoveryComplex but powerful (ingress, service mesh)
State managementVolumes on one hostPersistent Volume Claims across nodes
CI/CDSimple (compose up)Complex (Helm, ArgoCD, etc.)
When to use< 10 services, < 5 nodes, < 20 engineers> 10 services, multi-node, auto-scaling needed

Rule of thumb: Start with Docker Compose. Move to Kubernetes when you need multi-node scaling, auto-healing, or complex service mesh. Many production workloads never need Kubernetes.

Image lifecycle management

Build → Tag → Scan → Push → Deploy → Monitor → Rotate

Tag strategy:
  - git SHA: myapp:abc1234 (immutable, traceable)
  - semver: myapp:1.2.3 (release versions)
  - latest: myapp:latest (convenience, mutable — avoid in production)
  - branch: myapp:main (for staging, mutable)

Retention policy: Delete images older than N days, keep last N tagged releases. Use registry garbage collection.

Supply chain security

ThreatMitigation
Compromised base imagePin to digest, scan regularly, use official images
Malicious layer injectionVerify image provenance, sign images with cosign
Secrets in image layersNever COPY .env, use runtime secrets
Unpatched vulnerabilitiesAutomate scanning in CI, rebuild on base image updates
Untrusted registriesPull only from approved registries
# Scan an image for vulnerabilities
docker scout cves myapp:latest

# Sign an image
cosign sign ghcr.io/my-org/myapp:1.0.0

# Verify a signature
cosign verify ghcr.io/my-org/myapp:1.0.0

Multi-architecture builds

# Build for multiple architectures
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag ghcr.io/my-org/myapp:1.0.0 \
  --push .

This is essential for teams with mixed hardware (Intel Macs, Apple Silicon, ARM servers like AWS Graviton).

Advanced networking

services:
  web:
    networks:
      - frontend

  api:
    networks:
      - frontend
      - backend

  db:
    networks:
      - backend # ← web cannot reach db directly

networks:
  frontend:
  backend:

Network segmentation: The frontend can reach the API, the API can reach the database, but the frontend cannot reach the database directly. This is defense in depth.

Disaster recovery and rollback

ScenarioRecovery strategy
Bad image deployedRedeploy previous image tag
Database corruptionRestore from volume backup
Registry outageKeep local image cache, mirror critical images
Secret compromiseRotate secrets, redeploy all affected services
Host failureRun on multiple hosts, use orchestration

Rollback pattern:

# Rollback to previous version
docker compose pull        # gets latest tags
# OR specify the old version:
IMAGE_TAG=1.2.2 docker compose up -d

# Verify health
docker compose ps
docker compose logs --tail 50 api

Architecture review checklist

What expert engineers worry about that juniors miss

Expert concernJunior blind spot
Image provenance and signing”I just pull from Docker Hub”
Layer cache invalidation in CI”Why is the CI build so slow?”
Container resource limits”It works on my machine (with 32GB RAM)“
Log rotation and storageLogs fill disk, container crashes
Init system (tini/dumb-init)Zombie processes, signal handling bugs
DNS caching in containersStale DNS when a service restarts
node_modules platform mismatchmacOS bind mount + Linux container = broken native deps
Graceful shutdownApp doesn’t handle SIGTERM, connections drop
Image size driftImage grows from 200MB to 2GB over months
Volume backup strategy”We’ll deal with backups later”

10 advanced engineering discussion topics

  1. Init systems: Why should every container use tini or dumb-init? What happens to orphan processes without one?
  2. Distroless images: When should you use Google’s distroless images vs. Alpine? What do you lose?
  3. BuildKit secrets: How do you safely use secrets during build time (e.g., private npm registry tokens) without them persisting in layers?
  4. Container-native CI: Should your CI run inside containers, or should it build containers? What are the trade-offs?
  5. Sidecar pattern: When should you run a helper process in the same container vs. a separate sidecar container?
  6. Image promotion: Design a pipeline where an image is built once and promoted through dev → staging → production without rebuilding.
  7. Docker socket security: Why is mounting /var/run/docker.sock dangerous? When is it acceptable?
  8. Rootless Docker: What is rootless mode, and when should you use it?
  9. Container density: How many containers can you run on a single host before performance degrades? What are the limiting factors?
  10. Migration from Compose to Kubernetes: What signals indicate it’s time to migrate? What does the migration path look like?

3. Setup Guide

Step 1: Install Docker and Docker Compose

macOS:

# Install Docker Desktop (includes Docker Engine + Compose)
brew install --cask docker
# OR download from https://docker.com/products/docker-desktop

Linux (Ubuntu/Debian):

# Install Docker Engine
curl -fsSL https://get.docker.com | sh

# Add your user to the docker group (log out/in after)
sudo usermod -aG docker $USER

# Docker Compose v2 is included as a plugin
docker compose version

Verify:

docker --version          # Docker version 27.x
docker compose version    # Docker Compose version v2.x
docker run hello-world    # Should print a success message

Small project (single app)

my-app/
├── Dockerfile
├── .dockerignore
├── docker-compose.yml
├── docker-compose.dev.yml       # Development overrides
├── .env.example
├── src/
├── public/
├── package.json
└── package-lock.json

Medium project (frontend + backend)

my-project/
├── docker-compose.yml
├── docker-compose.dev.yml
├── .env.example
├── frontend/
│   ├── Dockerfile
│   ├── .dockerignore
│   ├── src/
│   ├── package.json
│   └── ...
├── backend/
│   ├── Dockerfile
│   ├── .dockerignore
│   ├── src/
│   ├── package.json
│   └── ...
└── nginx/
    └── nginx.conf

Large project (monorepo)

monorepo/
├── docker-compose.yml
├── docker-compose.dev.yml
├── docker-compose.prod.yml
├── .env.example
├── apps/
│   ├── web/
│   │   ├── Dockerfile
│   │   └── ...
│   ├── api/
│   │   ├── Dockerfile
│   │   └── ...
│   └── worker/
│       ├── Dockerfile
│       └── ...
├── packages/
│   ├── shared/
│   └── ui/
├── infra/
│   ├── nginx/
│   │   └── nginx.conf
│   └── postgres/
│       └── init.sql
├── package.json
└── pnpm-lock.yaml

Step 3: Example Dockerfiles

React app (Vite) — production

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Serve with Nginx
FROM nginx:alpine AS runner
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf for SPA
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /assets {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Image size comparison:

ApproachImage size
node:20 + serve~1.1 GB
node:20-alpine + serve~190 MB
Multi-stage + Nginx~25 MB

Next.js app — production

FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

Requires in next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
};
module.exports = nextConfig;

Astro app — production (static)

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine AS runner
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Node.js API — production

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 appuser

COPY --from=builder --chown=appuser:appgroup /app/package.json /app/package-lock.json ./
RUN npm ci --omit=dev
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist

USER appuser
EXPOSE 4000

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:4000/health || exit 1

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

Step 4: Example docker-compose.yml files

Frontend + Backend

services:
  frontend:
    build: ./frontend
    ports:
      - '3000:80'
    depends_on:
      - api

  api:
    build: ./backend
    ports:
      - '4000:4000'
    environment:
      - NODE_ENV=production

Frontend + Backend + Database

services:
  frontend:
    build: ./frontend
    ports:
      - '3000:80'
    depends_on:
      - api

  api:
    build: ./backend
    ports:
      - '4000:4000'
    environment:
      - DATABASE_URL=postgres://postgres:secret@db:5432/mydb
      - NODE_ENV=production
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    ports:
      - '5432:5432'
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

Next.js + PostgreSQL + Redis

services:
  web:
    build: .
    ports:
      - '3000:3000'
    environment:
      - DATABASE_URL=postgres://postgres:secret@db:5432/mydb
      - REDIS_URL=redis://redis:6379
      - NODE_ENV=production
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:
  redisdata:

Step 5: Development vs. production strategy

Use Compose overrides:

# docker-compose.yml (base — shared config)
services:
  web:
    build: .
    ports:
      - '3000:3000'
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
# docker-compose.dev.yml (development overrides)
services:
  web:
    build:
      target: deps # Use early stage with all deps
    volumes:
      - ./src:/app/src # Hot reload via bind mount
      - /app/node_modules # Anonymous volume — don't overwrite
    environment:
      - NODE_ENV=development
    command: npm run dev

  db:
    ports:
      - '5432:5432' # Expose DB port for local tools
# docker-compose.prod.yml (production overrides)
services:
  web:
    build:
      target: runner # Use final minimal stage
    environment:
      - NODE_ENV=production
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M

Usage:

# Development
docker compose -f docker-compose.yml -f docker-compose.dev.yml up

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Step 6: .dockerignore

# Dependencies
node_modules
.pnpm-store

# Build output
dist
.next
out
build

# Git
.git
.gitignore

# Docker
Dockerfile*
docker-compose*
.dockerignore

# IDE
.vscode
.idea
*.swp
*.swo

# Environment
.env
.env.local
.env*.local

# Tests (if not needed in image)
__tests__
*.test.ts
*.spec.ts
coverage
jest.config.*

# Misc
README.md
LICENSE
*.md

Step 7: Common scripts in package.json

{
  "scripts": {
    "docker:dev": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up",
    "docker:dev:build": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build",
    "docker:prod": "docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d",
    "docker:down": "docker compose down",
    "docker:clean": "docker compose down -v --rmi local",
    "docker:logs": "docker compose logs -f",
    "docker:shell": "docker compose exec web sh",
    "docker:db:shell": "docker compose exec db psql -U postgres"
  }
}

4. Cheatsheet

Docker commands

CommandPurpose
docker build -t name .Build image from Dockerfile
docker build -t name --target stage .Build specific stage
docker run -p 3000:3000 nameRun container with port mapping
docker run -d nameRun in background (detached)
docker run -it name shRun interactive shell
docker run -v $(pwd):/app nameRun with bind mount
docker run -e KEY=val nameRun with environment variable
docker run --rm nameRemove container after exit
docker psList running containers
docker ps -aList all containers
docker stop <id>Stop container
docker rm <id>Remove container
docker imagesList images
docker rmi <id>Remove image
docker logs <id>View container logs
docker logs -f <id>Follow logs (tail)
docker exec -it <id> shShell into running container
docker inspect <id>Detailed container info
docker statsLive resource usage
docker system pruneRemove unused data
docker system prune -aRemove all unused images too
docker system dfShow disk usage

Docker Compose commands

CommandPurpose
docker compose upStart all services
docker compose up -dStart in background
docker compose up --buildRebuild images then start
docker compose downStop and remove
docker compose down -vAlso remove volumes
docker compose psList services
docker compose logs -fFollow all logs
docker compose logs -f apiFollow specific service
docker compose exec api shShell into service
docker compose run api npm testRun one-off command
docker compose restart apiRestart a service
docker compose pullPull latest images
docker compose buildBuild all images
docker compose configValidate and display config
docker compose topShow running processes

Dockerfile syntax

InstructionPurposeExample
FROMBase imageFROM node:20-alpine
WORKDIRSet working directoryWORKDIR /app
COPYCopy files from contextCOPY package.json ./
RUNExecute a command during buildRUN npm ci
ENVSet environment variableENV NODE_ENV=production
ARGBuild-time variableARG NODE_VERSION=20
EXPOSEDocument the portEXPOSE 3000
CMDDefault command when container startsCMD ["node", "server.js"]
ENTRYPOINTMain executable (harder to override)ENTRYPOINT ["/sbin/tini", "--"]
USERSet the userUSER node
HEALTHCHECKDefine health checkSee health check section
LABELAdd metadataLABEL version="1.0"

Compose syntax

services:
  name:
    image: image:tag # Use existing image
    build: # OR build from Dockerfile
      context: .
      dockerfile: Dockerfile
      target: stage # Multi-stage target
      args:
        NODE_VERSION: 20
    ports:
      - 'host:container'
    expose:
      - '3000' # Internal only
    volumes:
      - ./src:/app/src # Bind mount
      - data:/app/data # Named volume
      - /app/node_modules # Anonymous volume
    environment:
      KEY: value
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped # Restart policy
    command: npm run dev # Override CMD
    working_dir: /app
    user: '1001:1001'
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:3000']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
    networks:
      - frontend
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '3'

volumes:
  data:

networks:
  frontend:

Environment variable patterns

# Inline
environment:
  - NODE_ENV=production
  - DATABASE_URL=postgres://db:5432/mydb

# Map syntax
environment:
  NODE_ENV: production
  DATABASE_URL: postgres://db:5432/mydb

# From file
env_file:
  - .env
  - .env.production

# Build args
build:
  args:
    NODE_VERSION: 20

Volume patterns

volumes:
  # Named volume (persistent, Docker-managed)
  - pgdata:/var/lib/postgresql/data

  # Bind mount (host directory)
  - ./src:/app/src

  # Bind mount read-only
  - ./config:/app/config:ro

  # Anonymous volume (prevent overwrite from bind mount)
  - /app/node_modules

  # tmpfs (in-memory, not persistent)
  # tmpfs:
  #   - /tmp

Network patterns

# Default — all services on one network
# (automatic, no config needed)

# Custom networks for isolation
services:
  web:
    networks: [frontend]
  api:
    networks: [frontend, backend]
  db:
    networks: [backend]

networks:
  frontend:
  backend:

Debugging commands

# Why won't my container start?
docker compose logs <service>

# What's inside the container?
docker compose exec <service> sh
docker compose exec <service> ls -la /app

# Is the port actually open?
docker compose exec <service> wget -qO- http://localhost:3000

# What environment variables are set?
docker compose exec <service> env

# Check container health
docker inspect --format='{{.State.Health.Status}}' <container-id>

# Check network connectivity
docker compose exec api ping db

# See what's using disk space
docker system df
docker system df -v

Common error messages

ErrorCauseFix
port is already allocatedAnother process uses the portChange the host port or stop the conflicting process
no space left on deviceDocker disk fulldocker system prune -a
COPY failed: file not foundFile not in build contextCheck path relative to Dockerfile, check .dockerignore
npm ERR! could not determine executableWrong WORKDIR or missing depsVerify WORKDIR and that npm ci ran
ECONNREFUSED localhostContainer trying to reach another via localhostUse the service name instead (db, api, etc.)
permission deniedNon-root user can’t access fileschown in Dockerfile or adjust permissions
exec format errorImage built for wrong architectureRebuild for the correct platform
OCI runtime create failedCorrupt image or incompatible baseRebuild the image, check base image

Performance tips

TipImpact
Order Dockerfile layers by change frequencyMaximize cache hits
Use .dockerignoreSmaller build context, faster builds
Use multi-stage buildsSmaller production images
Use Alpine base images~900MB → ~180MB
Use BuildKit (DOCKER_BUILDKIT=1)Parallel stage building, cache mounts
Combine RUN commandsFewer layers
Use npm ci --omit=devExclude devDependencies
Cache mount for npmPersist npm cache across builds

Security tips

TipPriority
Run as non-root userCritical
Use minimal base imagesHigh
Pin image versionsHigh
Scan images for CVEsHigh
Never put secrets in DockerfileCritical
Use .dockerignore (exclude .env)High
Make filesystem read-onlyMedium
Drop all capabilitiesMedium
Use no-new-privilegesMedium
Set resource limitsMedium

5. Real-World Engineering Mindset

Local development environment

Problem: “It takes a new engineer half a day to set up the project.”

Strategies:

StrategyProsCons
README with manual stepsSimple, no Docker knowledge neededFragile, OS-dependent, drift
Docker Compose for everythingOne command, identical everywhereSlower feedback loop (build time), Docker knowledge needed
Docker for services only (DB, Redis)Best of both worlds — native Node.js speed + containerized infrastructureSlightly more setup
Nix/devbox for toolchain + Docker for servicesReproducible toolchain + containerized servicesHigher learning curve

Senior choice: Docker for infrastructure (database, Redis, message queue), native Node.js for the application. This gives you hot reload speed while ensuring everyone has identical backing services. Use docker compose up db redis alongside npm run dev.


Sharing the same environment across team members

Problem: “It works on my machine but not on yours.”

Strategies:

StrategyProsCons
Docker Compose for devIdentical environment, one commandSlower than native for some workflows
Docker for infra onlyFast dev loop, consistent servicesApp-level differences still possible
Dev containers (VS Code)Full IDE + container integrationVS Code-specific, heavier

Senior choice: Commit docker-compose.dev.yml to the repo. New engineers run docker compose -f docker-compose.yml -f docker-compose.dev.yml up and have a working environment in minutes.


Running React + API + DB together

Problem: Frontend needs a backend and database running locally.

services:
  frontend:
    build:
      context: ./frontend
      target: deps
    volumes:
      - ./frontend/src:/app/src
      - /app/node_modules
    ports:
      - '3000:3000'
    command: npm run dev
    environment:
      - VITE_API_URL=http://localhost:4000

  api:
    build:
      context: ./backend
      target: deps
    volumes:
      - ./backend/src:/app/src
      - /app/node_modules
    ports:
      - '4000:4000'
    command: npm run dev
    environment:
      - DATABASE_URL=postgres://postgres:secret@db:5432/mydb
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    ports:
      - '5432:5432'
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

Key patterns:


Optimizing frontend image size

Problem: Your React image is 1.2 GB.

Strategies:

TechniqueBeforeAfter
Switch from node to node:alpine1.1 GB190 MB
Multi-stage: build in Node, serve with Nginx190 MB25 MB
Add .dockerignoreSlow build, bloated contextFast build
npm ci --omit=devAll deps in imageOnly prod deps

Senior choice: Multi-stage build. Build with Node, copy dist/ into Nginx. Result: ~25 MB image that serves static files with proper caching headers.


Running Next.js SSR in Docker

Problem: Next.js needs a Node.js runtime, not just a static file server.

Strategy: Use Next.js standalone output mode.

// next.config.js
module.exports = { output: 'standalone' };

This produces a self-contained server.js with bundled dependencies. The Docker image only needs Node.js + this output — no node_modules directory.

Result: ~120 MB image (vs. 800 MB+ without standalone).

Hidden pitfall: Static assets (.next/static and public/) are NOT included in the standalone output. You must copy them separately in the Dockerfile.


Hot reload in Docker

Problem: You change a file, but the app inside Docker doesn’t update.

Strategies:

StrategyProsCons
Bind mount src/Simple, works with Vite/Next.jsFile watching can be slow on macOS
Use pollingWorks on all OSCPU overhead
Docker for infra only, app runs nativelyBest hot reload performanceLess “containerized”
# docker-compose.dev.yml
services:
  web:
    volumes:
      - ./src:/app/src
      - /app/node_modules # ← prevent host node_modules from overwriting
    environment:
      - WATCHPACK_POLLING=true # For Next.js on macOS
      - CHOKIDAR_USEPOLLING=true # For Vite/CRA on macOS
    command: npm run dev

Senior choice: For development, run Node.js natively and only containerize infrastructure. Reserve full containerization for CI and production.


Database persistence

Problem: You run docker compose down and your database is empty.

Solution: Named volumes.

services:
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Rules:

Backup:

# Backup
docker compose exec db pg_dump -U postgres mydb > backup.sql

# Restore
docker compose exec -T db psql -U postgres mydb < backup.sql

Reverse proxy setup

Problem: Multiple services on one host, need routing by domain or path.

SolutionProsCons
Nginx (manual config)Full control, well understoodManual config for each service
Traefik (auto-discovery)Automatic via Docker labelsMore complex initial setup
Caddy (auto-HTTPS)Simple config, auto TLSLess ecosystem

Senior choice: Traefik for development and small production. Nginx for high-traffic production where you need fine-grained control.


Background workers

Problem: You need async job processing alongside your web app.

services:
  web:
    build: .
    command: node dist/server.js
    ports:
      - '3000:3000'

  worker:
    build: . # Same image!
    command: node dist/worker.js # Different entrypoint
    environment:
      - REDIS_URL=redis://redis:6379

  redis:
    image: redis:7-alpine

Key insight: The web and worker services use the same image but different commands. This is a common pattern — build once, run with different entrypoints.


Deploying containers

Strategies:

StrategyComplexityBest for
docker compose up on VPSLowSmall projects, personal apps
Docker SwarmMediumSmall-medium production
KubernetesHighLarge-scale, auto-scaling
Cloud container services (ECS, Cloud Run, Fly.io)MediumManaged infrastructure
Coolify / CapRoverLow-MediumSelf-hosted PaaS

Senior choice for a small team: VPS + Docker Compose + GitHub Actions. Simple, cheap, and sufficient for most apps.

Deployment script pattern:

#!/bin/bash
# deploy.sh — run on the server
cd /opt/myapp
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f

CI/CD pipeline with Docker

# GitHub Actions
name: Build and Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: deploy
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /opt/myapp
            export IMAGE_TAG=${{ github.sha }}
            docker compose pull
            docker compose up -d --remove-orphans

Monorepo strategy

Problem: Multiple apps share packages; you don’t want to rebuild everything on every change.

Strategies:

StrategyProsCons
One Dockerfile per app, copy workspaceSimpleLarge build context
Turbo prune + DockerOnly includes needed packagesTurborepo-specific
Shared base image + app layersReuses common depsMore complex image management

Turborepo prune pattern:

FROM node:20-alpine AS pruner
WORKDIR /app
RUN npm install -g turbo
COPY . .
RUN turbo prune --scope=web --docker

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/package-lock.json ./
RUN npm ci
COPY --from=pruner /app/out/full/ .
RUN npx turbo build --filter=web

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./.next/static
COPY --from=builder /app/apps/web/public ./public
CMD ["node", "server.js"]

Multi-environment setup

Problem: Dev, staging, and production need different configurations.

Strategy: Compose file layering.

# Development
docker compose -f docker-compose.yml -f docker-compose.dev.yml up

# Staging
docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Or use .env files:

# .env.staging
IMAGE_TAG=staging
NODE_ENV=staging
DATABASE_URL=postgres://...

# .env.production
IMAGE_TAG=v1.2.3
NODE_ENV=production
DATABASE_URL=postgres://...
docker compose --env-file .env.production up -d

Secret management

Problem: Database passwords, API keys, tokens — where do they go?

MethodSecurityUse case
.env fileLowLocal development only
Environment variablesMediumSimple production
Docker secretsMedium-HighDocker Swarm / Compose
External manager (Vault, AWS SM)HighEnterprise production

Senior choice: .env for local dev (gitignored), environment variables from CI/CD secrets for production, external secrets manager for sensitive production systems.


Scaling containers

Problem: One container can’t handle the load.

services:
  api:
    build: .
    deploy:
      replicas: 3
    expose:
      - '4000'

  nginx:
    image: nginx:alpine
    ports:
      - '80:80'
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - api
# Scale manually
docker compose up -d --scale api=3

For real scaling, move to Kubernetes, Docker Swarm, or a managed container platform (ECS, Cloud Run, Fly.io).


6. Brainstorm / Open Questions

Architecture

  1. Should this service live in the same container or a different one?
  2. When does a monolith in one container make more sense than microservices?
  3. How do you decide which services need their own Dockerfile vs. using off-the-shelf images?
  4. Should your development container match your production container exactly?
  5. When should you use a sidecar container vs. adding functionality to the main container?
  6. How should shared libraries in a monorepo be handled in Docker builds?
  7. What is the right boundary between “Docker Compose is enough” and “we need Kubernetes”?

Scaling

  1. How would this architecture change if traffic increased 100×?
  2. At what point should you move from docker compose --scale to a proper orchestrator?
  3. How do you handle database connection pooling when scaling app containers?
  4. What is the cost difference between scaling with bigger containers vs. more containers?
  5. How do you handle sessions and state when running multiple container instances?
  6. What happens when your build takes 15 minutes? How do you reduce it?
  7. How many containers can you realistically run on a single VPS?

Security

  1. What data should never be stored inside a container?
  2. How do you handle secrets that are needed at build time (e.g., private npm registry tokens)?
  3. What happens if a container is compromised — what can the attacker reach?
  4. How do you ensure your base images don’t have known vulnerabilities?
  5. When should you use image signing and verification?
  6. How do you prevent a container from accessing other containers’ data?
  7. What is the risk of running Docker as root on the host?
  8. How do you audit what containers are running and what they can access?

DX / Maintainability

  1. Can a new engineer get the project running in under 10 minutes?
  2. How do you prevent “Dockerfile rot” — Dockerfiles that gradually become outdated?
  3. When should you use a bind mount vs. a volume vs. rebuilding the image?
  4. How do you handle node_modules platform differences between macOS and Linux?
  5. What is the best strategy for hot reload in Docker?
  6. How do you keep Docker Compose files DRY across multiple environments?
  7. Should Dockerfile and docker-compose.yml live in the app repo or a separate infra repo?
  8. How do you debug a container that starts and immediately crashes?

Cost

  1. How much does a container-based VPS deployment cost compared to Vercel?
  2. When does the complexity of Docker outweigh its benefits?
  3. Is there a project size below which Docker is not worth it?
  4. How do you reduce Docker image storage costs in registries?
  5. What is the cost of slow Docker builds in developer productivity?

Reliability

  1. What happens when a container runs out of memory?
  2. How do you handle database migrations during container deployment?
  3. What is your rollback strategy if a new container version is broken?
  4. How do you ensure containers restart after a host reboot?
  5. What happens if the Docker daemon itself crashes?
  6. How do you monitor container health in production?
  7. What is the blast radius if your container registry goes down?

Deployment Strategy

  1. When should you use docker compose in production vs. a managed container service?
  2. How do you handle zero-downtime deployments with Docker Compose?
  3. Should you deploy the same image to all environments or build per-environment?
  4. How do you handle database schema changes during container updates?
  5. What is the right tagging strategy for production images?
  6. When should you use rolling updates vs. blue-green deployments?
  7. How do you coordinate deployments across multiple dependent services?

7. Practice Questions

Beginner (Level 1)

Q1. True/False: A Docker image is a running instance of a container.

AnswerFalse. A Docker container is a running instance of an image. The image is the blueprint; the container is the running process.

Q2. Single choice: What does docker run -p 8080:3000 myapp do?

AnswerB. The format is -p HOST:CONTAINER. Host port 8080 is mapped to container port 3000.

Q3. Fill in the blank: The file that tells Docker which files to exclude from the build context is called ._________.

Answerdockerignore — The file is .dockerignore.

Q4. Multiple choice: Which base image will give you the smallest Node.js image?

AnswerC. node:20-alpine is ~180 MB. node:20 and node:20-bookworm are ~1.1 GB. node:20-slim is ~250 MB.

Q5. Debugging: You build a Dockerfile but your app says “Cannot find module ‘express’.” What is the most likely cause?

AnswerThe npm ci or npm install step is missing, or it ran before COPY package.json, or .dockerignore excludes package.json or package-lock.json.

Q6. Single choice: Why should you COPY package.json and npm ci BEFORE COPY . .?

AnswerC. Docker caches layers. If package.json hasn't changed, npm ci uses the cached layer. If you COPY . . first, any source code change invalidates the cache for all subsequent layers.

Q7. Matching: Match the Dockerfile instruction to its purpose.

InstructionPurpose
A. FROM1. Run a command during build
B. COPY2. Set the base image
C. RUN3. Default command when container starts
D. CMD4. Add files from build context
AnswerA→2, B→4, C→1, D→3

Q8. True/False: EXPOSE 3000 in a Dockerfile automatically makes the container accessible on port 3000 from the host.

AnswerFalse. EXPOSE is documentation only. You must use -p 3000:3000 when running the container to actually publish the port.

Q9. Scenario: You run docker build -t myapp . and it takes 10 minutes because it reinstalls node_modules every time. Nothing in package.json changed. What is wrong?

AnswerThe Dockerfile likely has COPY . . before RUN npm ci. Any source code change invalidates the layer cache for the COPY, which invalidates all subsequent layers including npm ci. Fix: copy package.json and lockfile first, run npm ci, then COPY . ..

Q10. Fill in the blank: To use npm ci instead of npm install in a Dockerfile, you must have a _ file committed.

Answerpackage-lock.json (or equivalent lockfile). npm ci requires a lockfile to perform a deterministic install.

Junior (Level 2)

Q11. True/False: A container can access another container on the same Docker Compose network using localhost.

AnswerFalse. Containers on the same Docker network reach each other by service name, not localhost. localhost inside a container refers to the container itself.

Q12. Single choice: What is the purpose of a multi-stage Docker build?

AnswerB. Multi-stage builds separate the build environment (with dev tools, source code) from the runtime environment (minimal, production-only files). This dramatically reduces image size.

Q13. Debugging: Your Docker Compose app’s API says “ECONNREFUSED 127.0.0.1:5432” when trying to reach PostgreSQL. PostgreSQL is running. What is wrong?

AnswerThe API is trying to connect to localhost (127.0.0.1) instead of the service name. In Docker Compose, containers reach each other by service name. Change the connection string from localhost:5432 to db:5432 (or whatever the Postgres service name is).

Q14. Matching: Match the volume type to its use case.

TypeUse case
A. Named volume1. Temporary data that shouldn’t persist
B. Bind mount2. Database persistence across restarts
C. tmpfs3. Syncing source code for hot reload
AnswerA→2, B→3, C→1

Q15. Fill in the blank: To prevent the host’s node_modules from overwriting the container’s when using a bind mount, add an _ volume for /app/node_modules.

AnswerAnonymous. Adding - /app/node_modules as a volume (without a host path) creates an anonymous volume that preserves the container's node_modules.

Q16. Scenario: You add depends_on: [db] but your API crashes on startup because the database isn’t ready yet. Why?

Answerdepends_on only waits for the container to start, not for the service inside to be ready. PostgreSQL may take seconds to initialize. Use depends_on with condition: service_healthy and define a healthcheck on the database service.

Q17. Multiple choice: Which of these should be in .dockerignore? (Select all)

AnswerA, B, C, and E. node_modules is rebuilt inside the container. .git is unnecessary and large. .env may contain secrets. Dockerfile isn't needed inside the image. src/ is usually needed for the build.

Q18. Single choice: What does docker compose down -v do that docker compose down does not?

AnswerC. The -v flag removes named volumes. This deletes persistent data like database contents. Use with caution.

Q19. True/False: Using the latest tag for base images in production Dockerfiles is a best practice.

AnswerFalse. latest is mutable — it changes when a new version is published. This makes builds non-reproducible. Pin to specific versions like node:20.11-alpine.

Q20. Scenario: You want to run npx prisma migrate deploy once before your API starts, but only in Docker Compose. How would you approach this?

Answer Options:
1. Add a migrate service in Compose with command: npx prisma migrate deploy and make the API depends_on the migrate service.
2. Use a startup script that runs migrations then starts the app.
3. Run docker compose run api npx prisma migrate deploy as a separate step before docker compose up.
Option 1 is cleanest for automation.

Senior / Expert (Level 3–4)

Q21. Scenario: Your production Docker image is 2 GB and builds take 15 minutes. What should you investigate first?

Answer 1. Base image: Switch from node:20 (1.1 GB) to node:20-alpine (~180 MB).
2. Multi-stage build: Are dev dependencies and build tools in the final image?
3. .dockerignore: Is node_modules, .git, or test files being copied?
4. Layer order: Is npm ci being invalidated on every code change?
5. BuildKit cache: Are you using --mount=type=cache for npm?

Q22. True/False: Running a container as root inside the container also gives it root access to the host.

AnswerFalse — by default, container namespaces isolate root inside the container from the host. However, running as root inside the container increases the blast radius if a container escape vulnerability exists. Always use a non-root user for defense in depth.

Q23. Design question: You have a Next.js app, a Node.js API, a PostgreSQL database, and a Redis cache. Design the Docker networking so the frontend cannot directly access the database.

Answer Use two networks:
frontend-net: web + api
backend-net: api + db + redis
The API is on both networks. The web service can reach the API but not the database or Redis directly. This is network segmentation / defense in depth.

Q24. Single choice: Your container starts, passes health checks, but users report slow responses. docker stats shows the container using 490 MB of a 512 MB memory limit. What is happening?

AnswerB. The container is running near its memory limit. Node.js garbage collection becomes aggressive and frequent, causing latency spikes. Increase the memory limit or optimize the app's memory usage. Also consider setting Node's --max-old-space-size.

Q25. Scenario: A developer accidentally committed a .env file with database credentials to the Docker image three months ago. They removed it in a later commit. Is the secret safe?

AnswerNo. Docker image layers are additive. The .env file exists in an earlier layer even though it was removed in a later one. Anyone with access to the image can extract it with docker history or layer inspection tools. The image must be rebuilt from scratch, the secret must be rotated, and .env must be in .dockerignore.

Q26. Fill in the blank: To handle SIGTERM properly in a Docker container (for graceful shutdown), you should use an _ system like tini or dumb-init as the ENTRYPOINT.

AnswerInit. Docker sends SIGTERM to PID 1 when stopping a container. If PID 1 is your Node.js app, it may not handle signals correctly (especially if started via npm scripts). An init system like tini forwards signals properly and reaps zombie processes.

Q27. Debugging: You deploy a new container version. The old container stops. The new container starts but fails its health check. Now no containers are serving traffic. What went wrong?

Answer The deployment strategy lacks overlap. Options to fix:
1. Blue-green: Start the new container, verify health, then stop the old one.
2. Rolling update: Only remove old instances as new ones pass health checks.
3. Health check start period: Allow the new container time to initialize before checking health.
4. Rollback automation: Automatically redeploy the old image if the new one fails health checks.

Q28. Multiple choice: Which are valid reasons to pin a Docker base image to a digest (SHA) instead of a tag? (Select all)

AnswerA, C, and D. Tags can be moved to point to different images. Digests are immutable references. They don't affect pull speed (B is incorrect).

Q29. Scenario: You need a private npm token during docker build to install private packages, but you don’t want the token in any image layer. How do you solve this?

Answer Use BuildKit secret mounts:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/app/.npmrc npm ci
Build with:
docker build --secret id=npmrc,src=$HOME/.npmrc .
The secret is mounted during the RUN command but never persisted in any layer.

Q30. Design question: Your team has 15 microservices in a monorepo. CI builds all 15 images on every commit. Builds take 45 minutes. How would you redesign this?

Answer 1. Change detection: Only build images for services affected by the commit (path filters + dependency analysis).
2. Shared base image: Common dependencies in a base image, service-specific layers on top.
3. BuildKit cache: Use GitHub Actions cache (cache-from: type=gha) to reuse layers across builds.
4. Parallel builds: Build independent services concurrently.
5. Turbo prune: Use Turborepo to create minimal build contexts per service.
Combined: 45 minutes → 5-10 minutes for most commits.

8. Personalized Recommendations

Docker patterns most useful for your stack

Based on React, Next.js, Astro, TypeScript, and static files:

  1. Multi-stage builds — Build with Node, serve with Nginx (React/Astro) or standalone Node (Next.js). This is your most important pattern.
  2. Docker Compose for local infrastructure — Run PostgreSQL, Redis, and other services with one command while developing natively.
  3. Next.js standalone output — Use output: 'standalone' for minimal Docker images with SSR.
  4. Development Compose overrides — Bind mounts for hot reload, separate from production config.
  5. CI/CD image pipeline — Build, scan, tag, push to registry in GitHub Actions.
  6. .dockerignore — Essential for every project to keep builds fast and images clean.

What to learn first (priority order)

PriorityTopicWhy
1Dockerfile basics + docker build + docker runFoundation of everything
2.dockerignore + layer cachingPrevent pain early
3Multi-stage buildsImage size is the #1 frontend Docker problem
4Docker Compose for local dev (DB, Redis)Practical daily use
5Environment variables + secretsConfiguration management
6Volumes (named + bind mount)Data persistence + hot reload
7Networks + service communicationMulti-service architecture
8Health checksProduction readiness
9CI/CD integration (build + push)Deployment pipeline
10Security hardeningProduction safety

Best practice projects

ProjectWhat you learn
Dockerize a React app (Vite)Dockerfile, multi-stage, Nginx serving
Dockerize a Next.js appStandalone output, SSR in container
Docker Compose: Next.js + PostgreSQLCompose, volumes, networking, health checks
Full stack: React + Express + Postgres + RedisMulti-service architecture
CI/CD: Build image in GitHub Actions, deploy to VPSEnd-to-end pipeline

Common mistakes frontend engineers make with Docker

MistakeWhy it happensFix
1 GB+ imagesUsing node:20 baseUse node:20-alpine + multi-stage
Slow builds every timeWrong layer orderCopy lockfile first, then source
”Works in Docker but not locally” (or vice versa)Different Node versions, env varsMatch versions, use .env consistently
Using localhost between containersFrontend mental modelUse service names in Docker networks
Bind-mounting node_modulesHabit from local devUse anonymous volume to prevent overwrite
Putting secrets in DockerfileQuick and dirtyUse env vars, secrets, or BuildKit mounts
No .dockerignoreDidn’t know it existsAlways create one with node_modules, .git, .env
Ignoring image scanning”I trust the base image”Scan regularly — images have CVEs
No health checks”It starts, it works”Add health checks for reliable orchestration
Running as rootDefault behaviorAdd USER node or create a non-root user

30-day learning plan

Week 1: Foundations (Days 1–7)

DayTaskDeliverable
1Install Docker, run hello-world, explore Docker DesktopWorking installation
2Learn basic commands: run, ps, stop, rm, images, logs, execCommand fluency
3Write first Dockerfile for a simple Express serverWorking Dockerfile
4Learn .dockerignore and layer cachingOptimized build
5Dockerize a React (Vite) app with multi-stage build + Nginx~25 MB production image
6Dockerize a Next.js app with standalone outputWorking Next.js container
7Learn port mapping, environment variables, bind mountsConfiguration control

Week 2: Docker Compose (Days 8–14)

DayTaskDeliverable
8Write first docker-compose.yml — app + PostgreSQLMulti-container setup
9Learn volumes — persist database dataData survives restarts
10Learn networks — service-to-service communicationAPI connects to DB by name
11Add Redis as a third serviceThree-service architecture
12Set up development overrides with bind mounts for hot reloadFast dev feedback
13Learn depends_on with health checksReliable service ordering
14Debug a broken Compose setup — practice logs, exec, inspectDebugging confidence

Week 3: Production readiness (Days 15–21)

DayTaskDeliverable
15Security: non-root user, minimal base, read-only filesystemHardened Dockerfile
16Add health checks to all servicesObservable containers
17Set up Nginx reverse proxyProduction-like routing
18Environment management: .env files, Compose overridesMulti-environment config
19Build and push image in GitHub ActionsCI/CD pipeline
20Deploy to a VPS with docker compose pull && upEnd-to-end deployment
21Learn image scanning (docker scout)Security awareness

Week 4: Advanced patterns (Days 22–30)

DayTaskDeliverable
22Set up a full-stack project: Next.js + API + Postgres + RedisComplex architecture
23Optimize build times with BuildKit cache mountsFaster CI builds
24Study monorepo Docker strategyScalable structure
25Add background worker service (same image, different command)Worker pattern
26Set up logging and resource limitsProduction observability
27Study Docker vs. Kubernetes decision criteriaArchitecture knowledge
28Design a rollback strategyRecovery mechanism
29Review all Dockerfiles against the security checklistHardening pass
30Write an architecture decision record for your Docker setupDocumentation

Summary, Next Steps, and Advanced Topics

Concise Summary

Docker packages applications into portable, reproducible containers. Docker Compose orchestrates multiple containers on a single host. For a frontend engineer, the key insight is:

  1. Images are immutable artifacts — like dist/ from a build.
  2. Containers are running instances — like a server process.
  3. Multi-stage builds are essential — build big, run small.
  4. Compose replaces the “install all dependencies” README with one command.
  5. Treat containers like cattle, not pets — disposable, replaceable, stateless (data goes in volumes).

Next Steps

  1. Dockerize your current project today — start with a multi-stage Dockerfile.
  2. Add Docker Compose for local infrastructure (database, cache).
  3. Create .dockerignore and optimize layer caching.
  4. Set up a CI/CD pipeline that builds and pushes images.
  5. Harden your images: non-root user, alpine base, health checks.
  6. Deploy a containerized app to a VPS or cloud service.

Suggested Advanced Topics

TopicWhy it matters
Kubernetes basicsNext step after outgrowing Docker Compose
Helm chartsPackage and version Kubernetes deployments
Service mesh (Istio, Linkerd)Observability and security between services
Container security scanning (Trivy, Snyk)Automated vulnerability detection
Image signing and verification (cosign)Supply chain integrity
Multi-architecture builds (buildx)ARM support (Apple Silicon, AWS Graviton)
Rootless DockerEnhanced host security
Distroless imagesAbsolute minimal attack surface
BuildKit advanced featuresSecrets, SSH forwarding, cache mounts
Container observability (Prometheus, Grafana)Production monitoring and alerting
Docker-in-Docker (DinD) vs. Docker-out-of-DockerCI/CD container build strategies
Init systems (tini, dumb-init)Proper signal handling and zombie reaping
Container runtime alternatives (Podman, containerd)Broader ecosystem understanding
GitOps with containers (ArgoCD, Flux)Declarative deployment management
Fly.io / Railway / RenderManaged container platforms for small teams

Edit page
Share this post:

Previous Post
Cross-Site Request Forgery (CSRF)
Next Post
Error Tracking