dockerfile
RA convention: dockerfiles live at
.docker/<name>.dockerfilewith the repo root as the build context, and are built by thedaggerbuild system. Concrete starter dockerfiles are kept per-repo under each project's.docker/(they encode that repo's paths and services); this skill ships the universal guidance + ahadolint.yamlanddockerignorebaseline.
When to use this skill
- Creating a new image → pick the workload row in the decision tree, then read its reference.
- Modifying an existing
.docker/*.dockerfile. - Reviewing a dockerfile for size, security, or cache efficiency.
- Debugging a slow or bloated container build.
Decision tree
Workload?
├── Python + GPU (Ray, PyTorch, CUDA) → references/gpu-cuda.md
├── Python, no GPU (FastAPI, CLI) → references/python-uv.md
└── Static bundle (SvelteKit, Vite) → references/static-nginx.md
Always also load references/principles.md — applies to every dockerfile.
Hard rules
- File path:
.docker/<image-name>.dockerfileat repo root. Build context is always the repo root. - Single
.dockerignoreat repo root. No per-image ignore files. Start fromtemplates/dockerignore. - Multi-stage. Final stage = minimum runtime surface, non-root UID ≥ 10000, created with
useradd -r --no-create-home --shell /usr/sbin/nologin. - Digest-pinned
FROM. Every base image referenced by@sha256:<digest>, not a floating tag. Bump workflow inreferences/principles.md. - BuildKit cache mounts.
--mount=type=cache,target=/root/.cache/uv(uv) and--mount=type=cache,target=/root/.bun/install/cache(bun). Caches never ship in image layers. - PID 1.
tini --as ENTRYPOINT for Python processes.nginx-unprivilegedalready has its own init. - OCI labels. Declare
ARG BUILD_DATE/VCS_REF/VERSIONand emitorg.opencontainers.image.*labels (full set inreferences/principles.md). - Read-only-rootfs ready. Final image runs cleanly under
--read-only --tmpfs /tmp. Writable paths are/tmpor explicit volumes. - Secrets via
--mount=type=secret, neverARG. With--provenance=mode=max(SLSA), ARG values become public in the attestation. - No build leakage. Final image must not contain build toolchain (gcc, make), package managers (apt, the
uvbinary),.git, tests, or dev-dependencies. - First line is
# syntax=docker/dockerfile:1.11— pins the BuildKit frontend version. Required for the cache/bind/secret mount syntax used elsewhere in these rules.
When to load each reference
references/principles.md— every dockerfile change. Cache, layer ordering, COPY --link tradeoff, HEALTHCHECK, hadolint, setuid-strip, OCI labels, CVE-2024-3094 bump-guard, CI cache export.references/python-uv.md— any Python image. uv two-step--frozen/--lockedsync,UV_PROJECT_ENVIRONMENT=/opt/venv, workspace handling, arm64 cache-mount note.references/gpu-cuda.md— only when the image needs CUDA. Runtime vs devel, uv-managed Python on Ubuntu base, HF telemetry/transfer/secret patterns, thread-storm + PYTORCH_CUDA_ALLOC_CONF ENV defaults.references/static-nginx.md— only when serving static assets. bun build, nginx-unprivileged config, SPA fallback, /_app/version.json override, dotfile block, Svelte 5 CSP gotcha.
After authoring
- Run
docker buildx build --check -f .docker/<name>.dockerfile .— catchesSecretsUsedInArgOrEnv, missing stage-description comments. - Run
hadolint --config .hadolint.yaml .docker/<name>.dockerfile. CI gates on this. - Build twice:
docker buildx build -f .docker/<name>.dockerfile --build-arg BUILD_DATE=$(date -u +%FT%TZ) --build-arg VCS_REF=$(git rev-parse HEAD) --build-arg VERSION=$(git describe --always) -t <name>:dev .. The second build should be dominated byCACHEDlayers — that confirms the cache mount + bind mount + COPY-order discipline are correct. - When bumping a base image:
docker buildx imagetools inspect <ref>→ record digest. Refuse digests older than ~90 days (see bump workflow inreferences/principles.md); scan with Trivy/Grype/Docker Scout (CVE-2024-3094 was still found in pinned images in mid-2025).