10 Docker Anti-Patterns (And How to Fix Them)
I have audited many Dockerfiles across DevOps teams and startups. Despite differences in team size, maturity, and industry, the same architectural mistakes appear repeatedly.
These issues are rarely the result of negligence or lack of skill. Docker is easy to adopt but difficult to use correctly at scale. A Dockerfile copied from a tutorial may build successfully and pass initial tests, which makes it tempting to move on without revisiting it. Months later, teams are left diagnosing 15+ minute CI builds, multi-gigabyte images, and vulnerability scanners reporting critical findings.
This article outlines ten Docker anti-patterns that commonly appear in production environments and provides concrete, low-effort changes to correct them.
1. Non-Deterministic Base Images
The Anti-Pattern: Using the latest tag.
FROM node:latest
- Using
latestas a base image
Using latest creates a moving target. A build that works on Friday may fail on Saturday if the upstream image changes, breaking native dependencies or introducing incompatible updates.
Fix: Pin exact versions (runtime + OS variant) for reproducible builds. This ensures identical images every time. For maximum immutability, use digest pinning @sha256:… in CI/CD. In 2026, prefer Debian-based slim images for broad compatibility.
FROM node:20.11.1-bookworm-slim@sha256:<digest>
2. Bloated Base Images
Anti-pattern: Treating containers like mini VMs.
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y nodejs npm
This works, which is why it survives code review.
But full OS images quietly tax everything downstream: larger pulls, slower CI, noisier vulnerability scans, and patch cycles tied to an entire distribution instead of your runtime. None of this hurts locally. It only shows up once you operate at scale.
The Fix: Start from the minimum executable surface, not a familiar OS.
Your base image should exist for one reason only: to run the compiled artifact or runtime process. Anything beyond that is technical debt with interest.
For Node.js in 2026, the practical hierarchy looks like this:
FROM node:20.17.0-bookworm-slim
- Debian bookworm-slim remains the default choice for production
- glibc avoids native module surprises
- small enough to be efficient, large enough to be boring (boring is good)
FROM node:20.17.0-alpine
- Viable when image size is critical
- musl libc will surface edge cases in native dependencies
- requires explicit testing discipline, not hope
FROM cgr.dev/chainguard/node:20
- No shell, no package manager, no OS noise
- Forces runtime immutability by design
- Ideal for hardened production tiers and regulated environments
Switching off full OS images typically cuts image size 80–95%, reduces CVE noise dramatically, and shortens CI pipelines without changing application code.
3. Inefficient Build Caching
Anti-pattern: Busting the dependency cache on every commit.
FROM node:20.17.0-bookworm-slim
WORKDIR /app
COPY . .
RUN npm install
This invalidates the most expensive layer in your build every time any source file changes. CI still “works”, but build times quietly degrade as the repo grows.
At scale, this shows up as slow pipelines, wasted compute, and developers normalising 10–15 minute builds.
Fix: Order layers by change frequency. Let Docker cache do its job.
FROM node:20.17.0-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
COPY . .
- Dependency manifests change rarely → cached long-term
- Application code changes frequently → cheap rebuilds
- BuildKit cache mounts persist npm artifacts across builds
This pattern turns dependency installs from a recurring cost into a near one-time operation.
4. Violation of Least Privilege (Root User)
Anti-pattern: Accepting the default user.
By default, containers run as root. If a process is compromised and escapes the container, it escapes as root. That’s not theoretical; that’s how real incidents cascade.
Fix: Drop privileges explicitly.
Most modern runtime images already ship with an unprivileged user. Use it.
FROM node:20.17.0-bookworm-slim
WORKDIR /app
RUN chown -R node:node /app
USER node
COPY --chown=node:node . .
CMD ["node", "server.js"]
This is baseline defense-in-depth for production, not an “advanced hardening” step.
5. Hardcoded Secrets
Anti-pattern: Baking credentials into images.
ENV AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
Secrets embedded in images persist forever in layers, registries, caches, and backups. Rotating them doesn’t remove them.
Fix: Inject secrets at runtime or mount them ephemerally at build time.
# Build-time secret (e.g., private npm registry)
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci --omit=dev
Runtime injection (Docker/Kubernetes secrets, env vars) is safest.
If a secret ever appears in docker history, it’s already leaked.
6. Leaking the Build Context
Anti-pattern: Shipping your entire repo to the Docker daemon.
No .dockerignore means .git, node modules, test artifacts, and editor configs are sent on every build. This bloats context uploads, invalidates caches, and slows CI for no functional reason.
Fix: Be aggressively explicit about what doesn’t belong in the image.
.git
node_modules
.env
coverage
.vscode
dist
build
Dockerfile
On large repositories, this alone can shave minutes off CI builds.
7. Lack of Observability (Health Checks)
Anti-pattern: Assuming “running = healthy.”
A process can be alive but deadlocked, DB-disconnected, or otherwise non-functional. Without health indicators, orchestrators keep sending traffic to zombies. CI/CD and production monitoring silently degrade.
Fix: Define explicit health checks.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
In Kubernetes, mirror this with liveness and readiness probes.
Orchestrators then restart stuck containers or reroute traffic automatically, with no human intervention needed.
8. Monolithic Dockerfiles
Anti-pattern: One Dockerfile for everything.
Dev tools like git and curlin production? Yep. Bloated images, unnecessary attack surface, slower deployments. This “convenience-first” approach rarely pays off beyond local testing.
Fix: Multi-stage builds. Keep only what’s needed at runtime.
# syntax=docker/dockerfile:1
FROM node:20.17.0-bookworm-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build
FROM node:20.17.0-bookworm-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/index.js"]
The final image contains only runtime artifacts.
Smaller. Safer. Predictable. No surprises in prod.
9. Inefficient Layering
Anti-pattern: Multiple RUNs without cleanup.
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
Deleting files in one layer doesn’t shrink previous layers image size still balloons. At scale, this is silent infrastructure debt.
Fix: Chain commands and clean in the same layer.
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
Temporary files never persist. Cache efficiency and image size stay predictable. This is how production-grade engineers think about every layer: no surprises, minimal footprint.
10. Stale Base Images
Anti-pattern: Set and forget.
A base image untouched for months is a CVE time bomb. It doesn’t matter that the app works attacks don’t care either.
Fix: Scan + automate updates.
- Scan images in CI:
docker scout cvesor Trivy - Automate base bumps: Renovate, Dependabot, GitHub-native PRs
- Cadence: monthly (weekly if security-critical)
- Always test updates in staging before prod
Example Renovate snippet:
{
"packageRules": [
{
"matchManagers": ["dockerfile"],
"schedule": ["every weekend"]
}
]
}
This combination keeps CVEs low with minimal manual effort.
Impact
Applied together, these 10 fixes typically:
- Cut image size 70–90%
- Reduce CI build times 50–80%
- Lower vulnerability counts dramatically
Start with the worst offender run docker scout cvesor trivy image today. Most changes are one-line or small-block diffs, but the operational payoff is massive.
Most Docker problems aren’t Docker problems at all. They’re the result of copying a Dockerfile that worked once and never revisiting it. Treating container configuration as static, instead of something that evolves with your application and platform. I’ve been guilty of this too everyone has. We’re busy, and if it works, we move on.
But small inefficiencies compound fast. That 4-minute build multiplies into hours per week. That bloated image racks up storage and transfer costs. That root user? A real security risk, not just a talking point in a blog.
Here’s a pragmatic approach: pick one fix from this list either the one that irritates you most or the one that’s easiest to implement, and do it this week. Then tackle another next week.
You won’t transform your Docker setup overnight. But in a month, you’ll have smaller images, faster builds, and fewer 2 a.m. pages. That incremental improvement is worth every minute.
Top comments (0)