A Dockerfile review should catch anything that leaks a secret into the image, gives the running container wider privileges than its job calls for, makes the build impossible to repeat, or wastes time and space. All of that is visible in the file itself, before a single layer is built. This guide lists seven checks in the order that pays off most, with what the safe version looks like, so you can review a Dockerfile in a few minutes or hand the same list to a colleague.

1. Who the container runs as

If nothing says otherwise, the main process starts as the root user. OWASP names a user without privileges as the strongest guard against escalation, and shows the pattern: create a user in the Dockerfile, do the root-only work such as installing packages first, then switch with USER (OWASP cheat sheet). Docker's own guidance says the same for any service able to work without elevated rights (Docker docs).

The thing to look at is the last stage of the file. Root in an early build stage that is thrown away is harmless; root in the stage that becomes the image is the finding.

2. Which base image, exactly

FROM node or FROM ubuntu:latest means "whatever the publisher decides today". Docker explains that tags are mutable: a publisher can point a tag at a new image at any time, which is convenient for updates and bad for repeatability (Docker docs). A specific version tag is the minimum. Pinning a digest as well makes the base completely fixed, with a cost that the same page spells out: you opt out of automatic security fixes unless something updates the digest for you, and it suggests Dependabot for that.

A reviewer should accept either a version tag or a tag plus digest, and flag an untagged or latest base in a stage that ships.

3. Secrets that end up in a layer

Anything copied, set or passed during the build can be recovered from the image later. The classic forms are a token in ARG, a password in ENV, and a COPY of a .env file or a key. Deleting the file in a later RUN does not help, because the earlier layer still holds it. OWASP's advice is to keep sensitive data out of images and runtime commands and to use proper secret handling instead (OWASP cheat sheet).

The safe look-alike is RUN --mount=type=secret: the value exists only while that single instruction runs and is never written into the image. A file supplied that way should not be flagged.

4. The order of instructions and the build cache

Docker walks the instructions top to bottom and, for each one, checks whether it can reuse the stored result (Docker docs). Once one instruction changes, everything after it is rebuilt. So the file should copy the dependency manifest first, install dependencies, and only then copy the rest of the source. If all the source is copied ahead of the install, every edit to any file reinstalls every dependency, which is the typical cause of a build that feels slow for no reason.

This check often costs nothing to fix and saves the most minutes over a year of builds. A model asked to review code tends to be cautious about what it reports, a habit we looked at in a note on safe defaults in code review, and it applies here: a reviewer should still say plainly when the order is wrong.

5. Package lists left in the layer

On Debian-based images the package index is downloaded by apt-get update. Docker's example combines the update, the install with --no-install-recommends, and the removal of the index directory in one RUN, so the downloaded lists never become part of a layer (Docker docs). The docs also advise against adding packages out of habit: fewer packages mean fewer dependencies, a smaller image and faster builds.

Two separate RUN lines, one for the update and one for the install, are a finding for a second reason: a cached update step can go stale while the install runs against old lists.

6. ADD versus COPY, and what gets sent as context

COPY copies files from the build context or from another stage. ADD does that and can also fetch remote URLs and unpack archives automatically, which makes it surprising to read (Docker docs). Prefer COPY, and keep ADD for the cases that truly need the extra behavior, with a comment explaining the choice.

The context itself matters too. Without a .dockerignore, the whole directory is sent to the builder, including version-control history, local environment files and dependency folders. Docker's guide shows the file as the way to leave out what the build does not need, with patterns like those in .gitignore (Docker docs). A missing ignore file is both a speed problem and a secret-leak risk, because a broad COPY . . will then take the local .env along.

7. Multi-stage builds

Compilers, test tools and package caches belong in a build stage, not in the image people run. Multi-stage builds separate building from the final output, so the result contains only the files needed to run the application (Docker docs). The reviewer's question: does the final FROM start from a clean base and copy only artifacts from earlier stages with COPY --from? A single-stage file that installs a toolchain and then runs the app is the finding.

This check also supports the others: root, secrets and build tools in an earlier stage are much less serious when that stage does not ship.

A reading order that works

  1. Find the last FROM. That stage is the image.
  2. Check its base reference and the final USER.
  3. Search the whole file for ARG, ENV and COPY lines that can carry a credential.
  4. Read the order: manifest, install, then source.
  5. Look at every apt-get line.
  6. Confirm that a .dockerignore exists next to the file.

Where our skill fits

Our skill dockerfile-review reads one Dockerfile and lists a short set of mistakes: an image that ships as root, a base with no tag or the moving one, secrets passed by argument, environment value or copied file, source copied ahead of the install, and a package index left behind. Every line it prints names the instruction, the reason and the fix, and an empty result is stated plainly. It leaves the safe look-alikes alone, such as a mounted build secret or root in an early stage that is discarded. Use it before merging a change, or to find out why a build is slow. The whole store is at https://aiskills402.com.

When this does not apply

  • The file is only half the picture. What the container is given at run time, such as capabilities, mounted volumes, the network and resource limits, is outside the Dockerfile. A perfect file can still run with too much.
  • Orchestrator settings override it. A Kubernetes pod or Compose file can set the user, mount secrets or change the command, so check those as well before judging the whole system.
  • Some images need root. A few tools bind low ports or manage devices. The finding then becomes a question to answer in a comment, not an automatic error.
  • Pinning has a price. A digest keeps builds identical, but you must update it yourself, and an old pinned base can carry known flaws. Judge by how often someone will actually bump it.
  • A review does not scan the contents. Reading the file will not tell you whether the packages inside have known vulnerabilities; that needs an image scanner.
  • Our skill is a reader, not an oracle. Its reading can skip a rare pattern or flag something innocent, and it checks a fixed list. Treat its output as input to your decision and rebuild after changes to see the effect.

Read this post as Markdown: /blog/dockerfile-review-checklist.md · Atom feed.