Instruction file imported from Gadriel-ai/gadriel-copilot-plugin (
.github/instructions/gadriel-dockerfile-best-practices.instructions.md). Copyright stays with the author.
Dockerfile and Container Best Practices
This skill teaches Claude to review Dockerfiles, compose files, and Kubernetes manifests against the CIS Docker Benchmark and Gadriel's container-hardening rule pack. Used by both the security and operational pillars.
When this skill activates
- Finding IDs in
CODE-W1-CONTAINER-* - Tags:
dockerfile,image-bloat,root-user,secret-bake,cis-docker - User phrasings: "is this Dockerfile production-ready", "should we use alpine", "why is image size so large", "container running as root"
- File patterns:
Dockerfile*,*.Dockerfile,docker-compose*.yml,*.dockerignore, K8skind: Pod|Deploymentmanifests
Core concepts
- Least privilege — run as a non-root UID; drop
CAP_*capabilities; use a read-only root filesystem when possible. - Reproducibility — pin base images by digest (
@sha256:...), pin package versions;:latestis forbidden in production. - Small attack surface — distroless or
-slimimages; no shells, package managers, or debug tooling in the final stage. - No secrets in layers —
ARG/ENVfor build-time secrets, BuildKit--secret, neverCOPY .env. - Multi-stage builds — compile in a builder stage, copy only artifacts to the runtime stage.
- Defensible defaults —
USER,WORKDIR,HEALTHCHECK, explicitEXPOSE,STOPSIGNAL. - Provenance — sign images with cosign; attach SBOM (see
gadriel-sbom-guidance).
Detection patterns / cheatsheet
- No USER directive → container runs as root.
FROM ubuntu:latest→ unpinned tag.apt-get installwithout--no-install-recommendsand missingrm -rf /var/lib/apt/lists/*→ bloated layers.COPY . /appwithout a.dockerignore→ secrets and test data leak in.ARG SECRET_TOKENthenENV SECRET=${SECRET_TOKEN}→ secret baked into image history.RUN curl ... | sh→ unsigned remote script execution at build time.- No
HEALTHCHECK→ orchestrator cannot detect a wedged process. ADD <url>whenCOPYwould suffice → unintended TLS-trusting fetch.- K8s:
securityContext.runAsRoot: trueor missingrunAsNonRoot: true. - K8s:
hostNetwork: true,hostPID: truewithout explicit justification. - K8s:
privileged: trueanywhere outside a documented infra-tooling namespace. - Compose file with
privileged: trueorcap_add: [ALL].
Remediation playbook
- Add
USER 1000:1000(or a named non-root user) near the end of the Dockerfile. - Pin base images:
FROM python:3.12-slim@sha256:<digest>; update digests via Renovate/Dependabot. - Switch to multi-stage builds:
FROM ... AS builderthenFROM gcr.io/distroless/python3for runtime. - Replace
COPY .with explicitCOPY src/ ./src/plus a real.dockerignorelisting.env,.git,node_modules,__pycache__,*.pem. - Use BuildKit secrets:
RUN --mount=type=secret,id=npmrc npm ciinstead ofARG NPM_TOKEN. - Add
HEALTHCHECK CMD curl -fsS http://localhost:8080/health || exit 1. - K8s pod spec hardening:
securityContext: runAsNonRoot: true runAsUser: 1000 readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: { drop: ["ALL"] } - Sign images:
cosign sign --key cosign.key ghcr.io/org/app@<digest>; attach SBOM (cosign attach sbom). - Add a CI gate that fails on any finding in
CODE-W1-CONTAINER-*at severityhighor above.
References
- CIS Docker Benchmark v1.6.0 — https://www.cisecurity.org/benchmark/docker
- NIST SP 800-190 — Application Container Security Guide
- ADR-086 §D4 — skill is shared by
securityandoperationalagents - Distroless: https://github.com/GoogleContainerTools/distroless
- BuildKit secrets: https://docs.docker.com/build/building/secrets/