2026 Dockerfile Best Practices — Five Things 2020 Got Wrong

Five Dockerfile patterns that were *recommended in 2020* now *work against you in 2026*. What changed.

John Baek
John Baek
Founder, CollabOps
2026 Dockerfile Best Practices — Five Things 2020 Got Wrong

Five 2020-recommended Dockerfile patterns invert in 2026. The reasons: BuildKit, distroless, multi-arch builds, and supply-chain security tooling have all matured.

Change 1 — combine RUN commands is no longer the answer

2020 recommendation:

RUN apt-get update && apt-get install -y curl jq \
    && rm -rf /var/lib/apt/lists/*

Reasoning — each RUN creates a layer; combine to reduce layer count.

2026 — BuildKit supports cache mounts. Layer count itself is less important. Cache efficiency matters more.

RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt/lists \
    apt-get update && apt-get install -y curl jq

With cache mounts, each RUN has its own cache and rebuilds are faster than the combined version.

Change 2 — alpine is no longer the default

2020 — FROM alpine as the lightweight default.

2026 — Alpine's musl libc causes compatibility issues with Python / Node / glibc-based libraries. Debugging time consistently exceeds the size savings.

Recommended — distroless (Google) or chainguard minimal images. glibc compatible and smaller than Alpine.

# 2020
FROM alpine:3.18

# 2026
FROM gcr.io/distroless/nodejs22-debian12
# or
FROM cgr.dev/chainguard/node

Change 3 — never use latest is partly wrong

2020 — FROM node:latest is forbidden. Use a fixed version.

2026 — latest is still wrong, but pinning by digest has become the standard.

# 2020-recommended (insufficient)
FROM node:20-alpine

# 2026-recommended (digest-pinned)
FROM node:22.11.0-slim@sha256:af16...

Why — even the same tag (node:22-slim) can point to different images over time. Defending against supply-chain attacks (SLSA, etc.) requires pinning to the digest.

Change 4 — non-root user is not enough

2020 — switch to non-root via USER appuser.

2026 — non-root alone is insufficient. All five together:

USER appuser
WORKDIR /app
COPY --chown=appuser:appuser . /app

# 2026 additions
COPY --chmod=0444 ./config.json /app/config.json   # read-only files
LABEL org.opencontainers.image.base.digest=sha256:...
LABEL org.opencontainers.image.source=https://github.com/...

The added LABELs are for supply-chain traceability. Tools like docker scout / trivy / grype read these labels for provenance tracking.

Change 5 — single-stage is no longer justifiable

2020 — multi-stage builds are for complex cases. Simple apps go single-stage.

2026 — every production image is multi-stage. Build tools (gcc, npm, etc.) in the runtime image widen the attack surface. The complexity cost of multi-stage is dwarfed by the attack-surface reduction.

# Build stage
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

# Runtime stage
FROM gcr.io/distroless/nodejs22-debian12
WORKDIR /app
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
CMD ["dist/index.js"]

The runtime image contains no npm at all. The attack surface drops by dozens of build tools.

What the five share

The five share one thing — priorities have shifted. In 2020, image size and simplicity led. In 2026, supply-chain security, build-cache efficiency, and compatibility lead.

Dockerfile best practices change every six years. 2020's "best" can be 2026's "worst".

Do Dockerfiles written to the 2020 guide need fixing now?

A Dockerfile written to the 2020 guide that hasn't adopted three or more of the five changes above is likely to draw recommendations in your next security review. If your Dockerfiles haven't been touched since then, fixing them before the review forces you to is the cheaper path.

Tags#docker#dockerfile#container#infrastructure#devops