What Are Dockerfile Best Practices?
Learn Dockerfile best practices: layer caching, multi-stage builds, pinned base images, and non-root users — with a DevOps interview answer.
Expected Interview Answer
Dockerfile best practices center on minimizing image size and build time while maximizing cache reuse and security: use a small pinned base image, order instructions from least to most frequently changing, combine RUN commands to reduce layers, use multi-stage builds to drop build-only tooling, and run as a non-root user.
Ordering matters because Docker caches each layer keyed on the instruction and its inputs; placing dependency installation before source-code copy means a code-only change does not invalidate the expensive dependency layer. Multi-stage builds let a heavy compiler or SDK stage produce artifacts that get copied into a slim final stage, so the shipped image never carries build tooling. Pinning base image tags (e.g. node:20.11-alpine instead of node:latest) makes builds reproducible and avoids surprise breakage from upstream updates. A .dockerignore file keeps unnecessary files such as .git and node_modules out of the build context, speeding up builds and preventing secrets from leaking into layers, and switching to a non-root USER before CMD reduces the blast radius if the container is compromised.
- Smaller images that pull and deploy faster
- Higher build-cache hit rate for quicker CI builds
- Reduced attack surface by excluding build tooling and secrets
- Reproducible builds across every environment
AI Mentor Explanation
A coach preparing a touring squad packs the kit bag in a fixed order: bats and pads that rarely change go in first, personalized items like today’s batting gloves go in last, so repacking after one small swap does not mean unpacking the whole bag. The team also leaves the heavy training sled and cones behind at the academy once the squad is match-fit, carrying only what is needed on tour, just as a multi-stage build discards compilers from the final image. A senior player is designated to handle risky tasks like the toss coin, not the least experienced net bowler, mirroring running a container as a limited, non-root user. Packing the same fixed, minimal kit for every match keeps the squad fast to mobilize and safe from carrying unnecessary risk.
Step-by-Step Explanation
Step 1
Pin a minimal, specific base image
Use a fixed tag like node:20.11-alpine instead of a floating latest tag to keep builds reproducible and small.
Step 2
Order instructions by change frequency
Install dependencies before copying source code so the cache survives small code edits.
Step 3
Use multi-stage builds
Build with a full SDK stage, then copy only the compiled artifact into a slim final stage.
Step 4
Harden the final image
Add a .dockerignore, drop unnecessary layers, and switch to a non-root USER before CMD.
What Interviewer Expects
- Understanding of layer caching and instruction ordering
- Knowledge of multi-stage builds to shrink final image size
- Awareness of pinning base image tags for reproducibility
- Security awareness: non-root user, .dockerignore, minimal attack surface
Common Mistakes
- Using the :latest tag instead of a pinned version
- Copying source code before installing dependencies, breaking cache
- Shipping build tools and compilers in the final production image
- Running the container process as root by default
Best Answer (HR Friendly)
“We follow a few core habits when writing Dockerfiles: pin exact base image versions so builds are predictable, order steps so rarely-changing dependency installs come before frequently-changing source code, and use multi-stage builds so the final image ships without any compilers or dev tools. We also make sure the container runs as a non-root user, which keeps things both faster to build and more secure.”
Code Example
FROM node:20.11-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20.11-alpine AS runtime
WORKDIR /app
RUN addgroup -S appgrp && adduser -S appuser -G appgrp
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]Follow-up Questions
- Why does instruction order affect Docker build cache?
- How does a multi-stage build reduce final image size?
- Why is running a container as root risky?
- What does .dockerignore accomplish and why does it matter?
MCQ Practice
1. Why should dependency installation come before copying source code in a Dockerfile?
Placing rarely-changing steps earlier lets Docker reuse cached layers when only source code changes.
2. What is the main purpose of a multi-stage Dockerfile build?
Multi-stage builds compile in one stage and copy only the needed artifacts into a slim final stage, shrinking size and attack surface.
3. Why is pinning a specific base image tag preferred over using :latest?
A pinned tag guarantees the same base image contents every build, avoiding surprise breakage from upstream updates.
Flash Cards
Why order Dockerfile instructions by change frequency? — To maximize build-cache reuse — rarely-changing steps go first.
What does a multi-stage build achieve? — A slim final image without build-only compilers or tooling.
Why pin a base image tag? — For reproducible, predictable builds instead of a floating :latest tag.
Why run as a non-root USER? — To reduce the security blast radius if the container is compromised.