· devops · 3 min read

Bonnes pratiques avec uv dans un multi-stage build Docker

par Thibault Djaballah

Résolution d’un problème fréquent avec uv et Docker en multi-stage build et exécution en non-root. Une approche simple, reproductible et sécurisée pour vos images Python.

Contexte

L’outil uv simplifie la gestion des environnements Python et leur intégration dans des workflows reproductibles.
Mais ce dernier n’est pas des plus faciles à utiliser dans un multi stage build Docker, comme documenté dans l’issue #7758, surtout si on veut partir d’une image de base sans Python et un nonroot user dans l’image finale.

Dans cet article, nous allons voir comment construire une image Docker :

  • en utilisant un multi-stage build à partir d’une image de base wolfi
  • en optimisant les layers pour garder l’image finale légère et rapide à build
  • en installant Python via uv
  • en supprimant toutes les bibliothèques du python managé et installé par uv, qui peuvent se révéler vulnérables à l’issue de scans de l’image
  • en évitant d’avoir uv dans l’image finale
  • et en exécutant l’application avec un utilisateur non-root car on aime les bonnes pratiques de sécurité, n’est-ce pas ?

Structure du projet de démonstration

Le dépôt de test contient les éléments suivants :

test-docker/
├─ app/
│  └─ main.py
├─ pyproject.toml
├─ uv.lock
└─ Dockerfile

pyproject.toml:

[project]
name = "test-docker"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "numpy>=2.3.2",
]

app/main.py :

import numpy as np

def main():
    arr = np.array([1, 2, 3, 4, 5])
    print("Array:", arr)
    print("Sum:", np.sum(arr))

if __name__ == "__main__":
    main()

Dockerfile:

# ---- Image de base wolfi ----
FROM cgr.dev/chainguard/wolfi-base AS base

# Dépendances runtime nécessaires (pour NumPy par exemple)
RUN apk add --no-cache libstdc++ libgcc

# ---- Builder: installation via uv ----
FROM base AS builder

# Installer uv (binaire statique)
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app

# https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
ENV UV_COMPILE_BYTECODE=1 \
# https://docs.astral.sh/uv/guides/integration/docker/#caching
    UV_LINK_MODE=copy \
    UV_PYTHON_INSTALL_DIR=/opt/python

# https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-install-project --no-editable --no-dev \
# Supprimer toutes les bibliothèques du python managé et installé par uv
&& uv pip uninstall --break-system-packages \
      --python "$(uv python find --managed-python --system)" \
      pip setuptools wheel

# https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
COPY app ./app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-editable --no-dev

# ---- Image finale ----
FROM base AS final
WORKDIR /app

# Copier Python managé et app (venv inclus), en non-root, et mode lecture seule
COPY --from=builder --chown=nonroot:nonroot --chmod=755 /opt/python /opt/python
COPY --from=builder --chown=nonroot:nonroot --chmod=755 /app /app

# Activer le venv généré par uv
ENV PATH="/app/.venv/bin:${PATH}" \
    PYTHONPATH="/app"

USER nonroot
EXPOSE 8080

CMD ["python", "-m", "app.main"]

Build et run

Construire et lancer l’image :

docker build -t uv-demo .
docker run --rm uv-demo

Sortie attendue :

Array: [1 2 3 4 5]
Sum: 15

Bénéfices

  1. Multi-stage build : build optimisé, image finale légère, sans uv
  2. Python managé par uv : la version de python utilisée dans l’image finale est garantie par uv.
  3. Utilisateur non-root : conforme aux bonnes pratiques Docker.

Inconvénients

  1. Il ne faut pas oublier d’installer les dépendances au runtime nécessaires comme libstdc++ ou libgcc pour Numpy ou cmake pour OpenCV !

Conclusion

En suivant cette approche, vous obtenez une image sécurisée, reproductible et allégée, adaptée à un usage en production tout en respectant les bonnes pratiques de conteneurisation Python.

Back to Blog