Six Dockerfile Mistakes That Cost You
28 Sep 2026Most slow, bloated, or insecure Docker images come from the same handful of Dockerfile habits. Fix these six and you’ll cut build time, image size, and attack surface without touching anything else.
1. Copying everything before installing dependencies
COPY . .
RUN npm install
Every code change — even a one-line fix in a file npm install never touches — invalidates this layer and forces a full reinstall. Copy only the dependency manifest first, install, then copy the rest:
COPY package.json package-lock.json ./
RUN npm install
COPY . .
Now editing application code leaves the install layer cached.
2. Running as root
The default user in most base images is root, and most Dockerfiles never change that. If your app doesn’t need root, don’t run as root — it’s a bigger blast radius for zero benefit.
RUN useradd --uid 1000 --create-home appuser
USER appuser
3. One giant layer with secrets baked into history
RUN echo "API_KEY=abc123" >> .env
RUN some-build-step
RUN rm .env
Deleting the file in a later layer doesn’t remove it from the image — Docker layers are additive, and the earlier layer still contains the secret. Anyone with docker history or access to the layer blobs can pull it back out. Use build secrets instead:
RUN --mount=type=secret,id=api_key \
API_KEY=$(cat /run/secrets/api_key) some-build-step
docker build --secret id=api_key,src=./api_key.txt .
The secret is available only during that RUN, never written to a layer.
4. No .dockerignore
Without one, COPY . . sends your entire directory — node_modules, .git, build artifacts, local .env files — into the build context and potentially into the image. This bloats the image and can leak local secrets straight into a layer.
node_modules
.git
.env
*.log
dist
5. Not pinning base image versions
FROM node
This resolves to whatever node:latest is today, which is not what it’ll be in three months. A rebuild after a base image update can break silently on a Node major version bump you never asked for. Pin a specific tag:
FROM node:20-bookworm-slim
Still not perfectly reproducible — the tag can be repushed — but far more stable than floating on latest. For true reproducibility, pin the digest (node:20-bookworm-slim@sha256:...).
6. Installing unnecessary packages, and leaving build tools in the final image
FROM node:20
RUN apt-get update && apt-get install -y build-essential python3
COPY . .
RUN npm install && npm run build
CMD ["node", "server.js"]
build-essential and any native-module compile toolchain only needs to exist during the build, not at runtime. Left in, it’s dead weight and extra attack surface. Use a multi-stage build:
FROM node:20 AS build
RUN apt-get update && apt-get install -y build-essential python3
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
The final image never sees the compiler.
What this buys you
| Mistake | Cost |
|---|---|
| Copy-then-install order | Every code edit reinstalls deps |
| Running as root | Bigger blast radius on compromise |
| Secrets in layer history | Leaked keys, pullable forever |
No .dockerignore |
Bloated, leaky build context |
| Unpinned base image | Builds break without warning |
| No multi-stage build | Build tools ship to production |
Run docker history yourimage on an existing image before you fix any of this — it’ll show you exactly which layer is the bloated or leaky one, instead of guessing.