Python Docker Container mit uv
¶
Wir unterteilen unseren Docker-Workflow in unterschiedliche Layer. Dies erlaubt
uns schneller neue Builds bereitzustellen. Dabei beginnen wir mit den Layern,
die sich am wenigsten ändern, damit wir die Artefakte so lange wie möglich
zwischenspeichern können. Dies ist auch der Grund, weswegen wir die
Installationen der Abhängigkeiten aus uv.lock
und Installation unserer
Anwendung strikt getrennthalten –
wahrscheinlich ändert sich unser Code schneller als der der Abhängigkeiten.
Siehe auch
Zunächst bauen wir einen initialen Build-Container, der verhindert, dass wir unsere Build-Tools ausliefern müssen.
Siehe auch
1# syntax=docker/dockerfile:1.9 2FROM ubuntu:noble AS build 3 4SHELL ["sh", "-exc"] 5 6RUN <<EOT 7apt update -qy 8apt install -qyy \ 9 -o APT::Install-Recommends=false \ 10 -o APT::Install-Suggests=false \ 11 build-essential \ 12 ca-certificates \ 13 python3-setuptools \ 14 python3.12-dev 15EOT
- Zeile 4:
Falls ihr Podman verwendet, solltet ihr den Docker-Kompatibilitätsmodus verwenden.
- Zeile 6:
Ggf. könnt ihr jedem
RUN
-Skript auchset -ex
voranstellen, wodurch die Fehlersuche einfacher wird.
Anschließend bauen wir eine virtuelle Python-Umgebung mit unserer Anwendung im Verzeichnis
/app
und kopieren diese dann in unseren Runtime-Container. Das hat u.a. den Vorteil, dass derselbe Basiscontainer für verschiedene Python-Versionen und virtuelle Umgebungen verwendet werden kann. Mit uv 0.4.4 ist dies sehr viel einfacher geworden dank der UmgebungsvariableUV_PROJECT_ENVIRONMENT
:1COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv 2 3ENV UV_LINK_MODE=copy \ 4 UV_COMPILE_BYTECODE=1 \ 5 UV_PYTHON_DOWNLOADS=never \ 6 UV_PYTHON=python3.12 \ 7 UV_PROJECT_ENVIRONMENT=/app
- Zeile 1:
Sicherheitsbewußte Organisationen sollten
uv
selbst überprüfen und packen.- Zeile 3:
Dies verhindert, dass uv sich beschwert, keine Hardlinks verwenden zu können.
- Zeile 4:
Die Python-Pakete werden Byte-kompiliert, damit die Startzeiten des Containers verkürzt werden.
- Zeile 6:
Python-Version auswählen.
- Zeile 7:
/app
als Ziel füruv sync
deklarieren.
Nun erstellen wir das
app
-Dockerfile:1COPY pyproject.toml /_lock/ 2COPY uv.lock /_lock/ 3 4RUN --mount=type=cache,target=/root/.cache <<EOT 5cd /_lock 6uv sync \ 7 --locked \ 8 --no-dev \ 9 --no-install-project 10EOT 11 12COPY . /src 13RUN --mount=type=cache,target=/root/.cache \ 14 cd /src && uv sync --locked --no-dev --no-editable
- Zeilen 1–2:
Die
lock
-Dateien werden in ein Verzeichnis verschoben, das nicht im Runtime-Container liegt. Der Schrägstrich am Ende sorgt dafür, dassCOPY
automatisch /_lock/ erstellt.- Zeile 4:
Der Build-Cache-Mount verhindert z.B., dass alle Wheels neu gebaut werden müssen, wenn der Layer mit euren Abhängigkeiten neu gebaut werden muss.
Siehe auch
- Zeilen 6–9:
Die Abhängigkeiten werden ohne die Anwendung selbst synchronisiert. Dieser Layer wird zwischengespeichert, bis sich die uv.lock-Datei oder
pyproject.toml
ändern.- Zeile 14:
myapp
wird aus/src
ohne jegliche Abhängigkeiten installiert.
Schließlich erstellen wir den Runtime-Container:
1FROM python:3.12-slim 2SHELL ["sh", "-exc"] 3 4ENV PATH=/app/bin:$PATH 5 6RUN <<EOT 7groupadd -r app 8useradd -r -d /app -g app -N app 9EOT 10 11ENTRYPOINT ["/docker-entrypoint.sh"] 12 13STOPSIGNAL SIGINT 14 15RUN <<EOT 16apt update -qy 17apt install -qyy \ 18 -o APT::Install-Recommends=false \ 19 -o APT::Install-Suggests=false \ 20 python3.12 \ 21 libpython3.12 \ 22 libpcre3 \ 23 libxml2 24 25apt clean 26rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 27EOT 28 29COPY docker-entrypoint.sh / 30COPY . /app/ 31 32COPY --from=build --chown=app:app /app /app 33 34USER app 35WORKDIR /app 36 37RUN <<EOT 38python -V 39python -Im site 40python -Ic 'import myapp' 41EOT
- Zeile 4:
Optional: Fügt die virtuelle Umgebung zum Suchpfad hinzu.
- Zeilen 6–9:
Führt die Anwendung als Service-User
app
aus.- Zeile 13:
Im Python Ökosystem ist es nicht unbedingt üblich, dass eure Anwendung auf ein
SIGTERM
reagiert.STOPSIGNAL SIGINT
ist eine einfache Möglichkeit, dies zu umgehen.- Zeilen 20–21:
Beachtet, dass sich die Abhängigkeiten zur Laufzeit von den Abhängigkeiten zur Build-Zeit unterscheiden. Zudem gibt es auch kein
uv
.- Zeilen 29–30:
Wenn eure Anwendung kein Python-Paket ist, das mit
uv sync
installiert wurde, müsst ihr eure Anwendung hier in den Container kopieren.- Zeile 32:
Dies kopiert das vorgefertigte Verzeichnis
/app
in den Runtime-Container und ändert die Berechtigungen auf den Service-Userapp
und die Gruppeapp
in einem Schritt.- Zeilen 37–41:
Optional: Üblicherweise nutze ich diese Introspektion für einen Smoke-Test, der sicherstellt, dass die Anwendung auch tatsächlich importiert werden kann.