---
name: docker-conventions
description: Use when writing a Dockerfile, docker-compose file, or CI containerization config — multi-stage builds, layer caching, security (non-root user, minimal base images, no secrets in layers), and health checks.
metadata:
  type: skills
  complexity: low
  scope: [all]
---

# Docker Conventions

Best practices for Dockerfiles and container configurations. Focus areas:
image size, build cache efficiency, security, and production readiness.

---

## Base images

- Use official images with a pinned version tag: `python:3.12-slim`,
  `node:22-alpine`, `golang:1.24-alpine`.
- Prefer `slim` (Debian-based, small) or `alpine` (even smaller, musl
  libc) variants. Avoid `latest` — it changes without notice.
- For production, prefer distroless or scratch (Go static binaries) to
  minimise the attack surface.

```dockerfile
# Good: specific version, minimal base
FROM python:3.12-slim

# Bad: unpinned, full Debian base
FROM python:latest
```

---

## Multi-stage builds

Use multi-stage builds to keep the production image small — the build
tools (compilers, test runners, dev dependencies) stay in the build stage
and never reach the final image.

```dockerfile
# Stage 1: build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: production
FROM node:22-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
```

---

## Layer caching

Docker caches layers until a layer (or one of its inputs) changes. Copy
dependency files before source code so the dependency install step is
cached across code-only changes.

```dockerfile
# Good: dependency install cached when only code changes
COPY package*.json ./
RUN npm ci
COPY . .         # ← only invalidates layers from here down

# Bad: copies all source first — dependency install re-runs on every code change
COPY . .
RUN npm install
```

---

## Security

**Never embed secrets in a Dockerfile or in a layer.** Secrets baked into
a layer remain accessible in the image history even if deleted in a later
layer.

```dockerfile
# WRONG: secret in a layer (visible via `docker history`)
RUN echo "API_KEY=abc123" > /app/.env

# RIGHT: pass secrets at runtime via environment variable or a secret mount
# docker run -e API_KEY=$API_KEY ...
# or use BuildKit secrets for build-time:
RUN --mount=type=secret,id=api_key cat /run/secrets/api_key > /tmp/key
```

**Run as a non-root user.** Containers running as root have unnecessary
privileges; if the process is compromised, the attacker has root inside
the container.

```dockerfile
# Create a non-root user and switch to it
RUN addgroup --system app && adduser --system --ingroup app app
USER app
```

**Minimal attack surface:**
- Remove package manager caches in the same `RUN` layer that installs
  packages: `apt-get install -y ... && rm -rf /var/lib/apt/lists/*`
- Don't install `curl`, `wget`, or debugging tools in production images.

---

## Health checks

Add a `HEALTHCHECK` so the orchestrator (Docker Compose, Kubernetes) can
detect when the container is running but unhealthy.

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

---

## .dockerignore

Always provide a `.dockerignore`. Without it, `COPY . .` sends your
entire working directory (including `node_modules`, `.git`, and build
artifacts) to the Docker daemon.

```dockerignore
.git
.env
*.env
node_modules
__pycache__
.venv
dist
build
*.log
```

---

## docker-compose for local development

```yaml
services:
  app:
    build:
      context: .
      target: development     # use the dev stage of a multi-stage build
    ports:
      - "3000:3000"
    volumes:
      - .:/app                # mount source for hot-reload
      - /app/node_modules     # anonymous volume prevents host override
    environment:
      - NODE_ENV=development
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 5s
      retries: 5

volumes:
  postgres_data:
```

---

## Review checklist

- [ ] Base image pinned to a specific version tag (not `latest`)
- [ ] Multi-stage build separates build tools from the final image
- [ ] Dependency files copied before source code for cache efficiency
- [ ] No secrets in Dockerfile or image layers; use runtime env vars
- [ ] Container runs as a non-root user
- [ ] Package manager cache cleaned in the same `RUN` layer
- [ ] `HEALTHCHECK` defined for service containers
- [ ] `.dockerignore` present and excludes `node_modules`, `.git`, `.env`
