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.
답글 남기기