Dockerfile anti-patterns that AI assistants keep producing
Von Seaworthy · 15. März 2026
Photo by Bernd Dittrich on Unsplash
Ask any AI assistant to write you a Dockerfile and it will produce something that works. It will also, with near-certainty, contain at least three of the six mistakes described below. Not because the models are bad at Docker, but because they optimize for "runs on my machine" rather than "safe to ship to production."
Here is what keeps showing up, and what to do about it.
Running as root
Every LLM-generated Dockerfile I have reviewed runs the process as root unless you explicitly say otherwise. The model never asks. Root inside a container is not the same as root on the host, but the gap is smaller than most developers assume. A container escape, a path-traversal bug, or a supply-chain compromise in a dependency all become catastrophically worse when the process has uid 0.
The fix is one line:
RUN addgroup --system app && adduser --system --ingroup app app
USER app
Add it before your CMD or ENTRYPOINT. If your app needs to bind to port 80, use a reverse proxy instead of running as root.
Mutable base image tags
FROM node:20 does not mean what you think it means. It means "whatever the maintainers decide node:20 points to today." A build that passes CI on Tuesday can fail on Wednesday because an upstream layer changed. Worse, a compromised registry or a hijacked base image will silently propagate into every build.
Pin to the digest:
FROM node:20.12.2-alpine3.19@sha256:bf77dc26e48ea95fca9d1aceb5acfa69d2e546b765ec2abfb502975b1a2d27f4
Yes, you need to update that digest periodically. That is the point: updates become an explicit, reviewable choice rather than a silent surprise.
COPY . . before installing dependencies
This one is pure inefficiency that compounds every time someone saves a source file. Docker builds layers in order and caches each layer until something above it changes. Copying your entire source tree before running npm install means that any change to any file, including a README edit, invalidates the dependency installation layer and forces a full reinstall.
The correct order:
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
Dependencies are cached as long as package.json and the lockfile are unchanged. Source changes only invalidate the final COPY. On a medium-sized Node project this can cut build times from three minutes to fifteen seconds.
Secrets baked into layers
AI assistants love ENV and ARG for secrets. They will write things like ENV DATABASE_URL=... or ARG API_KEY without blinking. The problem is that both instructions are recorded in the image history. Anyone who can pull your image can run docker history --no-trunc and read every value.
# What the assistant wrote
ARG API_KEY
RUN curl -H "Authorization: Bearer $API_KEY" https://internal.api/seed-data
# What ends up in the image history, visible to anyone
ARG API_KEY=sk-prod-...
Secrets needed at build time belong in BuildKit secret mounts, which are never written to any layer:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=api_key \
API_KEY=$(cat /run/secrets/api_key) \
curl -H "Authorization: Bearer $API_KEY" https://internal.api/seed-data
Secrets needed at runtime should come from environment variables injected by your orchestrator, not baked into the image at all.
Missing .dockerignore
Without a .dockerignore, every COPY . . instruction sends your entire working directory into the build context: node_modules, .env, .git, build artifacts, test fixtures, the lot. This does two things you do not want. It bloats the image. And it risks shipping files that contain credentials.
A sensible starting point:
.git
.env
.env.*
node_modules
npm-debug.log
*.test.ts
coverage/
dist/
The .env line is the one that matters most. A surprising number of production images contain a .env file with real credentials because no one added a .dockerignore.
Bloated production images from skipping multi-stage builds
A single-stage build that compiles TypeScript ships the TypeScript compiler, ts-node, all dev dependencies, and often the raw source files into the production image. There is no reason for any of that to be there.
Multi-stage builds solve this cleanly:
# syntax=docker/dockerfile:1
FROM node:20.12.2-alpine3.19@sha256:bf77dc26e48ea95fca9d1aceb5acfa69d2e546b765ec2abfb502975b1a2d27f4 AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20.12.2-alpine3.19@sha256:bf77dc26e48ea95fca9d1aceb5acfa69d2e546b765ec2abfb502975b1a2d27f4 AS production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
RUN addgroup --system app && adduser --system --ingroup app app
USER app
CMD ["node", "dist/index.js"]
The production image contains only the compiled output and runtime dependencies. Dev tools never touch it. The attack surface shrinks and so does the image size, often by several hundred megabytes.
None of these are obscure edge cases. They are the default output of every major AI coding assistant when you ask for a Dockerfile without specifically constraining the request. The models know the right patterns exist; they just do not apply them unless pushed. Worth pushing. Seaworthy flags all six of these patterns automatically.
Dieser Artikel wurde von KI generiert und fasst öffentlich verfügbare Quellen zusammen.
Seaworthy scannt dein Repository nach den Problemen, die in Artikeln wie diesem behandelt werden.
Sicherheitslücken, offengelegte Geheimnisse und Fehlkonfigurationen — erkannt, bevor du bereitstellst. Kostenlos, kein Konto nötig.