Docker voor Python in 2026: de moderne uv + Multi-Stage Build Guide
Open een vijf jaar oude "Dockerize your Python app" tutorial en je zult dezelfde vier regels vinden: FROM python:3.11-slim, COPY require.txt ., RUN pip install -r require.txt, COPY . .. Het werkt. Het levert ook een image van meer dan 200 MB, wordt langzaam opnieuw opgebouwd omdat elke afhankelijkheidsinstallatie een volledige cache is, mist het moment dat een regel applicatiecode verandert, en sleept stilletjes een C-compiler en headerbestanden binnen die uw actieve container nooit meer zal aanraken nadat de build is voltooid.
Niets van dat alles is meer nodig. Tussen uv (de op Rust gebaseerde pakketbeheerder en installatieprogramma van Astral), BuildKit-cache-mounts en distributieloze basisimages is de standaard voor een productie-Python-container in 2026 een meerfasige build die alleen een Python-interpreter, je virtuele omgeving en je code levert, en niets anders. Deze gids loopt door het oude patroon, het nieuwe patroon en een Docker-bestand dat u vandaag nog naar een echt project kunt kopiëren.
Het oude patroon, en waarom het duur is
DOCKERFILEFROM python:3.13-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "app.py"]
Dit patroon heeft drie kosten die toenemen naarmate een project groeit. Ten eerste wordt python:3.13-slim al geleverd met ongeveer 150 tot 200 MB gecomprimeerd voordat je ook maar een enkele afhankelijkheid toevoegt, omdat "slim" nog steeds een volledig Debian-gebruikersland, apt en de C-extensies van de Python-standaardbibliotheek bevat. Ten tweede wordt elke RUN pip install-laag in de cache opgeslagen door Docker's inhoudshash van de voorgaande lagen, dus verander één regel in app.py vóór COPY. . maakt upstream niets ongeldig, maar een wijziging in requirements.txt maakt de gehele installatiestap ongeldig en downloadt elk wiel opnieuw. Ten derde, als een afhankelijkheid een native build nodig heeft (een ontbrekend ManyLinux-wiel, een ongebruikelijke pakketversie), haalt pip stilletjes build-tools binnen die dan voor altijd in het uiteindelijke beeld blijven, omdat er maar één fase is om het weg te gooien.
Het nieuwe patroon: een uv-bouwerfase plus een distributieloze laatste fase
De oplossing bestaat uit een build in meerdere fasen: één fase met een compiler, uv en uw lockfile, en een tweede, veel kleinere fase die alleen de voltooide virtuele omgeving en uw bronstructuur kopieert. In de laatste fase zie je nooit pip, uv, apt of een C-compiler.
DOCKERFILEFROM python:3.13-slim AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project --no-dev COPY . /app RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev FROM gcr.io/distroless/python3-debian12 COPY --from=builder /app /app WORKDIR /app ENV PATH="/app/.venv/bin:$PATH" ENTRYPOINT ["/app/.venv/bin/python", "-m", "app"]
gcr.io/distroless/python3-debian12 heeft geen shell, geen pakketbeheerder en geen pip, en dat is precies wat je wilt in productie: er zit niets meer in de container dat een aanvaller die code-uitvoering krijgt, kan gebruiken om meer tools binnen te halen. Het uiteindelijke beeld bestaat uit de sitepakketten van uw virtuele omgeving plus uw code, die voor een typische FastAPI-service ruim onder de 50 MB uitkomt in plaats van ruim 200 MB.
BuildKit cache-mounts: download nooit hetzelfde wiel twee keer opnieuw
De regel --mount=type=cache,target=/root/.cache/uv doet hier het echte werk. Zonder dit downloadt elke docker build op een nieuwe runner elk wiel opnieuw van PyPI, omdat de cache van de laag alleen overleeft als de voorgaande lagen, inclusief COPY uv.lock, byte-identiek zijn. Een BuildKit-cachemount is een persistent volume buiten de laaggrafiek: het overleeft tussen builds op dezelfde host, of, met een registercache-backend, tussen CI-runners, zelfs als het lockfile verandert. Koppel het met uv sync --frozen, dat weigert het lockbestand aan te raken en luidruchtig faalt als pyproject.toml en uv.lock niet gesynchroniseerd zijn, in plaats van stilletjes een andere afhankelijkheidsboom op te lossen dan degene waar je tests tegenaan liepen.
Het splitsen van de stap uv sync --no-install-project van de laatste stap uv sync is de andere helft van de truc: de eerste aanroep installeert alleen afhankelijkheden van derden (gecacht zolang uv.lock niet verandert), en de tweede aanroep wordt uitgevoerd na COPY . /app, installeert uw eigen project zonder pakketten van derden opnieuw aan te raken. Wijzig één regel applicatiecode en de afhankelijkheidslaag blijft in de cache.
Native afhankelijkheden: numpy, kussen en het ManyLinux-probleem
Pakketten zoals numpy en Pillow leveren veel vooraf gebouwde Linux-wielen voor de gebruikelijke platforms, dus op linux/amd64 of linux/arm64 downloadt uv sync normaal gesproken alleen maar een wiel en raakt nooit een compiler aan. De foutmodus komt naar voren als je een ongebruikelijke architectuur gebruikt, een heel nieuwe release vastzet voordat de wielen de achterstand inlopen, of afhankelijk bent van een pakket dat alleen een brondistributie levert. In dat geval heeft de bouwerfase bouwtools nodig, de laatste fase mag nooit het volgende zien:
DOCKERFILEFROM python:3.13-slim AS builder RUN apt-get update && apt-get install -y --no-install-recommends build-essential \ && rm -rf /var/lib/apt/lists/* # uv + sync steps as in the stage above
build-essential (gcc, make, libc headers) leeft alleen in de builder-fase. De laatste fase van de distroless kopieert het reeds gecompileerde .venv, nooit de compiler die het heeft gebouwd. Als je wilt oefenen met het van begin tot eind bouwen en verzenden van een echte containerservice, loopt de [FastAPI-cursus] (/courses/fastapi) door precies dit soort API's, van het eerste eindpunt tot een productievormige implementatie.
uv-beheerde Python: stop met afhankelijk te zijn van welke tolk de basisimage ook heeft verzonden
python:3.13-slim geeft je de patchrelease die Debian die week besloot uit te brengen. uv kan in plaats daarvan de interpreter zelf beheren: uv python install 3.13.1 downloadt een specifieke, reproduceerbare CPython-build van het python-build-standalone project en pint deze in .python-version, zodat uw builder-fase en uw laptop exact dezelfde interpreter uitvoeren tot aan de patchversie. Gecombineerd met UV_PYTHON_DOWNLOADS=never in het Dockerbestand, zodat een verkeerd vastgezette versie de build luid mislukt in plaats van stilletjes iets anders te downloaden, dicht dit het gat "werkt op mijn machine, mislukt op de runner" dat een zwevende basisimage-tag openlaat.
Foutopsporing in een shell-loze container
De enige echte kosten van distroless: er is geen shell, dus docker exec -it container sh werkt niet. Voor lokaal debuggen bouwt u hetzelfde Docker-bestand met een doel dat stopt in de builder-fase (docker build --target=builder -t myapp:debug .), dat nog steeds het volledige gebruikersland van python:3.13-slim en een shell heeft. Houd dat foutopsporingsdoel buiten uw implementatiepijplijn; het bestaat voor uw terminal, niet voor productie.
De sjabloon met één bestand
DOCKERFILEFROM python:3.13-slim AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project --no-dev COPY . /app RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev FROM gcr.io/distroless/python3-debian12 COPY --from=builder /app /app WORKDIR /app ENV PATH="/app/.venv/bin:$PATH" ENTRYPOINT ["/app/.venv/bin/python", "-m", "app"]
Kopieer dit, verander de modulenaam ENTRYPOINT zodat deze overeenkomt met uw projectlay-out, en u heeft een build die kleiner is, sneller bij herhaalde builds en moeilijker aan te vallen dan het pip install -r vereisten.txt-patroon dat de meeste tutorials nog steeds leren. Klaar om de backend te oefenen die in deze container past? Begin met FastAPI.
Veelgestelde vragen
Moet BuildKit ingeschakeld zijn om de cache-mount te laten werken?
Ja, --mount=type=cache is alleen een functie van BuildKit. Het was de standaardbuilder in Docker Desktop en recente Docker Engine-releases, maar op oudere CI-runners moet je mogelijk DOCKER_BUILDKIT=1 expliciet instellen, of docker buildx build in plaats van gewone docker build.
Waarom gebruik je niet gewoon python:3.13-alpine voor een kleinere afbeelding?
Alpine's musl libc doorbreekt de veronderstelling van veel Linux-wielen waar numpy, Pillow en de meeste gecompileerde pakketten op vertrouwen, zodat je vooraf gebouwde wielen kwijtraakt en ofwel vanaf de broncode compileert, ofwel subtiele runtime-bugs tegenkomt. distroless-glibc vermijdt die valkuil.
Vervangt uv pip en poëzie volledig?
Voor de meeste nieuwe projecten wel. uv lost en installeert sneller, leest hetzelfde pyproject.toml en genereert een lockfile, maar als een team al afhankelijk is van Poetry-plug-ins of een workflow die uv nog niet dekt, is er geen urgentie om halverwege het project te migreren. Probeer uv eerst uit op een nieuwe dienst.
Wat moet ik doen als mijn basisimage een beveiligingspatch nodig heeft nadat ik de image al heb gebouwd?
Herbouwen. Zet een samenvatting vast, niet alleen een tag, voor python:3.13-slim in de bouwfase, en herbouw volgens een schema, wekelijks is redelijk voor de meeste teams, dus een gepatchte basisimage vloeit over in een nieuwe build in plaats van maandenlang oud te blijven.
Kan ik dit patroon gebruiken voor een Django-project in plaats van FastAPI?
Ja, het maakt de Dockerfile niet uit welk raamwerk je draait; alleen de laatste ENTRYPOINT verandert (bijvoorbeeld gunicorn myproject.wsgi:application). De FastAPI-cursus is een snelle manier om het patroon van begin tot eind te oefenen als u een nieuwe dienst start.
Is distributieloze productie klaar of is het nog experimenteel?
Het wordt veel gebruikt in de productie; Google onderhoudt het en veel teams verzenden het als hun standaard Python-basis. De wisselwerking om te accepteren dat je naar binnen gaat, is het ontbreken van een shell, wat een bewuste veiligheidskeuze is en geen bug.