DEV Community

Cover image for 7 Dockerfile Mistakes That Are Quietly Costing You
James Joyner for DevOps AI ToolKit

Posted on

7 Dockerfile Mistakes That Are Quietly Costing You

Most Dockerfiles work. That's the problem — "it builds and runs" hides a lot of quiet costs in security, speed, and size that don't announce themselves until an audit, an incident, or a cloud bill does it for them. Here are seven mistakes I see constantly, and what to do instead.

1. Running as root

By default, the process in your container runs as root — and if someone breaks out, they're root on a surface they shouldn't be. Add a non-root user and switch to it:

RUN useradd --system --uid 10001 appuser
USER appuser
Enter fullscreen mode Exit fullscreen mode

Cheap, and it closes off a whole category of "well, at least it wasn't root" incidents.

2. FROM some-image:latest

latest is not a version — it's "whatever was newest when this happened to build." Two builds a week apart can produce different images with no diff to explain it, and a surprise base upgrade is a fun way to spend a Friday. Pin a specific tag, ideally by digest:

FROM node:20.11.1-slim
Enter fullscreen mode Exit fullscreen mode

3. Baking secrets into layers

COPY .env . or ARG API_KEY followed by using it — and now the secret lives in an image layer forever, recoverable by anyone who pulls the image, even if a later layer deletes the file. Layers are immutable and additive; you can't delete your way out of a leak. Use build secrets (--mount=type=secret) or inject at runtime, never at build.

4. No .dockerignore

Without one, COPY . . sweeps your .git directory, local env files, node_modules, and test data into the build context — bloating the image and, worse, potentially baking credentials and history into a layer. A five-line .dockerignore is one of the highest-leverage files in the repo.

5. Layer order that destroys your cache

COPY . .
RUN npm ci      # ← reinstalls on EVERY code change
Enter fullscreen mode Exit fullscreen mode

Docker invalidates every layer after the first change. Copy the lockfile and install dependencies before copying the rest of your source, so a one-line code change doesn't trigger a full reinstall. This is a build-speed bug hiding as a style choice.

6. Leaving package manager cruft in the image

RUN apt-get update && apt-get install -y curl
Enter fullscreen mode Exit fullscreen mode

That leaves the apt lists sitting in the layer. And cleaning them in a separate RUN doesn't help — the bytes are already committed to the earlier layer. Do it all in one:

RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*
Enter fullscreen mode Exit fullscreen mode

7. No HEALTHCHECK

Without one, Docker (and your orchestrator) only knows whether the process is alive — not whether the app can actually serve. A container can be "up" and completely wedged. A healthcheck that hits a real endpoint lets the platform notice and recycle it:

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:8080/healthz || exit 1
Enter fullscreen mode Exit fullscreen mode

(Make sure that endpoint checks something real, not just "is the web server running" — but that's a whole other article.)


The theme

None of these break the build. They surface later — as a security finding, a slow pipeline, a bloated registry, or a container that's "healthy" while failing. The fixes are all a line or two; the hard part is remembering them at 4 p.m. on a Dockerfile you just want to ship.

Two things that help me not forget:

  • I run Dockerfiles and Compose files through a client-side validator (runs in your browser, nothing uploaded) to catch the structural stuff before it ships.
  • And I keep a Docker troubleshooting toolkit for when one of these mistakes graduates into an actual error at runtime.

Which of these did you learn the hard way? Mine was #3, and I think about it more than I'd like to admit.

Top comments (0)