Agent skill
docker_patterns
Install this agent skill to your Project
npx add-skill https://github.com/CleanExpo/ATO/tree/main/.agent/skills/docker_patterns
SKILL.md
Docker Patterns
Multi-stage builds, layer caching, security hardening, and Docker Compose orchestration for NodeJS-Starter-V1.
Metadata
| Field | Value |
|---|---|
| Skill ID | docker-patterns |
| Category | Observability & DevOps |
| Complexity | Medium |
| Complements | health-check, graceful-shutdown, ci-cd-patterns |
| Version | 1.0.0 |
| Locale | en-AU |
Description
Codifies Docker patterns for NodeJS-Starter-V1: multi-stage builds for Python (FastAPI) and Next.js frontends, .dockerignore configuration, Docker Compose service orchestration with healthchecks and dependency ordering, container security hardening (non-root users, read-only filesystems), and layer caching strategies for fast CI builds.
When to Apply
Positive Triggers
- Writing or modifying a Dockerfile for backend or frontend services
- Configuring Docker Compose services, networks, or volumes
- Optimising Docker build times or image sizes
- Adding new services to the containerised stack
- Implementing container security hardening (non-root, capabilities)
- Debugging container health, networking, or volume mount issues
Negative Triggers
- Running containers locally for development without modifying Docker config (just use
pnpm run docker:up) - Kubernetes-specific orchestration (out of scope; use
infrastructure-as-code) - CI/CD pipeline configuration (use
ci-cd-patternsinstead) - Application-level health endpoints (use
health-checkskill instead)
Core Principles
The Three Laws of Docker
- Smallest Possible Image: Every unnecessary byte is attack surface and transfer time. Use multi-stage builds, alpine bases, and aggressive .dockerignore.
- Layer Cache Is King: Order instructions from least-changed to most-changed. Copy dependency manifests before source code.
- Non-Root by Default: Never run containers as root in production. Create a dedicated user and drop all capabilities.
Pattern 1: Python Backend Multi-Stage Build
Project Reference: apps/backend/Dockerfile:1-30 — current single-stage build without multi-stage or non-root user.
Multi-Stage with Security
# ── Stage 1: Dependencies ──────────────────────────────────
FROM python:3.12-slim AS deps
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock* ./
RUN uv sync --frozen --no-dev
# ── Stage 2: Runtime ───────────────────────────────────────
FROM python:3.12-slim AS runtime
# Security: non-root user
RUN groupadd --gid 1001 appuser \
&& useradd --uid 1001 --gid appuser --shell /bin/false appuser
# Runtime-only system deps (curl for healthcheck)
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy virtual environment from deps stage
COPY --from=deps /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
# Copy application source
COPY --chown=appuser:appuser src/ ./src/
# Drop to non-root
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "src.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
Key Improvements: Two-stage (deps → runtime), non-root appuser (UID 1001), --no-install-recommends for smaller image, direct uvicorn call (no uv wrapper in production).
Pattern 2: Next.js Frontend Multi-Stage Build
Standalone Output Mode
Next.js must be configured for standalone output before containerising:
// next.config.ts — add output: 'standalone'
const nextConfig: NextConfig = {
output: 'standalone',
reactStrictMode: true,
// ... existing config
};
Project Reference: apps/web/next.config.ts:3 — currently no output property set.
Frontend Dockerfile
# ── Stage 1: Dependencies ──────────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app
RUN corepack enable pnpm
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/web/package.json ./apps/web/
RUN pnpm install --frozen-lockfile --filter=web
# ── Stage 2: Build ─────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
RUN corepack enable pnpm
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm turbo run build --filter=web
# ── Stage 3: Runtime ───────────────────────────────────────
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Security: non-root user
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
# Copy standalone output
COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder /app/apps/web/public ./apps/web/public
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1
CMD ["node", "apps/web/server.js"]
Key Points:
- Three-stage: deps → build → runtime (runtime image ~150 MB vs ~1 GB without standalone)
corepack enable pnpmfor pnpm in AlpineNEXT_TELEMETRY_DISABLED=1prevents telemetry in build and runtimewgetfor healthcheck (pre-installed in Alpine, unlikecurl)- Standalone output copies only necessary server files
Pattern 3: .dockerignore
Backend .dockerignore
# apps/backend/.dockerignore
__pycache__/
*.pyc
*.pyo
.mypy_cache/
.ruff_cache/
.pytest_cache/
htmlcov/
coverage.xml
.coverage
.venv/
*.egg-info/
dist/
build/
.git/
.github/
.env
.env.local
.env.production
tests/
docs/
*.md
!README.md
Project Reference: No .dockerignore currently exists in the project. Without it, the entire build context (including .git/, node_modules/, test files) is sent to the Docker daemon.
For monorepo root builds, also exclude .git/, .github/, .beads/, .bin/, .claude/, .skills/, NodeJS-Starter-V1/, docs/, scripts/, *.md, and all node_modules//.venv/ directories.
Rule: Always create a .dockerignore in the same directory as the Dockerfile. Without it, Docker sends the entire directory tree as build context.
Pattern 4: Docker Compose Orchestration
Service Dependencies and Health
Project Reference: docker-compose.yml:1-68 — PostgreSQL (pgvector:pg15, port 5433) + Redis (7-alpine, port 6380) with healthchecks. Backend service commented out at line 36.
When enabling the backend service, add health-aware dependencies and shutdown integration:
backend:
build:
context: .
dockerfile: apps/backend/Dockerfile
environment:
DATABASE_URL: postgresql://starter_user:local_dev_password@postgres:5432/starter_db
REDIS_URL: redis://redis:6379
AI_PROVIDER: ${AI_PROVIDER:-ollama}
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
stop_grace_period: 35s
Key Rules
| Rule | Why |
|---|---|
Always use condition: service_healthy |
Prevents app starting before database is ready |
| Use named volumes for data persistence | Anonymous volumes are lost on docker compose down |
| Map to non-standard host ports (5433, 6380) | Avoids conflicts with locally installed PostgreSQL/Redis |
| Use bridge network for inter-service communication | Services reference each other by name, not localhost |
Set stop_grace_period > app shutdown timeout |
Prevents SIGKILL during graceful shutdown drain |
Development Overrides
Use a docker-compose.override.yml for development-specific settings:
# docker-compose.override.yml (auto-merged by docker compose)
services:
backend:
volumes:
- ./apps/backend/src:/app/src:ro
command: uvicorn src.api.main:app --host 0.0.0.0 --port 8000 --reload
environment:
- PYTHONUNBUFFERED=1
Rule: Never bind-mount source code in production. Use volumes only in development for hot-reload. The override file is auto-detected by docker compose and does not need -f flags.
Pattern 5: Security Hardening
Non-Root Execution
# Python (Debian-based)
RUN groupadd --gid 1001 appuser \
&& useradd --uid 1001 --gid appuser --shell /bin/false appuser
USER appuser
# Node.js (Alpine-based)
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
USER nextjs
Read-Only Root Filesystem
services:
backend:
read_only: true
tmpfs:
- /tmp
- /app/.cache
volumes:
- postgres_data:/var/lib/postgresql/data
Rule: Use read_only: true in production Compose files. Add tmpfs mounts for directories the application needs to write to (temp files, caches).
Capability Dropping
services:
backend:
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
Image Pinning
Rule: In production, pin images to at least minor version (python:3.12.8-slim). Use digest pinning (python:3.12-slim@sha256:abc...) for maximum reproducibility. Never use latest or major-only tags.
Pattern 6: Layer Caching
Dependency-First Copy Pattern
The most impactful caching strategy: copy dependency manifests before source code.
# ✅ Correct order: manifests → install → source
COPY pyproject.toml uv.lock* ./ # Layer 1: rarely changes
RUN uv sync --frozen --no-dev # Layer 2: cached unless deps change
COPY src/ ./src/ # Layer 3: changes frequently
# ❌ Wrong order: all files → install
COPY . . # Invalidates ALL subsequent layers on any change
RUN uv sync --frozen --no-dev # Reinstalls every time
Project Reference: apps/backend/Dockerfile:14-20 — already follows this pattern correctly.
BuildKit Cache Mounts
For large dependency trees, use BuildKit cache mounts to persist the package manager cache across builds:
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS deps
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock* ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
# Node.js with pnpm store cache
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
Rule: Enable BuildKit with DOCKER_BUILDKIT=1 or docker buildx build. Cache mounts are not included in the final image — they only speed up builds.
CI Layer Caching
In GitHub Actions, use docker/build-push-action@v6 with cache-from: type=gha and cache-to: type=gha,mode=max to persist Docker layer cache across workflow runs via GitHub Actions cache.
Pattern 7: Container Healthchecks
Dockerfile HEALTHCHECK
# Python (curl available)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Node.js Alpine (wget pre-installed, curl is not)
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1
Compose Healthcheck
healthcheck:
test: ["CMD-SHELL", "pg_isready -U starter_user -d starter_db"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
Timing Guidelines
| Parameter | Development | Production | Rationale |
|---|---|---|---|
interval |
10s | 30s | Faster feedback in dev; lower overhead in prod |
timeout |
5s | 10s | More tolerance for loaded prod systems |
start_period |
5s | 30s | Longer startup in prod (migrations, warmup) |
retries |
3 | 5 | More retries before marking unhealthy in prod |
Complements: health-check skill — defines the application-level /health and /ready endpoints. This skill defines the container-level HEALTHCHECK that calls those endpoints.
Anti-Patterns
| Pattern | Problem | Correct Approach |
|---|---|---|
COPY . . before RUN install |
Invalidates cache on every source change | Copy manifests first, install, then copy source |
| Running as root in production | Container escape grants host root access | Create non-root user with useradd/adduser |
No .dockerignore |
Sends .git/, node_modules/ to daemon |
Create .dockerignore in every build context |
latest tag in FROM |
Non-reproducible builds, surprise breakage | Pin to version (python:3.12-slim) |
apt-get install without cleanup |
Layer retains package cache (~100+ MB) | Chain with && rm -rf /var/lib/apt/lists/* |
| Bind-mounting source in production | Leaks source code, breaks immutability | Bind mounts for dev only; COPY in prod |
depends_on without condition |
App starts before database is ready | Use condition: service_healthy |
| Storing secrets in ENV in Dockerfile | Secrets baked into image layers | Use runtime environment variables or secrets |
| Single-stage build for production | Image includes build tools, dev deps | Multi-stage: build in stage 1, copy artefacts to stage 2 |
Checklist
Before merging Docker changes:
- Dockerfile uses multi-stage build (deps → runtime)
- Non-root user created and set with
USER -
.dockerignoreexists and excludes.git/, tests, docs, env files - Dependency manifests copied before source (layer cache order)
-
apt-getcleanup in same layer (&& rm -rf /var/lib/apt/lists/*) - HEALTHCHECK instruction defined with appropriate timing
-
depends_onusescondition: service_healthy -
stop_grace_periodexceeds application shutdown timeout - No secrets in Dockerfile ENV instructions
- Image tags pinned to at least minor version
Response Format
When applying this skill, structure implementation as:
### Docker Implementation
**Service**: [backend / frontend / infrastructure]
**Build Strategy**: [single-stage / multi-stage]
**Base Image**: [image:tag]
**User**: [root / non-root (UID)]
**Healthcheck**: [endpoint / command]
**Compose Integration**: [depends_on / healthcheck / stop_grace_period]
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
ato-rate-scraping
Automated scraping and extraction of current Australian tax rates from ATO.gov.au and related government sources
rate_limiter
payg-instalment-optimization
PAYG instalment strategy analysis — amount vs rate method, variation penalty risk, GDP adjustment
idea-queue-capture
Instantly capture user ideas to work queue with intelligent grouping
simple-report-export
Simple report generation without Google Cloud - uses Gmail App Password or local file export (PDF, Excel, Word-compatible formats)
cashflow-forecasting
Project future tax obligations and cash flow requirements based on historical transaction patterns
Didn't find tool you were looking for?