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
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
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
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
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/*
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
(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)