Video by DreamsofCode · Watch on YouTube
Most engineers build Docker images the same way they've always done it — cargo-culting patterns without understanding the tradeoffs. This video debunks five common Dockerfile misconceptions, from base image choices to multi-stage builds, and shows how fixing them saves time, disk space, and money.
The core message: challenge your defaults. Alpine isn't always better. COPY . . isn't the problem — missing .dockerignore is. Multi-stage builds can shrink Go images from 272 MB to 2.3 MB.
.dockerignore)
4. Multi-Stage Builds: Slashing Image Size
5. Multi-Process Containers with Supervisord
Bonus: Pin Digests, Not Tags
Alpine is tiny, but it uses musl libc — not the glibc that virtually all application releases target. What looks like a size win often becomes a nightmare:
confluent-kafka, lmdb) fail entirely on Alpineapk add build-base) installs a full toolchain, negating the size advantage| Tag | Base | Size | Best For |
|---|---|---|---|
| Slim | Debian-based (glibc) | ~120 MB | Default — compatibility + small |
| Alpine | musl libc | ~30 MB | Only if you verify all deps work |
| Full | Full OS packages | ~1 GB | Development / debugging |
Prefer this FROM python:3.12-slim Avoid unless verified FROM python:3.12-alpine
Docker caches layers. If an earlier layer's input changes, all subsequent layers rebuild. Common mistake: COPY . . before RUN npm install. Every code edit invalidates the entire dependency install.
BAD — every code change re-runs npm install
COPY . .
RUN npm ci
Copy what changes slowly first, what changes fast last. Think of it like an onion — peel from the outside.
GOOD — dependency cache survives code edits
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
If only src/index.js changes, the npm ci layer is cached — instant rebuild.
COPY . . isn't the bad pattern. The bad pattern is sending garbage into the build context. Without .dockerignore, Docker sends node_modules, .git, logs, build artifacts — making builds slow and cache keys huge.
Some teams respond with 20+ precise COPY commands — fragile and error-prone.
Use .dockerignore the same way you use .gitignore:
node_modules/ .git/ *.log dist/ .env
Then COPY . . is clean and readable. Rebuilding shows zero context transferred for ignored files.
Clean and fast
COPY . . # Only non-ignored files are sent
Compiled languages like Go often get built in a single stage with a full base image — including the entire Go toolchain, compilers, and headers that the binary doesn't need at runtime.
BAD — 272 MB image
FROM golang:1.22-alpine
COPY . .
RUN go build -o /app .
CMD ["/app"]
Builder stage for compilation, minimal runtime stage for execution:
GOOD — 2.3 MB image
FROM golang:1.22-alpine AS builder
COPY . .
RUN CGO_ENABLED=0 go build -o /app .
FROM scratch
COPY --from=builder /app /app
CMD ["/app"]
| Base Image | Size |
|---|---|
golang:alpine | 272 MB |
alpine + binary | 11 MB |
scratch + binary | 2.3 MB |
| Image | Size | Shell | TLS Certs | Users |
|---|---|---|---|---|
scratch | 0 MB | No | No | No |
distroless | ~2 MB | No | Yes | Yes |
Tip: Use distroless for most cases — small but less annoying than scratch.
"One process per container."
It's a vibe, not a law. When your app needs nginx + a Python server and you're not at Kubernetes scale, splitting into two containers adds complexity that outweighs the benefit.
Use supervisord — a lightweight process control system:
FROM python:3.12-slim RUN apt-get update && apt-get install -y nginx supervisor COPY app.py /app/ COPY nginx.conf /etc/nginx/ COPY supervisord.conf /etc/supervisor/ CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
; supervisord.conf [program:nginx] command=nginx -g "daemon off;" autostart=true [program:app] command=python /app/app.py autostart=true
Multiple processes in one container is fine when they're tightly coupled and the alternative (separate containers) adds more operational debt.
Tags like node:22-slim or latest can be moved — accidentally or intentionally. Pin by digest for reproducible builds:
Fragile — tag can move FROM node:22-slim Immutable — pin by digest FROM node@sha256:abc123...
Use docker buildx imagetools inspect <image>:<tag> to find the digest.
D-Roast is a Rust utility that scans your Dockerfile and roasts bad patterns — npm install → npm ci, COPY . . without .dockerignore, missing digest pins. Run it locally or in CI.
| Misconception | Bad Pattern | Good Pattern |
|---|---|---|
| Alpine is always better | FROM ...:alpine | Prefer slim (glibc) unless verified |
| Cache doesn't matter | COPY . . before deps | Copy lockfile first, then npm ci, then code |
COPY . . is bad | Over-engineered multi-copy | .dockerignore + single COPY . . |
| Single-stage builds | One image for everything | Builder + minimal runtime stage |
| One process per container | Complex multi-container | Supervisord for tightly-coupled processes |
| Tags are permanent | FROM node:latest | Pin by digest hash |
Guide generated from Dockerfile Misconceptions by DreamsofCode