Zum Hauptinhalt springen
Python-Tools2026-11-01 · 11 Min. Lesezeit

Docker für Python im Jahr 2026: Der moderne UV + Multi-Stage Build Guide

Öffnen Sie ein beliebiges fünf Jahre altes „Dockerize your Python app“-Tutorial und Sie werden die gleichen vier Zeilen finden: „FROM python:3.11-slim“, „COPY require.txt“, „RUN pip install -r require.txt“, „COPY“. .`. Es funktioniert. Es liefert außerdem ein Image mit mehr als 200 MB, baut sich langsam neu auf, da jede Abhängigkeitsinstallation ein vollständiger Cache-Fehler ist, sobald sich eine Zeile Anwendungscode ändert, und zieht stillschweigend einen C-Compiler und Header-Dateien hinein, die Ihr laufender Container nach Abschluss des Builds nie wieder berühren wird.

Nichts davon ist mehr notwendig. Zwischen uv (dem Rust-basierten Paketmanager und Installer von Astral), BuildKit-Cache-Mounts und distroless-Basisimages ist der 2026-Standard für einen Produktions-Python-Container ein mehrstufiger Build, der nur einen Python-Interpreter, Ihre virtuelle Umgebung und Ihren Code liefert, sonst nichts. Dieser Leitfaden führt durch das alte Muster, das neue Muster und eine Docker-Datei, die Sie heute in ein echtes Projekt kopieren können.

Das alte Muster und warum es teuer ist

DOCKERFILE
FROM python:3.13-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "app.py"]

Dieses Muster hat drei Kosten zur Folge, die sich mit zunehmendem Projektwachstum verstärken. Erstens wird „python:3.13-slim“ bereits mit etwa 150 bis 200 MB komprimiert ausgeliefert, bevor Sie eine einzelne Abhängigkeit hinzufügen, da „slim“ immer noch ein vollständiges Debian-Benutzerland, apt und die C-Erweiterungen der Python-Standardbibliothek enthält. Zweitens wird jede „RUN pip install“-Ebene vom Docker-Inhalts-Hash der vorherigen Ebenen zwischengespeichert, sodass eine Zeile in „app.py“ vor „COPY“ geändert wird. .` macht den Upstream nicht ungültig, aber eine Änderung an „requirements.txt“ macht den gesamten Installationsschritt ungültig und lädt jedes Rad erneut herunter. Drittens: Wenn eine Abhängigkeit einen nativen Build benötigt (ein fehlendes Manylinux-Rad, eine ungewöhnliche Paketversion), zieht Pip stillschweigend Build-Tools ein, die dann für immer im endgültigen Image verbleiben, da es nur eine Phase gibt, in der es weggeworfen werden kann.

Das neue Muster: eine UV-Builder-Stufe plus eine verteilungslose Endstufe

Der Fix ist ein mehrstufiger Build: eine Stufe mit einem Compiler, UV und Ihrer Sperrdatei und eine zweite, viel kleinere Stufe, die nur die fertige virtuelle Umgebung und Ihren Quellbaum kopiert. In der letzten Phase werden weder Pip, UV, Apt noch ein C-Compiler angezeigt.

DOCKERFILE
FROM 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“ hat keine Shell, keinen Paketmanager und kein Pip, was genau das ist, was Sie in der Produktion wollen: Im Container ist nichts mehr übrig, was ein Angreifer, der die Codeausführung erhält, nutzen könnte, um weitere Tools einzuschleusen. Das endgültige Image besteht aus den Site-Paketen Ihrer virtuellen Umgebung und Ihrem Code, der für einen typischen FastAPI-Dienst deutlich unter 50 MB statt mehr als 200 MB landet.

BuildKit-Cache-Mounts: Laden Sie niemals dasselbe Rad zweimal erneut herunter

Die Zeile „--mount=type=cache,target=/root/.cache/uv“ erledigt hier die eigentliche Arbeit. Ohne sie lädt jeder „Docker-Build“ auf einem neuen Runner jedes Rad erneut von PyPI herunter, da der Layer-Cache nur dann überlebt, wenn die vorherigen Layer, einschließlich „COPY uv.lock“, byteidentisch sind. Ein BuildKit-Cache-Mount ist ein persistentes Volume außerhalb des Layer-Diagramms: Es bleibt über Builds auf demselben Host oder, mit einem Registry-Cache-Backend, über CI-Runner hinweg bestehen, selbst wenn sich die Sperrdatei ändert. Kombinieren Sie es mit „uv sync --frozen“, das sich weigert, die Sperrdatei zu berühren und lautstark fehlschlägt, wenn „pyproject.toml“ und „uv.lock“ nicht synchron sind, anstatt stillschweigend einen anderen Abhängigkeitsbaum aufzulösen als den, gegen den Ihre Tests ausgeführt wurden.

Die Aufteilung des „uv sync --no-install-project“-Schritts vom letzten „uv sync“ ist die andere Hälfte des Tricks: Der erste Aufruf installiert nur Abhängigkeiten von Drittanbietern (im Cache, solange sich „uv.lock“ nicht ändert), und der zweite Aufruf wird nach „COPY“ ausgeführt. /app`, installiert Ihr eigenes Projekt, ohne Pakete von Drittanbietern erneut zu bearbeiten. Wenn Sie eine Zeile Anwendungscode ändern, bleibt die Abhängigkeitsschicht im Cache.

Native Abhängigkeiten: Numpy, Pillow und das Manylinux-Problem

Pakete wie numpy und Pillow liefern vorgefertigte viele Linux-Räder für die gängigen Plattformen, sodass „uv sync“ unter „linux/amd64“ oder „linux/arm64“ normalerweise nur einen Rädchen herunterlädt und nie einen Compiler berührt. Der Fehlermodus tritt auf, wenn Sie sich auf einer ungewöhnlichen Architektur befinden, eine sehr neue Version pinnen, bevor die Räder aufholen, oder auf ein Paket angewiesen sind, das nur eine Quelldistribution liefert. In diesem Fall benötigt die Builder-Phase Build-Tools, die die letzte Phase niemals sehen darf:

DOCKERFILE
FROM 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-Header) lebt nur in der Builder-Phase. Die distroless-Endstufe kopiert die bereits kompilierte „.venv“, niemals den Compiler, der sie erstellt hat. Wenn Sie das Erstellen und Versenden eines echten Container-Dienstes durchgängig üben möchten, führt der FastAPI-Kurs genau diese Art von API durch, vom ersten Endpunkt bis zu einer produktionsförmigen Bereitstellung.

UV-verwaltetes Python: Stoppen, je nachdem, welcher Interpreter das Basis-Image geliefert hat

„python:3.13-slim“ bietet Ihnen die Patch-Version, die Debian in dieser Woche ausliefern wollte. uv kann stattdessen den Interpreter selbst verwalten: „uv python install 3.13.1“ lädt einen bestimmten, reproduzierbaren CPython-Build aus dem python-build-standalone-Projekt herunter und pinnen ihn in „.python-version“, sodass Ihre Builder-Stufe und Ihr Laptop genau denselben Interpreter bis zur Patch-Version ausführen. In Kombination mit „UV_PYTHON_DOWNLOADS=never“ in der Docker-Datei, sodass eine falsch angeheftete Version den Build lautstark fehlschlägt, anstatt stillschweigend etwas anderes herunterzuladen, schließt dies die Lücke „Funktioniert auf meinem Computer, schlägt auf dem Runner fehl“, die ein schwebendes Basis-Image-Tag offen lässt.

Debuggen eines Containers ohne Shell

Der einzige wirkliche Preis von Distroless: Es gibt keine Shell, daher funktioniert „docker exec -it container sh“ nicht. Erstellen Sie für das lokale Debuggen dieselbe Docker-Datei mit einem Ziel, das in der Builder-Phase stoppt („docker build --target=builder -t myapp:debug .“), das immer noch über das vollständige Userland von „python:3.13-slim“ und eine Shell verfügt. Halten Sie dieses Debug-Ziel aus Ihrer Bereitstellungspipeline fern. Es existiert für Ihr Terminal, nicht für die Produktion.

Die Ein-Datei-Vorlage

DOCKERFILE
FROM 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"]

Kopieren Sie dies, ändern Sie den Modulnamen „ENTRYPOINT“, damit er zu Ihrem Projektlayout passt, und Sie haben einen Build, der kleiner, bei wiederholten Builds schneller und schwerer angreifbar ist als das „pip install -r require.txt“-Muster, das in den meisten Tutorials immer noch gelehrt wird. Sind Sie bereit, das Backend zu üben, das in diesem Container enthalten ist? Beginnen Sie mit FastAPI.

FAQ

Muss BuildKit aktiviert sein, damit die Cache-Bereitstellung funktioniert?

Ja, „--mount=type=cache“ ist eine reine BuildKit-Funktion. Es war der Standard-Builder in Docker Desktop und neueren Docker Engine-Versionen, aber auf älteren CI-Runnern müssen Sie möglicherweise „DOCKER_BUILDKIT=1“ explizit festlegen oder „docker buildx build“ anstelle von einfachem „docker build“.

Warum nicht einfach python:3.13-alpine für ein kleineres Bild verwenden?

Alpines musl libc durchbricht die Manylinux-Wheel-Annahme, auf der Numpy, Pillow und die meisten kompilierten Pakete basieren, sodass Sie vorgefertigte Wheels verlieren und entweder aus dem Quellcode kompilieren oder auf subtile Laufzeitfehler stoßen. distroless-glibc vermeidet diese Falle.

Ersetzt UV Pip und Poetry vollständig?

Für die meisten neuen Projekte ja. uv lässt sich schneller auflösen und installieren, liest die gleiche „pyproject.toml“ und generiert eine Sperrdatei, aber wenn ein Team bereits auf Poetry-Plugins angewiesen ist oder ein Workflow noch nicht von uv abgedeckt wird, besteht keine Dringlichkeit, mitten im Projekt zu migrieren. Probieren Sie UV zunächst bei einem neuen Dienst aus.

Was passiert, wenn mein Basis-Image einen Sicherheitspatch benötigt, nachdem ich das Image bereits erstellt habe?

Wiederaufbau. Hängen Sie in der Builder-Phase einen Digest, nicht nur einen Tag, für „python:3.13-slim“ an und erstellen Sie ihn nach einem Zeitplan neu. Wöchentlich ist für die meisten Teams sinnvoll, sodass ein gepatchtes Basis-Image in einen neuen Build einfließt, anstatt monatelang veraltet zu bleiben.

Kann ich dieses Muster für ein Django-Projekt anstelle von FastAPI verwenden?

Ja, der Docker-Datei ist es egal, welches Framework Sie ausführen; nur der letzte „ENTRYPOINT“ ändert sich (zum Beispiel „gunicorn myproject.wsgi:application“). Der FastAPI-Kurs ist eine schnelle Möglichkeit, das Muster durchgängig zu üben, wenn Sie einen neuen Dienst starten.

Ist distroless produktionsbereit oder ist es noch experimentell?

Es wird häufig in der Produktion verwendet; Google pflegt es und viele Teams liefern es als Standard-Python-Basis aus. Der Kompromiss, den man eingehen muss, ist das Fehlen einer Shell, was eine bewusste Sicherheitsentscheidung und kein Fehler ist.

Bereit, es auszuprobieren?

15 Unterricht kostenlos nach kostenloser Anmeldung – keine Karte. Testen Sie die Plattform und entscheiden Sie dann.

Kostenlos starten →

Or unlock every lesson

Pro — $12/mo, unlimited AI, cancel any time. Refund within 14 days worldwide.

See Pro plans →

Get one Python lesson + one career idea every Friday

No spam, no "buy our course now". Three bullets, every Friday. Unsubscribe with one click.