Prerequisites
- Target system, dependencies and environment configured.
Usage
Purpose
The Dockerfile decides what's in your container and how it runs — and a few habits separate a hardened image from a bloated, root-running, secret-leaking one. This skill covers the Dockerfile practices that shrink the attack surface: minimal bases, non-root execution, no secrets baked into layers, and pinned, verifiable dependencies. It's where container security starts, before the image ever runs.
When to use it
Writing or reviewing any Dockerfile, and as a baseline for the whole container programme. It pairs with image scanning (which finds vulnerabilities) — hardening reduces what's there to be vulnerable in the first place.
Procedure
- Start from a minimal base image — the highest-impact choice. Every package in the base is attack surface and a potential CVE. Prefer distroless, alpine, or slim variants over full OS images. A smaller base means fewer vulnerabilities, a smaller image, and less for an attacker to use (fewer shells and tools inside).
- Run as a non-root user. By default containers run as root, so a container escape or app compromise is root. Create and switch to a non-root user (
USER), so the process has minimal privilege even if compromised. This is one of the most important hardening steps.
- Keep secrets out of the image — they persist in layers. Never
COPY a credential or ENV a secret; Docker layers are inspectable, so a secret added in one layer remains even if "removed" in a later one. Use build secrets (BuildKit --secret) or inject at runtime, never bake them in.
- Pin and verify what you install. Pin base image tags (ideally by digest, not a moving tag) and package versions so builds are reproducible and you know what you shipped. Verify downloaded artifacts where possible.
- Minimise layers and content. Combine related
RUN commands, remove build tools and caches in the same layer they're used (a package cache left in an earlier layer persists), and use .dockerignore so you don't copy secrets, .git, or junk into the image. Multi-stage builds keep build-time tools out of the final image.
- Drop unnecessary capabilities and set a read-only filesystem where possible (often at runtime, but design for it) — the container should have only what it needs.
- Lint and scan the result. Use a Dockerfile linter (hadolint) to catch bad practices and a scanner (Trivy — the image-scanning skill) on the built image. Automate both in CI.
Cheatsheet
the Dockerfile decides what's IN the container + how it RUNS. harden it.
1. MINIMAL BASE (highest impact): distroless / alpine / slim > full OS
fewer packages = fewer CVEs + smaller + fewer tools for an attacker
2. NON-ROOT: create + USER a non-root user (default is root -> escape = root)
3. NO SECRETS IN LAYERS: never COPY creds / ENV secrets
layers are inspectable — "removed" in a later layer STILL persists
-> BuildKit --secret / runtime injection
4. PIN + VERIFY: base by digest (not moving tag), package versions -> reproducible
5. MINIMISE: combine RUNs, remove build tools/caches in the SAME layer,
.dockerignore (no .git/secrets/junk), MULTI-STAGE build (build tools out of final)
6. drop capabilities + read-only filesystem (design for it)
7. LINT (hadolint) + SCAN (trivy) in CI
quick check: does it run as root? secrets in layers? full-fat base? unpinned?
Reading a Dockerfile
- A full-OS base image = large attack surface and many inherited CVEs; switching to distroless/alpine/slim removes most of them at once. The single highest-impact hardening choice.
- No
USER directive (runs as root) = a container escape or app compromise is immediately root; a non-root user is one of the most important mitigations. A common, high-value finding.
- A secret
COPY'd or ENV'd = it persists in the image layers, extractable by anyone who pulls the image, even if a later layer "deletes" it. A direct credential leak — the layer history is inspectable.
- Unpinned base tags / package versions = non-reproducible builds; you can't be sure what you shipped, and
latest can change under you. Pin by digest.
- Build tools and caches in the final image = unnecessary bloat and attack surface (compilers, package managers an attacker can use); multi-stage builds and same-layer cleanup remove them.
- A minimal, non-root, secret-free, pinned, multi-stage image = the hardened baseline; scanning it should find far less.
The fix / best practice
- Minimal base + non-root user + no baked secrets are the three highest-value habits; adopt them as defaults.
- Multi-stage builds to keep build-time tooling out of the runtime image.
- Pin base images by digest and pin package versions for reproducibility.
- BuildKit secrets or runtime injection for anything sensitive; never in layers.
.dockerignore to avoid copying .git, secrets, and junk.
- Lint (hadolint) and scan (Trivy) in CI, failing the build on bad practices and fixable high/critical vulnerabilities.
Pitfalls
- Running as root. The default, and a major mitigation missed; escape or compromise becomes root. Add a non-root
USER.
- Secrets in layers.
COPY/ENV of a credential persists in the inspectable layer history even if "removed" later — a real leak. Use build secrets or runtime injection.
- Fat base images. A full OS base inherits huge CVE surface and gives attackers tools/shells; use minimal bases.
- Unpinned tags.
latest and unpinned packages make builds non-reproducible and can change silently; pin by digest.
- Leaving build tooling in the final image. Compilers and package managers are attack surface; use multi-stage builds.
- Not linting/scanning. Bad practices and vulnerabilities ship silently; automate hadolint and Trivy in CI.
References
- Docker security best practices and Dockerfile reference
- hadolint (Dockerfile linter) and Trivy (image scanner) documentation
- CIS Docker Benchmark
- The container-image-scanning and supply-chain-for-images skills
Inputs
- Relevant source code, logs, network traces, or system specifications.
Outputs
- Analysis findings, security audit report, or generated code artifacts.
1---2name: dockerfile-hardening3description: Use when writing or reviewing a Dockerfile for security — non-root users, minimal base images, no secrets in layers, and the build practices that shrink the attack surface.4---5678## Prerequisites9- Target system, dependencies and environment configured.1011## Usage12### Purpose1314The Dockerfile decides what's in your container and how it runs — and a few habits separate a hardened image from a bloated, root-running, secret-leaking one. This skill covers the Dockerfile practices that shrink the attack surface: minimal bases, non-root execution, no secrets baked into layers, and pinned, verifiable dependencies. It's where container security starts, before the image ever runs.1516### When to use it1718Writing or reviewing any Dockerfile, and as a baseline for the whole container programme. It pairs with image scanning (which finds vulnerabilities) — hardening reduces what's there to be vulnerable in the first place.1920### Procedure21221. **Start from a minimal base image — the highest-impact choice.** Every package in the base is attack surface and a potential CVE. Prefer distroless, alpine, or slim variants over full OS images. A smaller base means fewer vulnerabilities, a smaller image, and less for an attacker to use (fewer shells and tools inside).232. **Run as a non-root user.** By default containers run as root, so a container escape or app compromise is root. Create and switch to a non-root user (`USER`), so the process has minimal privilege even if compromised. This is one of the most important hardening steps.243. **Keep secrets out of the image — they persist in layers.** Never `COPY` a credential or `ENV` a secret; Docker layers are inspectable, so a secret added in one layer remains even if "removed" in a later one. Use build secrets (BuildKit `--secret`) or inject at runtime, never bake them in.254. **Pin and verify what you install.** Pin base image tags (ideally by digest, not a moving tag) and package versions so builds are reproducible and you know what you shipped. Verify downloaded artifacts where possible.265. **Minimise layers and content.** Combine related `RUN` commands, remove build tools and caches in the same layer they're used (a package cache left in an earlier layer persists), and use `.dockerignore` so you don't copy secrets, `.git`, or junk into the image. Multi-stage builds keep build-time tools out of the final image.276. **Drop unnecessary capabilities and set a read-only filesystem** where possible (often at runtime, but design for it) — the container should have only what it needs.287. **Lint and scan the result.** Use a Dockerfile linter (hadolint) to catch bad practices and a scanner (Trivy — the image-scanning skill) on the built image. Automate both in CI.2930### Cheatsheet3132```33the Dockerfile decides what's IN the container + how it RUNS. harden it.34351. MINIMAL BASE (highest impact): distroless / alpine / slim > full OS36 fewer packages = fewer CVEs + smaller + fewer tools for an attacker372. NON-ROOT: create + USER a non-root user (default is root -> escape = root)383. NO SECRETS IN LAYERS: never COPY creds / ENV secrets39 layers are inspectable — "removed" in a later layer STILL persists40 -> BuildKit --secret / runtime injection414. PIN + VERIFY: base by digest (not moving tag), package versions -> reproducible425. MINIMISE: combine RUNs, remove build tools/caches in the SAME layer,43 .dockerignore (no .git/secrets/junk), MULTI-STAGE build (build tools out of final)446. drop capabilities + read-only filesystem (design for it)457. LINT (hadolint) + SCAN (trivy) in CI4647quick check: does it run as root? secrets in layers? full-fat base? unpinned?48```4950### Reading a Dockerfile5152- **A full-OS base image** = large attack surface and many inherited CVEs; switching to distroless/alpine/slim removes most of them at once. The single highest-impact hardening choice.53- **No `USER` directive (runs as root)** = a container escape or app compromise is immediately root; a non-root user is one of the most important mitigations. A common, high-value finding.54- **A secret `COPY`'d or `ENV`'d** = it persists in the image layers, extractable by anyone who pulls the image, even if a later layer "deletes" it. A direct credential leak — the layer history is inspectable.55- **Unpinned base tags / package versions** = non-reproducible builds; you can't be sure what you shipped, and `latest` can change under you. Pin by digest.56- **Build tools and caches in the final image** = unnecessary bloat and attack surface (compilers, package managers an attacker can use); multi-stage builds and same-layer cleanup remove them.57- **A minimal, non-root, secret-free, pinned, multi-stage image** = the hardened baseline; scanning it should find far less.5859### The fix / best practice6061- **Minimal base + non-root user + no baked secrets** are the three highest-value habits; adopt them as defaults.62- **Multi-stage builds** to keep build-time tooling out of the runtime image.63- **Pin base images by digest and pin package versions** for reproducibility.64- **BuildKit secrets or runtime injection** for anything sensitive; never in layers.65- **`.dockerignore`** to avoid copying `.git`, secrets, and junk.66- **Lint (hadolint) and scan (Trivy) in CI**, failing the build on bad practices and fixable high/critical vulnerabilities.6768### Pitfalls6970- **Running as root.** The default, and a major mitigation missed; escape or compromise becomes root. Add a non-root `USER`.71- **Secrets in layers.** `COPY`/`ENV` of a credential persists in the inspectable layer history even if "removed" later — a real leak. Use build secrets or runtime injection.72- **Fat base images.** A full OS base inherits huge CVE surface and gives attackers tools/shells; use minimal bases.73- **Unpinned tags.** `latest` and unpinned packages make builds non-reproducible and can change silently; pin by digest.74- **Leaving build tooling in the final image.** Compilers and package managers are attack surface; use multi-stage builds.75- **Not linting/scanning.** Bad practices and vulnerabilities ship silently; automate hadolint and Trivy in CI.7677### References7879- Docker security best practices and Dockerfile reference80- hadolint (Dockerfile linter) and Trivy (image scanner) documentation81- CIS Docker Benchmark82- The container-image-scanning and supply-chain-for-images skills8384## Inputs85- Relevant source code, logs, network traces, or system specifications.8687## Outputs88- Analysis findings, security audit report, or generated code artifacts.