# Docker build patterns

Loaded on demand from `../SKILL.md`.

## 3. Multi-stage builds

Build tools, compilers and dev dependencies stay in earlier stages. The last
stage copies artifacts only.

```dockerfile
# syntax=docker/dockerfile:1.7
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/orders-api ./cmd/orders-api

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/orders-api /orders-api
EXPOSE 8080
ENTRYPOINT ["/orders-api"]
```

Useful stage tricks:

- A `test` stage that runs the suite, built with `--target test` in CI, keeps
  test tooling out of the release image.
- A `dev` stage with hot reload tools, selected in Compose with `target: dev`.
- `COPY --link` lets BuildKit reuse a layer even when an earlier layer changed.

### Choosing the runtime base

| Base | Size | Shell | Fits |
|------|------|-------|------|
| `scratch` | none | no | static binaries that need no CA bundle or tzdata |
| `distroless/static` / `distroless/base` | very small | no | Go, Rust, JVM (`distroless/java21`) |
| `*-slim` (Debian) | small | yes | Python, Node, anything needing glibc and debugging |
| `alpine` | small | yes | when musl is known to be fine for every native dependency |

Alpine's musl changes DNS behaviour and breaks some prebuilt wheels and native
modules. Pick it on evidence, not by default.

## Multi-architecture builds

```bash
docker buildx create --use --name multi
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --cache-from type=registry,ref=registry.example.com/orders-api:buildcache \
  --cache-to type=registry,ref=registry.example.com/orders-api:buildcache,mode=max \
  -t registry.example.com/orders-api:1.4.0 --push .
```

QEMU emulation works but is slow for compiled languages. Prefer cross
compilation in the build stage using `--platform=$BUILDPLATFORM` plus the
`TARGETOS` / `TARGETARCH` build args, or native runners per architecture.

## Compose v2 for local development

Use `compose.yaml` and `docker compose` (the v1 `docker-compose` binary and the
top-level `version:` key are obsolete).

```yaml
services:
  api:
    build: { context: ., target: dev }
    ports: ["8080:8080"]
    env_file: .env.local
    depends_on:
      db: { condition: service_healthy }
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package-lock.json
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: devonly
    volumes: [pgdata:/var/lib/postgresql/data]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 10
volumes:
  pgdata:
```

- `depends_on` with `service_healthy` replaces sleep-and-retry wrappers.
- `docker compose watch` syncs files or rebuilds on change.
- Profiles (`profiles: [tools]`) keep optional services off by default.
- Compose is for development and small single-host deployments; production
  orchestration settings (replicas, rollout) belong to the platform.
