← 全部新闻
devops5 分钟阅读

Dockerfile anti-patterns that AI assistants keep producing

作者:Seaworthy · 2026年3月15日

Overhead aerial view of colorful shipping containers stacked at a port with a cargo crane visible

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.

本文由 AI 生成,总结了公开可用的来源。

来自 Seaworthy 团队

Seaworthy 会扫描你的代码仓库,发现类似本文提到的各种问题。

安全漏洞、泄露的密钥和配置错误——在部署前捕获。免费运行,无需账户。

$ npx seaworthycode