Review the Dockerfile your agent wrote

Agent-written Dockerfiles build, run and often ship your keys. Six things to check in review, what to change, and how to make the agent get them right first time.

Containers for AI, part 3: Review the Dockerfile your agent wrote. A faint file outline with one line marked by a glowing dot.

Ask a coding agent to containerise an app and you get a Dockerfile in seconds. It builds. It runs. In review, it’s easy to skim: it’s eight lines, and the app works.

The trouble is where models learned to write Dockerfiles: tutorials and quick-start READMEs, written to get something running, not to ship. The same few shortcuts turn up again and again.

Six things to check first, in a Dockerfile as an agent wrote it. 1: FROM node:latest moves under you. 2: COPY . . copies .env and .git too. 3: ENV OPENAI_API_KEY is saved in the image forever. 4: RUN curl piped to sh runs whatever it gets. 5: RUN npm install ignores the lockfile. 6: CMD with no USER runs as root.

The six checks

1. Is the base image pinned? node:latest today is a different image next month. Pin a version and its digest: FROM node:22-slim@sha256:…. docker buildx imagetools inspect node:22-slim prints the digest. Dependabot or Renovate can keep it current, through pull requests you review.

2. What does COPY copy? COPY . . takes everything in the folder, including .env, .git and whatever the agent left lying around. Copy named paths, and add a .dockerignore:

.env*
.git
node_modules
*.pem

3. Are there secrets in ENV or ARG? Both are saved in the image. Anyone who can pull it can read them with docker history or docker inspect, even if a later step “removes” them. Pass runtime secrets when the container starts (--env-file, or your platform’s secrets). For build-time secrets, use a secret mount, which never lands in a layer:

RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci

4. Is anything piped to a shell? curl … | sh runs whatever that URL serves on the day you build. Install from a package manager, or download a pinned version and check its checksum.

5. Does the install use the lockfile? npm install can resolve newer versions than the ones you tested. npm ci installs exactly what package-lock.json says, and --ignore-scripts stops install scripts, a favourite place to hide malware. For Python, pip install --require-hashes -r requirements.txt does the same job.

6. Who does it run as? With no USER line, the app runs as root inside the container. Add USER with a non-root user, or start from an image that already has one.

After review

Put together, the fixes usually mean two stages: one that builds with all the tools it needs, and a small final image that has only the app. No shell, no package manager, nothing for an attacker to work with.

The same app after review: a build stage from a pinned node:22-slim digest that copies only package.json, package-lock.json and src and runs npm ci with ignore-scripts; and a run stage from a pinned distroless Node.js image that copies the app and runs as nonroot. Outside the file: secrets come in at run time, nothing is piped to a shell, and a .dockerignore drops .env and .git.

You don’t have to build minimal base images yourself. Distroless, Chainguard and Docker Hardened Images (free and open source since December 2025) all ship images without a shell or package manager, with a software bill of materials, and with non-root variants.

Make the agent get it right

Reviewing the same six things on every pull request gets old. Two cheaper fixes:

  • Tell the agent. Add the six rules to your AGENTS.md or CLAUDE.md, the file the agent reads in every session. It will follow them far more often than it guesses them.
  • Check it in CI. hadolint flags most of these in the Dockerfile itself, latest tags and missing pins included. Trivy or Grype then scans the built image for known vulnerabilities. A failing check catches what a tired reviewer won’t.