docker

>

duthaho/claudekit1 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: docker
description: >
license: MIT
---

# Docker

## When to Use

- Containerizing applications
- Local development environments
- CI/CD pipelines

## When NOT to Use

- Serverless-only deployments where containers are not part of the architecture (e.g., pure AWS Lambda, Cloudflare Workers)
- Local development without containers where native tooling is preferred
- Simple scripts or utilities that do not need isolation or reproducible environments

---

## Core Patterns

### 1. Multi-Stage Builds

Multi-stage builds separate build-time dependencies from the runtime image, producing
smaller, more secure containers.

#### Python (builder + slim runtime)

```dockerfile
# ---- Build stage ----
FROM python:3.12-slim AS builder

WORKDIR /build

# Install build-only dependencies (gcc, etc.) needed by some wheels
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc libpq-dev && \
    rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ---- Runtime stage ----
FROM python:3.12-slim

WORKDIR /app

# Copy only the installed packages from the builder
COPY --from=builder /install /usr/local

# Copy application code
COPY src/ ./src/
COPY main.py .

# Run as non-root
RUN addgroup --system app && adduser --system --ingroup app app
USER app

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```

#### Node.js (build + nginx/alpine)

```dockerfile
# ---- Build stage ----
FROM node:20-alpine AS builder

WORKDIR /app

# Install dependencies first for layer caching
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile

# Copy source and build
COPY tsconfig.json ./
COPY src/ ./src/
COPY public/ ./public/
RUN pnpm build

# ---- Runtime stage (static site served by nginx) ----
FROM nginx:1.27-alpine

# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Copy built assets from builder
COPY --from=builder /app/dist /usr/share/nginx/html

# Run as non-root
RUN chown -R nginx:nginx /usr/share/nginx/html && \
    chown -R nginx:nginx /var/cache/nginx && \
    chown -R nginx:nginx /var/log/nginx && \
    touch /var/run/nginx.pid && \
    chown -R nginx:nginx /var/run/nginx.pid
USER nginx

EXPOSE 8080

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

CMD ["nginx", "-g", "daemon off;"]
```

#### Node.js (API server with alpine runtime)

```dockerfile
# ---- Build stage ----
FROM node:20-alpine AS builder

WORKDIR /app

COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile

COPY tsconfig.json ./
COPY src/ ./src/
RUN pnpm build

# Prune dev dependencies for a lighter production node_modules
RUN pnpm prune --prod

# ---- Runtime stage ----
FROM node:20-alpine

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

RUN addgroup -S app && adduser -S app -G app
USER app

EXPOSE 3000

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

CMD ["node", "dist/index.js"]
```

#### Go (build + scratch)

```dockerfile
# ---- Build stage ----
FROM golang:1.22-alpine AS builder

WORKDIR /build

# Download dependencies first for caching
COPY go.mod go.sum ./
RUN go mod download

# Copy source and build a static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server

# ---- Runtime stage (scratch = empty image) ----
FROM scratch

# Copy CA certificates for HTTPS calls
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy the static binary
COPY --from=builder /app/server /server

EXPOSE 8080

ENTRYPOINT ["/server"]
```

---

### 2. Docker Compose for Development

A full-featured Compose file with services, volumes, networks, healthchecks, and
environment variable management.

```yaml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: builder          # Use builder stage for dev with hot-reload
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
      DATABASE_URL: postgresql://user:pass@db:5432/app
      REDIS_URL: redis://redis:6379
    env_file:
      - .env.local             # Local overrides (gitignored)
    volumes:
      - .:/app                 # Bind-mount source for hot-reload
      - /app/node_modules      # Anonymous volume to preserve node_modules
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - backend
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d app"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - backend

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    networks:
      - backend

  worker:
    build:
      context: .
      dockerfile: Dockerfile.worker
    environment:
      DATABASE_URL: postgresql://user:pass@db:5432/app
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - backend
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

networks:
  backend:
    driver: bridge
```

---

### 3. Layer Caching

Docker caches each layer. If a layer has not changed, every layer after it is also
cached. Order instructions from least-frequently-changed to most-frequently-changed.

#### Optimal instruction order

```dockerfile
FROM python:3.12-slim

WORKDIR /app

# 1. System dependencies (rarely change)
RUN apt-get update && apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

# 2. Dependency manifests (change when adding packages)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 3. Application code (changes most often)
COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
```

#### .dockerignore patterns

Always include a `.dockerignore` to keep the build context small and avoid leaking
secrets into layers.

```
# Version control
.git
.gitignore

# Dependencies (rebuilt inside container)
node_modules
__pycache__
*.pyc
.venv
venv

# Build output
dist
build
*.egg-info

# IDE and editor files
.vscode
.idea
*.swp
*.swo

# Environment and secrets
.env
.env.*
*.pem
*.key

# Docker files (not needed in context)
Dockerfile*
docker-compose*
.dockerignore

# Documentation and misc
README.md
CHANGELOG.md
LICENSE
docs/
```

---

### 4. Health Checks

Health checks let Docker (and orchestrators like Compose/Swarm/K8s) know when a
container is actually ready to serve traffic.

#### HTTP health check with curl

```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -f http://localhost:8000/health || exit 1
```

#### HTTP health check with wget (alpine images without curl)

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

#### TCP port check (for non-HTTP services)

```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD nc -z localhost 5432 || exit 1
```

#### Python-native check (no extra binaries needed)

```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
```

**Parameter reference:**

| Parameter        | Description                                      | Default |
|------------------|--------------------------------------------------|---------|
| `--interval`     | Time between checks                              | 30s     |
| `--timeout`      | Max time for a single check                      | 30s     |
| `--start-period` | Grace period before checks count as failures     | 0s      |
| `--retries`      | Consecutive failures before marking unhealthy    | 3       |

---

### 5. Security Hardening

#### Run as non-root user

```dockerfile
# Debian/Ubuntu based images
RUN addgroup --system app && adduser --system --ingroup app app
USER app

# Alpine based images
RUN addgroup -S app && adduser -S app -G app
USER app
```

#### Use minimal base images

| Base Image         | Size    | Use Case                              |
|--------------------|---------|---------------------------------------|
| `alpine`           | ~5 MB   | General minimal base                  |
| `*-slim`           | ~50 MB  | Debian-based with fewer packages      |
| `distroless`       | ~20 MB  | Google's no-shell, no-package-manager |
| `scratch`          | 0 MB    | Static binaries only (Go, Rust)       |

```dockerfile
# Distroless for Python
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /app /app
CMD ["main.py"]
```

#### Never put secrets in image layers

```dockerfile
# BAD - secret is baked into image history
COPY .env /app/.env
RUN echo "API_KEY=secret123" >> /app/.env

# GOOD - pass secrets at runtime
CMD ["python", "main.py"]
# docker run -e API_KEY=secret123 myapp
# or docker run --env-file .env myapp
```

#### Multi-stage to exclude build tools

Build tools (compilers, package managers, source code) stay in the builder stage
and never reach the runtime image. This reduces attack surface and image size.

```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build && pnpm prune --prod

FROM node:20-alpine
WORKDIR /app
# Only the built output and production deps are copied
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/index.js"]
```

---

### 6. Environment Configuration

#### ARG vs ENV

| Directive | Available at | Persists in image | Use for                     |
|-----------|-------------|-------------------|-----------------------------|
| `ARG`     | Build time  | No                | Build-time variables        |
| `ENV`     | Build + run | Yes               | Runtime configuration       |

```dockerfile
# ARG - only available during build
ARG NODE_ENV=production
ARG BUILD_VERSION=unknown

# ENV - available at build and runtime
ENV NODE_ENV=${NODE_ENV}
ENV APP_VERSION=${BUILD_VERSION}

# Build with: docker build --build-arg BUILD_VERSION=1.2.3 .
```

#### .env files with Compose

```yaml
services:
  app:
    build: .
    # Single .env file
    env_file:
      - .env

    # Multiple files (later files override earlier ones)
    env_file:
      - .env.defaults
      - .env.local

    # Inline environment variables (override env_file)
    environment:
      LOG_LEVEL: debug
      DEBUG: "true"
```

#### Secrets management with Docker Compose

```yaml
services:
  app:
    build: .
    secrets:
      - db_password
      - api_key
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    environment: API_KEY    # Read from host environment
```

Inside the container, secrets are mounted at `/run/secrets/<name>` as files.

---

### 7. Networking

#### Bridge networks for service isolation

```yaml
services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    networks:
      - frontend-net
      - backend-net     # Can reach the API

  api:
    build: ./api
    ports:
      - "8000:8000"
    networks:
      - backend-net     # Reachable by frontend and workers

  db:
    image: postgres:16-alpine
    networks:
      - backend-net     # Only reachable by api and workers
    # No ports exposed to host

  worker:
    build: ./worker
    networks:
      - backend-net

networks:
  frontend-net:
    driver: bridge
  backend-net:
    driver: bridge
```

#### Service discovery

Within a Docker Compose network, services reach each other by **service name**
as the hostname.

```python
# In the api service, connect to db using its service name
DATABASE_URL = "postgresql://user:pass@db:5432/app"

# In the frontend service, call the api by service name
API_URL = "http://api:8000"
```

#### Exposing ports

```yaml
services:
  app:
    ports:
      - "3000:3000"             # host:container, binds to 0.0.0.0
      - "127.0.0.1:3000:3000"  # bind to localhost only (more secure)
    expose:
      - "3000"                  # expose to other containers only, not host
```

---

## Best Practices

1. **Use multi-stage builds** -- Separate build dependencies from the runtime
   image. The final image should contain only what is needed to run the
   application.

2. **Pin image tags** -- Use `node:20.11-alpine` or a digest instead of
   `node:latest` or `node:20`. Floating tags lead to unpredictable builds.

3. **Order instructions for cache efficiency** -- Copy dependency manifests and
   install dependencies before copying application code. This ensures that code
   changes do not invalidate the dependency layer cache.

4. **Use .dockerignore** -- Exclude `.git`, `node_modules`, `__pycache__`, `.env`
   files, and anything not needed inside the container to keep the build context
   small and avoid leaking secrets.

5. **Run as non-root** -- Add a `USER` instruction to run the process as an
   unprivileged user. Never run production containers as root.

6. **Combine RUN commands** -- Merge related `RUN` instructions with `&&` to
   reduce layers and always clean up apt/apk caches in the same layer that
   installs packages.

7. **Use COPY instead of ADD** -- `COPY` is explicit and predictable. `ADD` has
   implicit behaviors (tar extraction, URL fetching) that can surprise you.

8. **Set explicit HEALTHCHECK** -- Define health checks in the Dockerfile so
   orchestrators know when the container is ready. This prevents routing traffic
   to containers that are still starting up.

---

## Common Pitfalls

1. **Bloated images** -- Using full base images like `python:3.12` instead of
   `python:3.12-slim` adds hundreds of megabytes. Always prefer slim or alpine
   variants. Use multi-stage builds to exclude build tools.

2. **Cache invalidation by COPY order** -- Placing `COPY . .` before
   `RUN pip install` means every code change reinstalls all dependencies. Always
   copy the dependency manifest first, install, then copy the rest of the code.

3. **Running as root** -- Forgetting the `USER` instruction means the container
   process runs as root. If the application is compromised, the attacker has full
   control of the container filesystem.

4. **Secrets baked into layers** -- Using `COPY .env .` or `ARG` for secrets
   embeds them in the image layer history. Anyone with access to the image can
   extract them with `docker history`. Pass secrets at runtime via environment
   variables or Docker secrets.

5. **Missing .dockerignore** -- Without a `.dockerignore`, the entire directory
   (including `.git`, `node_modules`, `.env` files) is sent as build context.
   This slows builds, increases image size, and risks leaking credentials.

6. **Ignoring healthchecks in Compose** -- Using `depends_on` without
   `condition: service_healthy` means the dependent service starts as soon as
   the database container starts, not when the database is actually ready to
   accept connections. Always pair `depends_on` with healthchecks.

---

## Related Skills

- `devops/github-actions` - CI/CD workflows for building and deploying Docker containers
- `security/owasp` - Security best practices for container hardening and vulnerability scanning
- `patterns/logging` — Container logging and log aggregation

More DevOps & Infrastructure skills

← All DevOps & Infrastructure skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY