L'utilisation d'images Docker/OCI minimalistes permet non seulement de limiter l'espace disque utilisé ainsi que le temps de chargement et d'exécution mais réduit également la surface d'attaque. En revanche ces images présentent des limites concernant les possibilités de déboguage (absence de commandes standards, gestionnaire de paquet, shell, parties de l'arborescence, etc.).
Utiliser l'image distroless Python fournie par Google
Google fournit un ensemble d'image distroless plutôt complet avec des variantes couvrant différents cas d'usage (Python, Java, etc.). Bien qu'étant appelées "distroless", ces images reposent en fait sur Debian tout en étant dégrossies au maximum.
Penchons-nous sur l'image Python
gcr.io/distroless/python3-debian12 (identique à
gcr.io/distroless/python3). Cette image contient la version
3.11 de Python (assez ancienne à l'heure actuelle) sans librairie
particulière ni la possibilité d'en compiler.
docker run --rm gcr.io/distroless/python3-debian12:latest -c 'import platform; print(platform.python_version())'
3.11.2
Pour ajouter les librairies nécessaires au bon fonctionnement de
l'applicatif, nous allons passer par la méthode de contruction d'image
multi-stage. Nous ferons appel dans un premier temps à l'image
python:3.11-slim alignée sur la même version que notre
image distroless afin de créer un environnement Python virtuel (venv)
compatible embarquant les librairies énumérées dans le
requirements.txt.
Une fois l'environnement virtuel créé, il suffit de le copier dans
l'image distroless et d'indiquer le chemin dans les variables
PYTHONPATH et PATH:
FROM python:3.11-slim AS builder
COPY requirements.txt /opt/app/
WORKDIR /opt/app
RUN set -xe; \
python3 -m venv /opt/app/venv; \
/opt/app/venv/bin/python3 -m pip install --no-cache-dir -r requirements.txt
gcr.io/distroless/python3-debian12
ENV PYTHONPATH=/opt/app/venv/lib/python3.11/site-packages
ENV PATH="/opt/app/venv/bin:$PATH"
COPY --from=builder /opt/app/venv /opt/app/venv
COPY sources/ /opt/app
WORKDIR /opt/app
ENTRYPOINT ["python3", "/opt/app/app.py"]
Cette méthode n'est pas optimale car elle implique la duplication de fichiers amenés avec le venv. D'autre part la version 3.11 de Python n'est plus trop d'actualité et les images distroless ne proposent pas d'autre version.
Pour utiliser une version Python de notre choix il faut utiliser
pyinstaller afin de construire un paquet exécutable
contenant l'intégralité de l'applicatif, l'interpréteur et les
librairies. Il est à noter que cet exécutable, bien que plus portable
qu'un venv, n'est pas compilé statiquement et dépendra toujours de
librairies externes comme Glibc qui devront être disponibles dans
l'image distroless.
FROM python:3.14.2-slim-bookworm AS builder
WORKDIR /opt/app
COPY sources/ requirements.txt /opt/app/
RUN set -xe; \
apt-get update; \
apt-get install --yes --no-install-suggests --no-install-recommends binutils
RUN pip3 install --no-cache-dir --break-system-packages -r requirements.txt
RUN pyinstaller --name app_package --strip --optimize 2 --collect-all adbc_driver_manager --collect-all adbc_driver_postgresql --onefile app.py --add-data "logging.yml:."
FROM gcr.io/distroless/python3-debian12
COPY --from=builder --chmod=755 /opt/app/dist/app_package /app_package
ENTRYPOINT ["./app_package"]
Il faut veiller à utiliser une version de Debian (builder) alignée sur la version Debian de l'image distroless (ici Debian 12 Bookworm) autrement il y aura des disparités sur des librairies système comme Glibc ce qui causera des erreurs du type:
[PYI-7:ERROR] Failed to load Python shared library '/tmp/_MEIS1hqW6/libpython3.14.so.1.0': /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.38' not found (required by /tmp/_MEIS1hqW6/libpython3.14.so.1.0)
Le paramètre --collect-all permet de forcer pyinstaller
à inclure les libraries qu'il ne détecte pas automatiquement. Le
paramètre --add-data permet d'ajouter un fichier au
paquet.
L'incovénient de cette méthode est qu'en utilisant l'image
gcr.io/distroless/python3-debian12 nos obtenons une image
finale comprenant l'interpréteur Python 3.11. Il faudrait donc veiller à
supprimer celui-ci avec une image intermédiaire supplémentaire pour en
copier le contenu en excluant les fichiers Python 3.11 dans une image
scratch. Pour déterminer l'emplacement des fichier relatif
à Python:
docker run --rm gcr.io/distroless/python3-debian12:latest -c 'import site; print(site.getsitepackages())'
['/usr/local/lib/python3.11/dist-packages', '/usr/lib/python3/dist-packages', '/usr/lib/python3.11/dist-packages']
Le Dockerfile incluant l'image
gcr.io/distroless/python3-debian12 sans Python 3.11:
FROM python:3.14.2-slim-bookworm AS builder
WORKDIR /opt/app
COPY sources/ requirements.txt /opt/app/
RUN set -xe; \
apt-get update; \
apt-get install --yes --no-install-suggests --no-install-recommends binutils
RUN pip3 install --no-cache-dir --break-system-packages -r requirements.txt
RUN pyinstaller --name app_package --strip --optimize 2 --collect-all adbc_driver_manager --collect-all adbc_driver_postgresql --onefile app.py --add-data "logging.yml:."
FROM scratch AS base
COPY --from=gcr.io/distroless/python3-debian12 --exclude=**/python3.11 --exclude=**/python3 / /
FROM base
COPY --from=builder --chmod=755 /opt/app/dist/app_package /app_package
ENTRYPOINT ["./app_package"]
Pour aller plus loin, il existe l'outil staticx qui
permet l'édition de lien statique sur un exécutable (comme le paquet
exécutable produit par pyinstaller) pour y inclure les
librairies système et rendre possible l'utilisation d'image distroless
plus minimaliste comme scratch. En revanche
staticx ne semble pas aboutir à coup sûr dès lors que
l'applicatif comprend certaines/trop de librairies Python.
Utiliser l'image scratch et le langage C
L'utilisation du langage C passe par une étape de compilation avec
édition de lien. Le soin est laissé au lecteur d'utiliser une image
Docker intermédiaire pour la compilation du projet C. On considère le
programme C suivant main.c:
#include <glib.h>
int main(void) {
g_print ("OK\n");
return 0;
}Pour le compiler (nécessite le paquet glib2-devel):
gcc $(pkgconf --cflags glib-2.0) main.c $(pkgconf --libs glib-2.0) -o test
L'exécutable produit est dynamiquement lié aux librairies suivantes:
ldd test
linux-vdso.so.1 (0x00007efcb3d04000)
libglib-2.0.so.0 => /lib64/libglib-2.0.so.0 (0x00007efcb3b8b000)
libc.so.6 => /lib64/libc.so.6 (0x00007efcb3997000)
libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007efcb38ea000)
/lib64/ld-linux-x86-64.so.2 (0x00007efcb3d06000)
En l'état cet exécutable n'est pas utilisable dans l'image
scratch qui est dépourvue de ces librairies. Pour les y
ajouter il faut au préalable les copier dans le dossier courant
(contexte Docker) puis utiliser la directive COPY dans le
Dockerfile:
FROM scratch
COPY libglib-2.0.so.0 /lib64/libglib-2.0.so.0
COPY ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2
COPY libc.so.6 /lib64/libc.so.6
COPY libpcre2-8.so.0 /lib64/libpcre2-8.so.0
COPY --chmod=755 test /bin/
ENTRYPOINT ["test"]
Allons plus loin dans les options de compilation pour une édition de
lien statique afin de rendre l'exécutable produit directement utilisable
dans scratch, la plus minimaliste des images Docker. Pour
compiler le programme (nécessite les paquets glibc-static
et glib2-static):
gcc -s -static $(pkgconf --cflags glib-2.0) main.c $(pkgconf --static --libs glib-2.0) -o test
L'option -s pour "strip" permet de réduire la taille de
l'exécutable. Après quelques warnings, on obtient un exécutable non
dynamique:
ldd test
n'est pas un exécutable dynamique
Nous pouvons alors l'inclure dans l'image scratch:
FROM scratch
COPY test /opt/app/
WORKDIR /opt/app
ENTRYPOINT ["./test"]
Utiliser l'image scratch et le langage Rust
Rust compile ses programmes avec un linking dynamique sur Glibc. Pour
passer en statique il faut utiliser musl, une librairie C
alternative qui permet l'édition de liens statique plus facile. Le soin
est laissé au lecteur d'utiliser une image Docker intermédiaire pour la
compilation du projet Rust.
Installer la cible de compilation
x86_64-unknown-linux-musl:
rustup target add x86_64-unknown-linux-musl
Dans le projet, éditer le fichier Cargo.toml pour y
ajouter l'option strip dans la section
profile.release afin de rendre l'exécutable produit plus
léger:
[package]
name = "myapp"
version = "0.1.0"
edition = "2025"
[dependencies]
rayon = "1.11.0"
walkdir = "2.5.0"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
strip = trueCompiler le projet comme suit:
cargo build --release --target x86_64-unknown-linux-musl
Le Dockerfile pour produire une image à partir de
scratch:
FROM scratch
COPY target/x86_64-unknown-linux-musl/release/myapp /opt/app/myapp
WORKDIR /opt/app
ENTRYPOINT ["./myapp"]
Considérations légales concernant les exécutables statiques
L'édition de liens statique est discutable sur le plan technique (non abordé ici) mais également légal. En effet la licence de certaines librairies peuvent contraindre le développeur à publier son code source comme c'est le cas pour la librairie Glibc. Ce problème ne se pose pas pour musl qui est publiée sous licence MIT, plus permissive. Cependant musl peut amener des problèmes d'incompatibilité avec d'autres librairies qui dépendent fortement de Glibc.