mirror of
https://github.com/duthaho/claudekit.git
synced 2026-06-10 20:24:57 +03:00
656 lines
16 KiB
Markdown
656 lines
16 KiB
Markdown
# DevOps — Docker Patterns
|
|
|
|
|
|
# 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
|
|
|
|
- `github-actions` - CI/CD workflows for building and deploying Docker containers
|
|
- `owasp` - Security best practices for container hardening and vulnerability scanning
|