Dev.to Security 🔐 Cybersecurity 👁 1 📖 3 min read

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

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.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.