feat: adding new skills, including testing patterns and methodologies, along with bundled resources for better usability.

This commit is contained in:
duthaho
2026-03-30 12:18:00 +07:00
parent 0ff5ae4082
commit 7fa9a48c6c
89 changed files with 25808 additions and 923 deletions
+606 -41
View File
@@ -1,70 +1,216 @@
---
name: docker
description: >
Use this skill whenever containerizing applications, writing Dockerfiles, configuring Docker Compose, or optimizing container images. Trigger on keywords like Docker, Dockerfile, container, docker-compose, multi-stage build, image, or container registry. Also applies when setting up local development environments with containers, debugging container networking, or preparing applications for container-based deployment in CI/CD pipelines.
---
# Docker
## Description
Docker containerization including Dockerfiles, compose, and best practices.
## 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
### Multi-stage Dockerfile (Node.js)
### 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 node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- Build stage ----
FROM python:3.12-slim AS builder
# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
```
WORKDIR /build
### Python Dockerfile
# 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/*
```dockerfile
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ---- Runtime stage ----
FROM python:3.12-slim
WORKDIR /app
# Install dependencies first (caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy only the installed packages from the builder
COPY --from=builder /install /usr/local
COPY . .
# 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
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
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"]
```
### Docker Compose
#### 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
version: '3.8'
services:
app:
build: .
build:
context: .
dockerfile: Dockerfile
target: builder # Use builder stage for dev with hot-reload
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/app
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
db:
condition: service_healthy
redis:
condition: service_started
networks:
- backend
restart: unless-stopped
db:
image: postgres:16-alpine
@@ -72,23 +218,442 @@ services:
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
2. Order commands for cache efficiency
3. Use .dockerignore
4. Run as non-root user
5. Use specific image tags
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
- **Large images**: Use slim/alpine bases
- **Cache busting**: Order COPY commands properly
- **Root user**: Add USER instruction
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
@@ -0,0 +1,196 @@
# Dockerfile Best Practices Reference
Quick reference for writing efficient, secure, and maintainable Dockerfiles.
## Layer Ordering for Cache Optimization
Order instructions from least-frequently-changed to most-frequently-changed:
```dockerfile
# 1. Base image (changes: rarely)
FROM node:22-slim
# 2. System dependencies (changes: rarely)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# 3. App dependency manifest (changes: sometimes)
COPY package.json pnpm-lock.yaml ./
# 4. Install dependencies (changes: sometimes, cached if manifests unchanged)
RUN pnpm install --frozen-lockfile
# 5. Copy source code (changes: frequently)
COPY . .
# 6. Build step (changes: frequently)
RUN pnpm build
# 7. Runtime config (changes: rarely, but placed last for clarity)
EXPOSE 3000
CMD ["node", "dist/server.js"]
```
**Key rule**: If a layer changes, all subsequent layers are rebuilt. Separate dependency installation from source code copying.
## Multi-Stage Builds
Reduce final image size by separating build and runtime stages.
```
+-------------------+ +-------------------+
| Build Stage | | Runtime Stage |
| | | |
| - Full toolchain | ---> | - Minimal base |
| - Dev deps | | - Only artifacts |
| - Source code | | - No build tools |
| - Build output | | - No source code |
+-------------------+ +-------------------+
~800 MB ~80 MB
```
**Benefits**: Smaller images, faster deploys, reduced attack surface, no build tools in production.
## Base Image Selection
| Image | Size | Use Case | Security | Package Manager |
|-------|------|----------|----------|-----------------|
| **alpine** | ~5 MB | Small images, CLI tools | Good (small surface) | apk |
| **slim** (Debian) | ~80 MB | Most apps (Python, Node) | Good | apt |
| **distroless** | ~20 MB | Production, no shell needed | Excellent (no shell) | None |
| **scratch** | 0 MB | Static Go/Rust binaries | Excellent (nothing) | None |
| **full** (Debian) | ~300 MB | Build stages, debugging | Fair (large surface) | apt |
### Recommendations by Language
| Language | Build Stage | Runtime Stage |
|----------|-------------|---------------|
| **Python** | `python:3.12-slim` | `python:3.12-slim` or `distroless/python3` |
| **Node.js** | `node:22-slim` | `node:22-slim` or `distroless/nodejs22` |
| **Go** | `golang:1.23` | `scratch` or `distroless/static` |
| **Rust** | `rust:1.83` | `scratch` or `distroless/cc` |
| **Java** | `eclipse-temurin:21-jdk` | `eclipse-temurin:21-jre-alpine` |
## Instruction Best Practices
### RUN: Combine and Clean Up
```dockerfile
# BAD: Multiple layers, leftover cache
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
# GOOD: Single layer, cache cleaned
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
```
### COPY: Be Specific
```dockerfile
# BAD: Copies everything, including .git, node_modules, etc.
COPY . .
# GOOD: Copy only what's needed (use .dockerignore too)
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY src/ ./src/
COPY tsconfig.json ./
```
### .dockerignore Essentials
```
.git
node_modules
__pycache__
.env
*.log
dist
.venv
.pytest_cache
coverage
.DS_Store
```
### USER: Don't Run as Root
```dockerfile
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser -s /bin/false appuser
USER appuser
```
### HEALTHCHECK
```dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
```
### ARG vs ENV
| Directive | Available at | Persists in image | Use for |
|-----------|-------------|-------------------|---------|
| `ARG` | Build time only | No | Build-time toggles, versions |
| `ENV` | Build + runtime | Yes | App configuration |
```dockerfile
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim
ENV APP_ENV=production
ENV PORT=8000
```
## Security Checklist
| Practice | Command/Example |
|----------|----------------|
| Pin base image digests | `FROM node:22-slim@sha256:abc123...` |
| Run as non-root | `USER appuser` |
| No secrets in layers | Use `--mount=type=secret` or build args |
| Scan for vulnerabilities | `docker scout cves`, `trivy image` |
| Read-only filesystem | `docker run --read-only` |
| Drop capabilities | `docker run --cap-drop ALL` |
| Use `.dockerignore` | Exclude `.env`, `.git`, credentials |
| Minimal base image | Use slim/distroless/scratch |
### Secrets at Build Time (BuildKit)
```dockerfile
# Mount a secret file without baking it into a layer
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm install
# Build command:
# docker build --secret id=npm_token,src=.npmrc .
```
## Image Size Reduction Checklist
1. Use multi-stage builds
2. Choose slim/alpine/distroless base
3. Combine RUN commands
4. Remove package manager caches (`rm -rf /var/lib/apt/lists/*`)
5. Use `.dockerignore`
6. Don't install dev dependencies in runtime stage
7. Remove unnecessary files after build
8. Use `--no-install-recommends` with apt
## Common Pitfalls
| Pitfall | Impact | Fix |
|---------|--------|-----|
| `COPY . .` before `npm install` | No dependency caching | Copy lockfile first, install, then copy source |
| Using `latest` tag | Non-reproducible builds | Pin specific version tags or digests |
| Secrets in `ENV` or `COPY` | Leaked in image layers | Use BuildKit secrets mount |
| Running as root | Security vulnerability | Add `USER` directive |
| No `.dockerignore` | Bloated context, slow builds | Add and maintain `.dockerignore` |
| Installing build tools in final stage | Bloated image | Use multi-stage; build in first stage |
| Not using `--frozen-lockfile` | Non-deterministic installs | Always use lockfile flags |
@@ -0,0 +1,93 @@
# =============================================================================
# Multi-Stage Node.js Dockerfile
# Usage:
# docker build -t myapp .
# docker run -p 3000:3000 myapp
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Install dependencies
# ---------------------------------------------------------------------------
FROM node:22-slim AS deps
# Enable corepack for pnpm support.
RUN corepack enable
WORKDIR /app
# Copy only package manifests first for dependency layer caching.
# Dependencies are only reinstalled when these files change.
COPY package.json pnpm-lock.yaml ./
# Install production and dev dependencies (dev deps needed for build step).
RUN pnpm install --frozen-lockfile
# ---------------------------------------------------------------------------
# Stage 2: Build the application
# ---------------------------------------------------------------------------
FROM node:22-slim AS builder
RUN corepack enable
WORKDIR /app
# Copy dependencies from the deps stage.
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/package.json /app/pnpm-lock.yaml ./
# Copy source code and config files needed for the build.
COPY tsconfig.json ./
COPY src/ ./src/
# COPY public/ ./public/ # Uncomment for Next.js or static assets
# Build the application.
RUN pnpm build
# Remove dev dependencies after build to reduce size.
RUN pnpm prune --prod
# ---------------------------------------------------------------------------
# Stage 3: Production runtime
# ---------------------------------------------------------------------------
FROM node:22-slim AS runtime
# Run as non-root for security.
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /bin/false appuser
WORKDIR /app
# Set production environment.
ENV NODE_ENV=production \
PORT=3000
# Copy only production artifacts from the builder stage.
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# For Next.js standalone output, use instead:
# COPY --from=builder /app/.next/standalone ./
# COPY --from=builder /app/.next/static ./.next/static
# COPY --from=builder /app/public ./public
# Switch to non-root user.
USER appuser
# Expose the application port.
EXPOSE 3000
# Health check -- adjust the endpoint to match your app.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://localhost:3000/health').then(r => { if (!r.ok) process.exit(1) })" || exit 1
# Run the application.
CMD ["node", "dist/server.js"]
# For Next.js standalone:
# CMD ["node", "server.js"]
# For NestJS:
# CMD ["node", "dist/main.js"]
# For Express with ts-node (dev only, not recommended for production):
# CMD ["npx", "ts-node", "src/server.ts"]
@@ -0,0 +1,78 @@
# =============================================================================
# Multi-Stage Python Dockerfile
# Usage:
# docker build -t myapp .
# docker run -p 8000:8000 myapp
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Build dependencies
# ---------------------------------------------------------------------------
# Use slim for building - it has gcc and headers available via apt.
FROM python:3.12-slim AS builder
# Prevent Python from writing .pyc files and enable unbuffered output.
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Install build-time system dependencies (if any compiled packages need them).
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Python dependencies into a virtual environment.
# Copying requirements first enables Docker layer caching --
# dependencies are only reinstalled when requirements.txt changes.
COPY requirements.txt .
RUN python -m venv /app/.venv \
&& /app/.venv/bin/pip install --no-cache-dir --upgrade pip \
&& /app/.venv/bin/pip install --no-cache-dir -r requirements.txt
# ---------------------------------------------------------------------------
# Stage 2: Runtime
# ---------------------------------------------------------------------------
FROM python:3.12-slim AS runtime
# Prevent .pyc files and enable unbuffered output for logging.
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH"
# Install only runtime system dependencies (no build tools).
# Add packages here if your app needs them at runtime (e.g., libpq for psycopg).
# RUN apt-get update && apt-get install -y --no-install-recommends \
# libpq5 \
# && rm -rf /var/lib/apt/lists/*
# Create a non-root user for security.
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /bin/false appuser
WORKDIR /app
# Copy the virtual environment from the builder stage.
COPY --from=builder /app/.venv /app/.venv
# Copy application source code.
COPY src/ ./src/
# Switch to non-root user.
USER appuser
# Expose the application port.
EXPOSE 8000
# Health check -- adjust the endpoint to match your app.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# Run the application.
# For FastAPI/Uvicorn:
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
# For Django/Gunicorn:
# CMD ["gunicorn", "src.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]
# For a plain script:
# CMD ["python", "-m", "src.main"]
@@ -0,0 +1,100 @@
# =============================================================================
# Development Docker Compose
# Usage:
# docker compose -f docker-compose.dev.yaml up
# docker compose -f docker-compose.dev.yaml down -v # remove volumes too
# =============================================================================
services:
# ---------------------------------------------------------------------------
# Application
# ---------------------------------------------------------------------------
app:
build:
context: .
dockerfile: Dockerfile
target: builder # Use the build stage for dev (includes dev deps)
ports:
- "${APP_PORT:-3000}:3000"
environment:
NODE_ENV: development
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/app_dev
REDIS_URL: redis://redis:6379/0
volumes:
# Mount source code for hot-reload. Exclude node_modules.
- .:/app
- /app/node_modules
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# Override CMD for development (hot-reload).
command: ["pnpm", "dev"]
# For Python:
# command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "3000", "--reload"]
# ---------------------------------------------------------------------------
# PostgreSQL
# ---------------------------------------------------------------------------
postgres:
image: postgres:17-alpine
ports:
- "${POSTGRES_PORT:-5432}:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_dev
volumes:
# Persist data across restarts.
- postgres_data:/var/lib/postgresql/data
# Run init scripts on first start.
# - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
# ---------------------------------------------------------------------------
# Redis
# ---------------------------------------------------------------------------
redis:
image: redis:7-alpine
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
# Persist data to disk every 60 seconds if at least 1 key changed.
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]
# ---------------------------------------------------------------------------
# Optional: pgAdmin (database GUI)
# ---------------------------------------------------------------------------
# pgadmin:
# image: dpage/pgadmin4:latest
# ports:
# - "5050:80"
# environment:
# PGADMIN_DEFAULT_EMAIL: admin@local.dev
# PGADMIN_DEFAULT_PASSWORD: admin
# depends_on:
# - postgres
# ---------------------------------------------------------------------------
# Optional: Mailpit (email testing)
# ---------------------------------------------------------------------------
# mailpit:
# image: axllent/mailpit:latest
# ports:
# - "8025:8025" # Web UI
# - "1025:1025" # SMTP
volumes:
postgres_data:
redis_data: