Dockerfile Best Practices That Actually Matter
Introduction¶
Most Dockerfiles work. Far fewer are small, fast to rebuild, and clean in a scanner. The difference is a short list of habits. This post covers the ones with the biggest payoff, with before/after examples you can apply today.
Concepts¶
An image is a stack of read-only layers. Each instruction in a Dockerfile
that changes the filesystem (RUN, COPY, ADD) creates one. Two things
follow from that:
- Cache invalidation is positional. When a layer changes, every layer after it is rebuilt. Order instructions from least- to most-frequently changing.
- Deleting a file in a later layer doesn't shrink the image — the file is
still in the earlier layer. Clean up within the same
RUN.
Hands-on¶
Order for cache hits¶
Copy dependency manifests and install before copying your source, so a code change doesn't bust the dependency layer:
# Bad: any source change reinstalls everything
COPY . .
RUN pip install -r requirements.txt
# Good: deps layer is cached until requirements.txt changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
Multi-stage builds¶
Build in a fat image, ship only the artifact:
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/app /app
USER 65532:65532
ENTRYPOINT ["/app"]
The final image has no compiler, no shell, no package manager — often 10–20 MB instead of 800 MB, and a much smaller attack surface.
Clean up in the same layer¶
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
Splitting the rm into its own RUN would leave the apt cache in the previous
layer.
Run as non-root¶
RUN useradd --system --uid 10001 app
USER 10001
Or use a base image that already does it (nginxinc/nginx-unprivileged,
distroless nonroot). A container that runs as root is one container-escape
away from host root.
.dockerignore¶
.git
node_modules
**/__pycache__
*.log
.env
This keeps COPY . . from shipping secrets, bloating the context, and busting
the cache on irrelevant changes. The build context is uploaded to the daemon in
full — a stray node_modules can add hundreds of megabytes and seconds.
Pin things¶
FROM python:3.12.7-slim-bookworm
Not python:3.12, and definitely not python:latest. A moving tag makes
builds non-reproducible and lets an upstream change break you silently. For
supply-chain-sensitive builds, pin by digest (python@sha256:...).
Verification and troubleshooting¶
# See the layers and their sizes.
docker history --no-trunc your-image:tag
# Build with BuildKit and inspect cache usage.
DOCKER_BUILDKIT=1 docker build --progress=plain -t your-image:tag .
# What's actually in the final image.
docker run --rm your-image:tag sh -c 'du -sh /* 2>/dev/null | sort -h'
Common problems:
- Every build reinstalls dependencies — you
COPY . .before the install step, or a file the manifest doesn't need (a log, a build timestamp) is in the context and changing. - Image is huge despite a cleanup
RUN— the cleanup is a separate instruction from the install. Combine them with&&. permission deniedafter addingUSER— the process needs to write somewhere it doesn't own. Create andchownthe directory in aRUNbefore theUSERline, or mount a volume.- Scanner flags dozens of CVEs — you're on a full
debian/ubuntubase. Move to-slim,alpine, or distroless; most of the findings are in packages you don't use. COPYfails in CI but works locally — the file is.dockerignored or.gitignored and your CI checks out a clean tree.
Related tools and reading¶
- On-site: Kubernetes Manifest Generator, .htpasswd Generator.
- Related posts: Multi-stage builds, explained, Shrink a Docker image.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Docker & Kubernetes
Kustomize Basics
Kustomize customizes plain YAML with overlays instead of templating. Here's bases vs overlays, the patch types, generators, and the gotchas around name suffixes.
September 13, 2026EKS vs Self-Managed Kubernetes
EKS runs the control plane for you; you still own nodes, networking, and add-ons. Here's the division of responsibility, the node options, and when self-managed is worth it.
September 11, 2026The AWS Load Balancer Controller
On EKS, the AWS Load Balancer Controller turns Ingress objects into ALBs and LoadBalancer Services into NLBs. Here's target types, ALB sharing, and the IAM/subnet-tag gotchas.
September 7, 2026