$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Rule Writing

AI Rules for Dockerfiles and Containers

AI-generated Dockerfiles run as root, skip multi-stage builds, and produce 2GB images. Rules for secure, minimal, and cacheable container images.

7 min read·March 7, 2025

AI generates Dockerfiles that run as root with 2GB images

Multi-stage builds, non-root users, layer caching, and container security

Why Dockerfile Rules Matter

Dockerfiles are deceptively simple — FROM, COPY, RUN, CMD. But every line has security and performance implications that AI assistants ignore by default. The AI generates a Dockerfile that builds and runs your app. It also runs as root (security risk), includes the entire build toolchain in the final image (2GB instead of 200MB), busts the layer cache on every build (10-minute builds instead of 30 seconds), and copies secrets into the image.

Unlike application code where mistakes cause bugs, Dockerfile mistakes cause security vulnerabilities that persist in every running container. A container running as root with a shell and build tools is an attacker's dream if any vulnerability is exploited.

These rules apply to Docker, Podman, and any OCI-compatible container builder. The Dockerfile syntax is the same; the security and performance principles are universal.

Rule 1: Multi-Stage Builds Always

The rule: 'Use multi-stage builds for every production Dockerfile. Stage 1 (builder): install dependencies and build the application. Stage 2 (runtime): copy only the built artifacts into a minimal base image. The runtime stage should not contain build tools, source code, or dev dependencies.'

For Node.js: 'Builder stage: FROM node:22-alpine AS builder. Runtime stage: FROM node:22-alpine. Copy only node_modules (production) and the built output. For Next.js: copy .next/standalone and .next/static. The final image should be under 200MB, not 1.5GB.'

For Go: 'Builder: FROM golang:1.22 AS builder. Runtime: FROM scratch or FROM gcr.io/distroless/static-debian12. Copy only the compiled binary. Go's static binaries run on scratch — the smallest possible image (under 20MB).'

AI assistants generate single-stage Dockerfiles because they're simpler. One rule forces multi-stage, cutting image size by 80-90% and removing the entire attack surface of build tools.

  • Always multi-stage: builder stage for compilation, runtime stage for execution
  • Runtime base: alpine, slim, or distroless — never full OS images
  • Copy only built artifacts — no source code, no dev deps, no build tools
  • Node.js final: <200MB | Go final: <20MB | Python final: <150MB
💡 80-90% Smaller

Multi-stage builds cut image size by 80-90%. A Node.js app goes from 1.5GB (with build tools) to under 200MB (runtime only). One Dockerfile pattern change, massive improvement.

Rule 2: Container Security

The rule: 'Never run containers as root. Create a non-root user and switch to it: RUN addgroup -S app && adduser -S app -G app then USER app. Never include secrets in the image — use build-time secrets (--mount=type=secret) or runtime environment variables. Never install SSH, sudo, or shells in production images. Use read-only filesystem where possible: docker run --read-only.'

For base images: 'Use specific version tags, never :latest — it makes builds non-reproducible. Prefer distroless or alpine for minimal attack surface. Scan images with Trivy or Snyk in CI — fail the build on critical vulnerabilities. Update base images monthly for security patches.'

For secrets: 'Never COPY .env into the image. Never use ARG for secrets (they're visible in image history). Use Docker BuildKit secrets: RUN --mount=type=secret,id=api_key cat /run/secrets/api_key. At runtime, inject secrets via environment variables or a secrets manager.'

⚠️ Never Root

Containers running as root mean any exploited vulnerability gives the attacker root access. Two lines fix this: RUN adduser -S app && USER app. Non-negotiable for production.

Rule 3: Layer Caching Optimization

The rule: 'Order Dockerfile instructions from least to most frequently changing. COPY package.json and lock file before COPY . — this caches dependency installation. Install system packages before application code. Use .dockerignore to exclude node_modules, .git, .env, and build artifacts from the build context.'

For Node.js caching: 'COPY package.json pnpm-lock.yaml ./ then RUN pnpm install --frozen-lockfile then COPY . . — the install layer is cached until dependencies change. Without this order, every code change re-installs all dependencies.'

The difference is dramatic: properly cached builds take 10-30 seconds. Poorly ordered Dockerfiles rebuild from scratch every time — 5-15 minutes. This single ordering rule saves hours of CI time per week across a team.

  • System packages → dependency files → dependency install → source code → build
  • COPY lock file before source — caches dep install until deps change
  • .dockerignore: node_modules, .git, .env, dist, build, *.log
  • Use BuildKit cache mounts for package manager caches: --mount=type=cache
ℹ️ Cache = Speed

Properly cached Docker builds take 10-30 seconds. Poorly ordered Dockerfiles rebuild from scratch every time: 5-15 minutes. Copy lock file before source code — the install layer caches until deps change.

Rule 4: Health Checks and Signal Handling

The rule: 'Include a HEALTHCHECK in every production Dockerfile: HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1. Use exec form for CMD and ENTRYPOINT: CMD ["node", "server.js"] — not CMD node server.js (shell form wraps in sh, preventing signal propagation). Handle SIGTERM gracefully in your application for clean shutdown.'

For graceful shutdown: 'The container orchestrator sends SIGTERM before killing the container. Your app must handle it: close database connections, finish in-flight requests, flush logs. Without proper signal handling, containers are killed mid-request and connections leak.'

For orchestration: 'If using Kubernetes, the HEALTHCHECK is informational (K8s uses its own probes). Define livenessProbe and readinessProbe in the pod spec. Liveness: is the process alive? Readiness: can it serve traffic? Separate these — a container can be alive but not ready (still warming up).'

Rule 5: Docker Compose for Development

The rule: 'Use Docker Compose for local development environments. Define all services (app, database, cache, queue) in docker-compose.yml. Use volume mounts for source code (hot reload). Use environment variables for configuration — never hardcode. Use depends_on with health checks for service startup ordering.'

For dev vs prod: 'Use docker-compose.override.yml for development overrides (volume mounts, debug ports, dev-only services). The base docker-compose.yml matches production configuration as closely as possible. Never use Docker Compose in production — use Kubernetes, ECS, or a proper orchestrator.'

For databases: 'Use named volumes for database data persistence. Include initialization scripts in init/ directory. Pin database versions to match production. Include a seed script for development data.'

Complete Docker Rules Template

Consolidated rules for Dockerfiles and container workflows.

  • Multi-stage builds always — builder for compilation, runtime for execution
  • Non-root user: adduser + USER directive — never run as root
  • No secrets in images — BuildKit secrets for build, env vars for runtime
  • Specific version tags — never :latest — scan with Trivy in CI
  • Layer caching: lock file → install → source code → build (in that order)
  • .dockerignore for node_modules, .git, .env, build artifacts
  • HEALTHCHECK + exec-form CMD — handle SIGTERM for graceful shutdown
  • Compose for dev — volume mounts, env vars, depends_on with health checks
AI Rules for Dockerfiles and Containers — RuleSync Blog