DevOps Tutorial · Containers and Kubernetes · Lesson 17 of 25
Writing Good Dockerfiles
Small, fast, secure images: layer caching, multi-stage builds, non-root users, .dockerignore and pinned base images.
A production Dockerfile
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.13-slim
WORKDIR /app
COPY --from=build /install /usr/local
COPY src/ ./src/
RUN useradd --uid 10001 --no-create-home app
USER app
EXPOSE 8080
CMD ["gunicorn", "-b", "0.0.0.0:8080", "src.app:app"]
The rules behind it
- Order layers by how often they change. Dependencies first, source code last, so a code change doesn't reinstall every package. Each instruction is a cached layer; a change invalidates it and every layer after it.
- Multi-stage builds: compilers and build tools stay in the build stage; the final image only gets the results.
- Small base images (
-slim, distroless, Alpine where compatible) mean fewer vulnerabilities. - Run as a non-root user (
USER). - Exec-form CMD (
["gunicorn", ...]) so the app is PID 1 and receives SIGTERM. - Never copy secrets into an image; deleting them in a later layer doesn't remove them from earlier
layers. Use BuildKit secret mounts (
RUN --mount=type=secret,...). .dockerignorekeeps.git,node_modules, local.envfiles and test data out of the build context.- Pin the base image to a version, or a digest for full reproducibility, not
latest.
def build(instructions, changed):
rebuilt, invalid = [], False
for name, inputs in instructions:
if invalid or inputs & changed:
invalid = True
rebuilt.append(name)
return rebuilt
good = [("FROM base", set()), ("COPY requirements.txt", {"requirements.txt"}),
("RUN pip install", set()), ("COPY src/", {"src/app.py"})]
bad = [("FROM base", set()), ("COPY . .", {"requirements.txt", "src/app.py"}), ("RUN pip install", set())]
print("good order, app.py changed -> rebuild:", build(good, {"src/app.py"}))
print("bad order, app.py changed -> rebuild:", build(bad, {"src/app.py"}))
Output
good order, app.py changed -> rebuild: ['COPY src/'] bad order, app.py changed -> rebuild: ['COPY . .', 'RUN pip install']
Test yourself
Why copy requirements.txt and install dependencies before copying the source code?
So code changes don't invalidate the cached dependency layerDocker requires that orderIt makes the image run as non-rootIt removes secrets from the image
Show answer
Answer: So code changes don't invalidate the cached dependency layer
A layer is rebuilt when its inputs change, and so is every layer after it.