container-image-review
Skill0 starsChanged 2 months agoApache-2.0View on GitHub
Review a Dockerfile or container image for correctness, size, build speed, security and reproducibility - base image choice, layer caching, multi-stage builds, non-root users, pinned versions, secrets leaked into layers, signal handling and healthchecks. Use when reviewing a Dockerfile, when an image is unexpectedly large or slow to build, or before an image is used in production.
- Reads credentials
- Deletes or force-pushes
- Installs packages
In this repository26 other files it gives its agents.
- AGENTS.mdAGENTS.md
- aws-production-debuggingSkill
- github-actions-debuggingSkill
- postgresql-troubleshootingSkill
- docker-debuggingSkill
- incident-triageSkill
- postmortem-analysisSkill
- production-debuggingSkill
- kubernetes-networkingSkill
- kubernetes-production-readinessSkill
- kubernetes-resource-debuggingSkill
- kubernetes-troubleshootingSkill
- linux-network-debuggingSkill
- linux-performanceSkill
- linux-troubleshootingSkill
- dns-troubleshootingSkill
- http-debuggingSkill
- tls-debuggingSkill
- observability-debuggingSkill
- prometheus-troubleshootingSkill
- slo-designSkill
- secrets-management-reviewSkill
- technology-jobSkill
- terraform-reviewSkill
- terraform-safe-changesSkill
- terraform-troubleshootingSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 4 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 213
Read scanner output with judgement.
- Distinctive: line 316
Verify in this order, because optimisation can break correctness silently:
- Distinctive: line 337
Bad: Use Alpine everywhere for size.
- Distinctive: line 345
Bad: Treat every scanner finding as a blocker.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Container Image Review
Review an image for the four things that actually matter: does it work, how big is it, how fast does it build, and what does it expose.
Purpose
Use this when:
- Reviewing a Dockerfile in a pull request.
- An image is unexpectedly large, or builds slowly.
- Preparing an image for production use.
- A vulnerability scan reports findings and you need to know which matter.
Do not use this when:
- A container is failing at runtime — use
docker-debugging. - The question is Kubernetes configuration around the image — use
kubernetes-production-readiness.
What this skill assumes: access to the Dockerfile and, ideally, the built image. Reviewing the Dockerfile alone misses what the base image contributes, which is often most of the size and most of the vulnerabilities.
Operating Procedure
- Build it and measure. Size, layer count, build time. Numbers before opinions.
- Review correctness first. An image that is small and insecure is better than one that does not run.
- Check the base image. It usually determines size, CVE count and libc compatibility.
- Check layer ordering for cache efficiency — the difference between a 10-second and a 10-minute rebuild.
- Check what got in that should not: secrets, build tools, caches,
.git. - Check the runtime posture: user, signals, healthcheck, filesystem.
- Produce ranked findings — correctness, then security, then size, then speed.
Size and layer data from docker images and docker history are Observed. A claim that an
image "should" be smaller is a Recommendation until you have built the alternative.
Investigation
Step 1 - measure
docker build -t <image>:review .
docker images <image>:review --format '{{.Size}}'
docker history <image>:review --human --format '{{.Size}}\t{{.CreatedBy}}' | head -25docker history shows the size of each layer and the instruction that produced it. Sort your
attention by size: usually two or three layers account for nearly everything, and they are the only
ones worth optimising.
docker run --rm <image>:review sh -c 'du -sh /* 2>/dev/null | sort -rh | head -10'This shows where the size actually is inside the final filesystem — frequently a package cache, a dependency directory that should have been pruned, or a copied build context nobody noticed.
Step 2 - base image
| Base | Typical size | Use when | Watch for |
scratch | 0 | Fully static binaries | No shell, no CA certificates, no timezone data |
distroless | 2-20 MB | Compiled languages, production | No shell — debug with :debug variants or ephemeral containers |
alpine | ~5 MB | Small images, tolerant of musl | musl, not glibc — breaks glibc-compiled binaries and can change DNS behaviour |
debian:*-slim | ~30 MB | glibc compatibility with moderate size | Still ships a package manager |
ubuntu | ~70 MB | Familiarity, broad tooling | Large, more CVEs to triage |
Language images (node, python) | 300 MB-1 GB | Build stages | Rarely appropriate as a final stage |
The Alpine question comes up constantly and the honest answer is conditional. Alpine is genuinely
small, but musl is not glibc: some binaries will not run, some language runtimes have known
performance differences, and DNS resolution behaviour differs in ways that surface as intermittent
failures. debian:slim with a multi-stage build often gets you most of the size benefit without
the compatibility risk.
docker image inspect <image>:review --format '{{.Config.User}} {{.Architecture}} {{.Os}}'Step 3 - multi-stage builds
The single highest-impact change for most images. Build in a full-featured stage, copy only the artifact into a minimal one:
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]Two things this gets right that are easy to miss:
- Dependencies are fetched before the source is copied. The
go mod downloadlayer is cached until the manifest changes, so ordinary code edits skip it entirely. The same principle applies topackage.jsonbeforenpm ci, andrequirements.txtbeforepip install. CGO_ENABLED=0produces a static binary, which is what makesstatic-debian12orscratchviable. A dynamically linked binary in one of those images fails with exit 127, and the cause is not obvious from the error.
Anything present only in an earlier stage is genuinely absent from the final image — compilers, credentials used at build time, source code.
Step 4 - layer caching and ordering
docker build --progress=plain -t <image>:review . 2>&1 | grep -E 'CACHED|DONE'Order instructions from least to most frequently changing:
FROM node:22-slim # changes rarely
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY package*.json ./ # changes occasionally
RUN npm ci --omit=dev
COPY . . # changes every commitRules that matter:
COPY . .invalidates everything after it, so it goes as late as possible.- Combine
apt-get updatewithinstallin oneRUN. Split across layers, a cachedupdatelayer serves a stale package index and the install fails or pulls the wrong versions. - Clean up in the same
RUNthat created the mess.rm -rf /var/lib/apt/lists/*in a later layer removes nothing from the image — the earlier layer still contains the files. Layers are additive; deleting in a later layer only hides. - Use a
.dockerignore. Without one,.git,node_modulesand local build output are sent to the daemon and frequently end up in the image.
cat .dockerignore 2>/dev/null || echo "NO .dockerignore"Step 5 - secrets and what leaked in
docker history --no-trunc <image>:review | grep -iE 'token|password|secret|key|ARG'
docker run --rm <image>:review sh -c 'ls -la /root/.ssh /root/.aws 2>/dev/null; cat /app/.env 2>/dev/null'Anything that exists in any layer is in the image, permanently, even if a later layer deletes it. Anyone who can pull the image can extract it.
| Wrong | Right |
ARG NPM_TOKEN then using it in a RUN | RUN --mount=type=secret,id=npm ... (BuildKit) |
COPY .npmrc . with credentials | BuildKit secret mount |
ENV API_KEY=... | Inject at runtime from a secret store |
COPY . . with a .env in the context | .dockerignore |
| Baking a private key into the image | Mount it at runtime |
ARG values are visible in docker history for anyone with the image. BuildKit secret mounts
never become part of a layer, which is the entire point of them.
Step 6 - runtime posture
USER 10001:10001
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s CMD ["/app", "healthcheck"]
ENTRYPOINT ["/app"]| Check | Why |
USER set to a non-root, numeric UID | Root in a container is root on the host in a container escape. A numeric UID also lets Kubernetes enforce runAsNonRoot. |
Exec-form ENTRYPOINT | Shell form makes sh PID 1, so SIGTERM never reaches the application and every stop is a SIGKILL |
| Versions pinned | FROM node:22-slim moves; FROM node:22.8.0-slim@sha256:... does not |
| No build tools in the final stage | Compilers and package managers are attack surface and size |
HEALTHCHECK present for non-Kubernetes use | Kubernetes uses its own probes and ignores this |
| CA certificates present if the app makes HTTPS calls | scratch and distroless/static include them; scratch alone does not |
Step 7 - vulnerability scan, read critically
trivy image --severity HIGH,CRITICAL <image>:review
trivy image --severity HIGH,CRITICAL --ignore-unfixed <image>:reviewRead scanner output with judgement. Most findings in a typical image are in base-image packages that the application never invokes, and many have no fix available. Triage in this order:
- Is it reachable? A CVE in a library your code never calls is a lower priority than one in your request path.
- Is there a fix?
--ignore-unfixedseparates what you can act on from what you can only track. - Is it in the base image? Then the fix is a base image update, not a code change — and rebuilding on a current base fixes many at once.
A smaller base image reduces the count dramatically, which is a real security benefit and also the reason the raw count is a poor metric on its own.
Decision Tree
Reviewing an image
├── Does it run? → docker-debugging first if not
├── Size
│ ├── > 1 GB → build tools in the final stage → multi-stage
│ ├── 200 MB-1 GB → language base as final stage → slim or distroless
│ └── Package cache present → clean in the same RUN
├── Build speed
│ ├── Dependencies reinstalled every build → COPY manifests before source
│ └── No .dockerignore → add one
├── Security
│ ├── Running as root → USER with a numeric UID
│ ├── Secrets in history → rotate them, then use BuildKit mounts
│ ├── Unpinned base → pin by digest
│ └── Build tools in final stage → multi-stage
└── Runtime
├── Shell-form ENTRYPOINT → exec form
└── No CA certificates but makes HTTPS calls → add themCommon Causes
| Finding | Effect | Fix | Risk |
| Language base as final stage | 10-50x larger than needed | Multi-stage into slim/distroless | mutating |
COPY . . before dependency install | Full reinstall on every commit | Copy manifests first | mutating |
Cleanup in a separate RUN | Files still in the earlier layer | Clean in the same RUN | mutating |
No .dockerignore | .git and local artifacts in the image | Add one | mutating |
Secret in ARG or ENV | Extractable by anyone with the image | Rotate, then BuildKit secret mount | mutating |
| Running as root | Larger blast radius on escape | USER with a numeric UID | mutating |
| Unpinned base image | Irreproducible builds | Pin by digest | mutating |
| glibc binary on Alpine | Exit 127 at runtime | Match libc, or build static | mutating |
Safety
Read-only
docker history, image inspect, trivy image, reading the Dockerfile, and running the image
with an overridden entrypoint to list files. These inspect and do not modify.
Building an image is effectively read-only with respect to your systems, but it does execute
arbitrary instructions from the Dockerfile and pull from the network. Do not build an untrusted
Dockerfile on a machine with credentials you care about — RUN executes as root inside the build,
with network access.
Potentially disruptive
- Changing the base image — can change libc, the shell, the package manager, the default user, and the CA bundle. Test the resulting image before shipping; this is the change most likely to break something subtly.
- Adding
USER— the application may write to paths it can no longer write to. Fix ownership in the build rather than reverting to root. - Pinning by digest — good practice, and it stops automatic security updates from the base. Pair it with automated digest bumps.
- Pushing a new tag over an existing one — anything pulling that tag gets different content with no visible change. This is the argument against mutable tags in production.
Destructive
docker system prune -ato reclaim build space — removes all unused images and the build cache. With--volumesit also removes volumes not attached to a running container, which includes stopped databases. Required first: confirm what would be removed by listing images and volumes explicitly.- Deleting image tags from a registry — if something in production references that tag or digest, it cannot be pulled again, and any node that needs to restart a pod will fail. Required first: confirm no running deployment references it, and check the registry's retention and immutability settings.
- Rotating a secret found in image history — necessary, and it will break anything still using the old value. Required first: identify every consumer, then rotate. Note the secret must be treated as compromised regardless: it is in every copy of that image everywhere.
Verification
Before: 1.2 GB, 47 layers, 4m12s rebuild on a code change, runs as root, 34 HIGH CVEs
After: 38 MB, 9 layers, 11s rebuild on a code change, runs as UID 10001, 2 HIGH CVEsdocker images <image>:review --format '{{.Size}}'
docker image inspect <image>:review --format '{{.Config.User}}'
time docker build -t <image>:review . # second run, after a source-only change
docker run --rm <image>:review <smoke-test-command>
trivy image --severity HIGH,CRITICAL --ignore-unfixed <image>:reviewVerify in this order, because optimisation can break correctness silently:
- It still runs and passes a smoke test. A smaller image that fails at runtime is a regression.
- The cache works. Change one source file and rebuild; dependency layers should show
CACHED. - The runtime posture is what you intended — user, entrypoint form, certificates present.
- Then the size and CVE numbers.
Anti-Patterns
Bad: FROM node:22 for the final stage because the build needs npm.
Better: Multi-stage. The final image needs the runtime, not the toolchain — usually a 10-20x
size difference and a large reduction in attack surface.
Bad: RUN rm -rf /var/lib/apt/lists/* as its own instruction.
Better: Layers are additive. Deleting in a later layer leaves the files in the earlier one and
adds a layer. Clean in the same RUN.
Bad: ARG GITHUB_TOKEN to install private dependencies.
Better: It is visible in docker history forever. Use a BuildKit secret mount; if one has
already shipped, the token is compromised and must be rotated.
Bad: Use Alpine everywhere for size.
Better: musl is not glibc. Check that your runtime and all native dependencies support it;
debian:slim in a multi-stage build is often the better trade.
Bad: Ship with :latest.
Better: You cannot say what is running, cannot reproduce a build, and cannot roll back to a
known image. Pin by digest.
Bad: Treat every scanner finding as a blocker. Better: Triage by reachability and fixability. A wall of unfixable base-image CVEs teaches the team to ignore the scanner entirely, which is worse than not running it.
Composition
docker-debugging— when the image does not run correctly.kubernetes-production-readiness— the deployment configuration around the image.secrets-management-review— when the review finds credentials in layers or history.github-actions-debugging— the pipeline that builds and pushes the image.
References
- Docker: Building best practices
- Docker BuildKit: build secrets — how to use a credential at build time without putting it in a layer.
- Distroless images — variants and their contents.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

