Python + Docker Best Practices
stuff to do when deploying a python project using docker
-
Follow 000-docker
-
PYTHONDONTWRITEBYTECODE=1or-Bat runtime, but compile at build.-Bdoes not skip compiling, only caching - python compiles every module on every import and throws it away. Read-only rootfs means there is nowhere to write a.pycanyway, so compile at build or pay it on every start forever.- Compile at build when the rootfs is read-only, the process restarts often (rolling deploys, HPA, scale-to-zero), the dependency tree is large (fastapi + sqlalchemy + pydantic is thousands of modules), or it is a CLI invoked repeatedly.
- Skip when image size is the constraint (
.pycroughly doubles the venv source footprint), it is a batch job that starts once and runs for hours, it is a dev image with bind-mounted source (stale.pycis a confusing bug), or build time is tight and nobody is complaining about startup. python -m compileall -q -j 0 --invalidation-mode unchecked-hash /app/.venv, orUV_COMPILE_BYTECODE=1.unchecked-hashis the one that matters. Default is timestamp based, so identical source gives different bytes and the image is not reproducible.-j 0uses all cores.compileallexits non-zero if any file fails. Some packages still ship broken py2 files. Exclude with-x, not|| true, or you lose the signal.-Oreads.opt-1.pyc. Compile the levels you run or skip-O.
-
export PYTHONUNBUFFERED=1, don’t buffer stdout or stderr. -
Prefer
uvfor dev + lockfile generation. Install into a venv, copy only the venv to the final stage. Skip therequirements.txtround-trip, it loses the hashes. Don’t copyuvto the final image.Split the sync in two so deps land in a layer app code cannot invalidate.
UV_LINK_MODE=copystops hardlink warnings. -
PYTHONFAULTHANDLER=1, always on crash reporting. -
python -m module -
python -m pip --no-cache-dir --index-url '<url>' -r requirements.txt, use pull-through index for enterprise, or set this up if you don’t currently have it.PIP_DISABLE_PIP_VERSION_CHECK=1and--only-binary=:all:so a missing wheel fails loudly instead of building from source with a toolchain you never meant to ship. -
--require-hashesby default.--no-hashesgives up supply chain verification. If the pull-through index rewrites them, write down that that is why. -
Handle SIGTERM. uvicorn and gunicorn do it themselves. Spawn subprocesses or threads and you need a handler, or the platform SIGKILLs you and in-flight work is lost.
-
Distroless: base image plus a standalone interpreter, not
gcr.io/distroless/python3which pins Google’s Python version.distroless/base-debian12has glibc, libssl, ca-certificates, tzdata. Copy in a python-build-standalone interpreter and the venv.base-nosslbreaksssl. Anything doing HTTPS or Postgres over TLS needs SSL.ccnotbaseif a wheel needs libstdc++. scipy and most ML stacks do.staticwill not work, no glibc.ENV PATH="/app/.venv/bin:$PATH", don’t try to activate anything.