Dockerfile Smells and Hardening
Purpose
A consistent set of Dockerfile anti-patterns and their fixes, so container reviews produce smaller, safer, cache-friendly images the same way every time. This is container-build domain knowledge, the specific smells and the reasons they matter, not generic Docker syntax.
Smell catalog
| Smell |
Why it hurts |
Fix |
Runs as root (no USER) |
container escape = host root; violates PSS/most policies |
add USER <uid> (numeric, non-root) |
latest / unpinned base tag |
non-reproducible; silent base drift; supply-chain risk |
pin FROM img@sha256:<digest> |
| Single-stage build |
ships compilers, headers, build deps in runtime image |
multi-stage: build → slim runtime |
COPY . . before dep install |
any source edit busts the dependency cache layer |
copy manifests, install, then copy source |
| Secrets baked in a layer |
recoverable from image history forever |
RUN --mount=type=secret; never ENV TOKEN= |
apt-get with no cleanup |
apt lists/caches bloat the layer |
--no-install-recommends + rm -rf /var/lib/apt/lists/* in the same RUN |
Missing .dockerignore |
huge context; .git/secrets sent to daemon |
add .dockerignore |
No HEALTHCHECK |
orchestrator can't detect a wedged process |
add HEALTHCHECK where the app exposes a check |
ADD for local files / remote URLs |
silent auto-extract, unverified downloads |
use COPY; fetch with verified checksums |
Many chained RUN layers |
extra layers, larger image |
combine related commands, order by volatility |
Hardening principles (apply in order)
- Least privilege at runtime: non-root numeric
USER, drop capabilities, read-only root filesystem where possible.
- Minimal surface: distroless or
-slim runtime base; install only what runs in production.
- Reproducibility: pin base image by digest; pin package versions where feasible.
- Cache efficiency: order layers least-volatile → most-volatile (base, then deps, then source).
- No secrets in layers: build secrets via BuildKit mounts; runtime secrets via the orchestrator, never
ENV/ARG bake-in.
Layer-ordering rule
Put the things that rarely change (base image, system packages, dependency manifests + install) above the things that change every commit (application source). One dependency layer that survives across builds saves the most CI time.
Additional Resources
Reference Files
For the full hardening checklist with the security rationale, consult:
references/hardening-checklist.md: non-root user, minimal/distroless base, digest pinning, no secrets in layers, HEALTHCHECK, dropping setuid/setgid, read-only root filesystem, and least-capability, each with the concrete threat it addresses.
For copy-pasteable multi-stage templates, consult:
references/multistage-patterns.md: build-vs-runtime split templates per ecosystem (Node, Python, Go, Java/JVM, Rust), cache-mount patterns, and exactly what to copy into the final stage.
1---2name: dockerfile-smells3description: This skill should be used when the user mentions "Dockerfile", "docker image size", "container security", "multi-stage build", "distroless", "run as root", "docker layer caching", or is reviewing/optimizing a container image. It provides a catalog of Dockerfile anti-patterns and the hardening principles that fix them.4---56# Dockerfile Smells and Hardening78## Purpose9A consistent set of Dockerfile anti-patterns and their fixes, so container reviews produce smaller, safer, cache-friendly images the same way every time. This is container-build domain knowledge, the specific smells and the reasons they matter, not generic Docker syntax.1011## Smell catalog1213| Smell | Why it hurts | Fix |14|-------|--------------|-----|15| Runs as `root` (no `USER`) | container escape = host root; violates PSS/most policies | add `USER <uid>` (numeric, non-root) |16| `latest` / unpinned base tag | non-reproducible; silent base drift; supply-chain risk | pin `FROM img@sha256:<digest>` |17| Single-stage build | ships compilers, headers, build deps in runtime image | multi-stage: build → slim runtime |18| `COPY . .` before dep install | any source edit busts the dependency cache layer | copy manifests, install, *then* copy source |19| Secrets baked in a layer | recoverable from image history forever | `RUN --mount=type=secret`; never `ENV TOKEN=` |20| `apt-get` with no cleanup | apt lists/caches bloat the layer | `--no-install-recommends` + `rm -rf /var/lib/apt/lists/*` in the same `RUN` |21| Missing `.dockerignore` | huge context; `.git`/secrets sent to daemon | add `.dockerignore` |22| No `HEALTHCHECK` | orchestrator can't detect a wedged process | add `HEALTHCHECK` where the app exposes a check |23| `ADD` for local files / remote URLs | silent auto-extract, unverified downloads | use `COPY`; fetch with verified checksums |24| Many chained `RUN` layers | extra layers, larger image | combine related commands, order by volatility |2526## Hardening principles (apply in order)271. **Least privilege at runtime**: non-root numeric `USER`, drop capabilities, read-only root filesystem where possible.282. **Minimal surface**: distroless or `-slim` runtime base; install only what runs in production.293. **Reproducibility**: pin base image by digest; pin package versions where feasible.304. **Cache efficiency**: order layers least-volatile → most-volatile (base, then deps, then source).315. **No secrets in layers**: build secrets via BuildKit mounts; runtime secrets via the orchestrator, never `ENV`/`ARG` bake-in.3233## Layer-ordering rule34Put the things that rarely change (base image, system packages, dependency manifests + install) **above** the things that change every commit (application source). One dependency layer that survives across builds saves the most CI time.3536## Additional Resources37### Reference Files38For the full hardening checklist with the security rationale, consult:39- **`references/hardening-checklist.md`**: non-root user, minimal/distroless base, digest pinning, no secrets in layers, HEALTHCHECK, dropping setuid/setgid, read-only root filesystem, and least-capability, each with the concrete threat it addresses.4041For copy-pasteable multi-stage templates, consult:42- **`references/multistage-patterns.md`**: build-vs-runtime split templates per ecosystem (Node, Python, Go, Java/JVM, Rust), cache-mount patterns, and exactly what to copy into the final stage.