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

  1. 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 auch set -ex voranstellen, wodurch die Fehlersuche einfacher wird.

  2. 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 Umgebungsvariable UV_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ür uv sync deklarieren.

  3. 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, dass COPY 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.

  4. 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-User app und die Gruppe app 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.