· devops · 3 min read
Bonnes pratiques avec uv dans un multi-stage build Docker
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
- Multi-stage build : build optimisé, image finale légère, sans
uv
- Python managé par uv : la version de python utilisée dans l’image finale est garantie par uv.
- Utilisateur non-root : conforme aux bonnes pratiques Docker.
Inconvénients
- 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.