A production Dockerfile for FastAPI: the two mistakes beginners always make
Chapter 6 of a local-Kubernetes series. Not another "your first Dockerfile" — it leads with the two things beginners get wrong that have real consequences: running as root (a security liability) and cache-busting layer order (a productivity tax).
Key takeaways
-
Layer order is caching. Put what rarely changes first.
COPY requirements.txt+pip installbeforeCOPY ./app, so a one-line code edit doesn't reinstall every dependency. The reverse (COPY . .before install) is the most common anti-pattern. -
Run as a non-root user.
adduser --disabled-password --uid 10001 appuserthenUSER appuser; reinforce withsecurityContext.runAsNonRoot: truein the Pod. Least privilege, smaller attack surface. -
Multi-stage build: install deps into a venv in a
buildstage,COPY --from=build /opt/venv /opt/venvinto a clean runtime image — no compilers or pip caches in the final layer. -
Base image trade-offs:
slim(glibc, best wheel compatibility — the sane default),alpine(musl → pip often compiles from source, slow/fragile, DNS quirks),distroless(no shell/package manager, great secure runtime, exec-form only, pair with multi-stage). -
exec form of CMD is not optional:
["fastapi","run",...]makes the app PID 1 and receivesSIGTERMdirectly. Shell form wraps it in/bin/sh, SIGTERM never reaches the app, and graceful shutdown breaks. -
FastAPI specifics:
fastapi run(prod-tuned uvicorn) not bare uvicorn; never--reloadin the cluster image;PYTHONUNBUFFERED=1or logs never reachkubectl logs; add--proxy-headersbehind Ingress. -
HEALTHCHECK without curl:
slim/distroless have nocurl, so probe with Python'surllib. -
The finale (and the trap):
docker builddoes NOT make the image visible to k3d — its nodes run isolated containerd. Deliver it viak3d image import myapp:dev -c dev(+imagePullPolicy: IfNotPresent) or push to the built-in registry.
Top comments (0)