DEV Community

BeardDemon
BeardDemon

Posted on

J'ai réduit mes images Docker de 800MB à 120MB (et vous pouvez faire pareil)

Le problème de départ

Mes premières images Docker pour le projet e-commerce:

  • API Clients: 847 MB
  • API Catalogue: 823 MB
  • API Commandes: 891 MB

Totalement ridicule pour des APIs Django qui font juste du CRUD.

Le pire ? Mes workers Kubernetes galéraient à pull les images. Ça prenait 2-3 minutes par image. Sur un cluster de 2 workers avec 4 services, faites le calcul...

Ce que j'ai changé

Après optimisation:

  • API Clients: 127 MB (-85%)
  • API Catalogue: 119 MB (-86%)
  • API Commandes: 132 MB (-85%)

Et le meilleur : elles marchent EXACTEMENT pareil.

La technique : multi-stage build + from scratch

Étape 1 : Builder (Alpine)

FROM python:3-alpine AS builder

# Installer les outils de compilation
RUN apk add --no-cache gcc python3-dev musl-dev mariadb-dev pkgconf

# Créer un venv
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Virer pip (on en a plus besoin)
RUN pip uninstall pip -y

# Nettoyage agressif
RUN find /opt/venv -type d \( \
        -name 'tests' -o \
        -name '__pycache__' -o \
        -name '*.dist-info' \
    \) -exec rm -rf {} + 2>/dev/null || true
Enter fullscreen mode Exit fullscreen mode

Cette étape compile tout. C'est gros, c'est moche, mais c'est temporaire.

Étape 2 : Syscollector (récupérer juste ce qu'il faut)

FROM python:3-alpine AS syscollector

RUN apk add --no-cache mariadb-connector-c

RUN mkdir -p /rootfs/lib /rootfs/usr/lib /rootfs/usr/local/bin

# Copier Python + stdlib complète
RUN cp /usr/local/bin/python3* /rootfs/usr/local/bin/ && \
    cp -r /usr/local/lib/python3* /rootfs/usr/local/lib/

# Copier sh (pour le entrypoint)
RUN cp /bin/sh /rootfs/bin/

# Récupérer les dépendances avec ldd
RUN ldd /usr/local/bin/python3* /bin/sh 2>/dev/null | \
    grep "=>" | awk '{print $3}' | \
    sort -u | xargs -I '{}' cp '{}' /rootfs/lib/

# Dynamic linker
RUN cp /lib/ld-musl-*.so.1 /rootfs/lib/

# Libs MariaDB
RUN find /usr/lib -name 'libmariadb*.so*' -exec cp {} /rootfs/usr/lib/ \;
Enter fullscreen mode Exit fullscreen mode

Ici, je récupère UNIQUEMENT ce dont j'ai besoin. Pas de package manager, pas d'outils de debug, rien.

Étape 3 : Image finale (from scratch)

FROM scratch

COPY --from=syscollector /rootfs /
COPY --from=builder /opt/venv /opt/venv
COPY . /app

ENV PATH="/opt/venv/bin:/usr/local/bin:/bin" \
    LD_LIBRARY_PATH="/lib:/usr/lib:/usr/local/lib" \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app/project
EXPOSE 8000

ENTRYPOINT ["sh", "/app/docker-entrypoint.sh"]
Enter fullscreen mode Exit fullscreen mode

FROM scratch = littéralement RIEN. Même pas un shell de base. C'est pour ça qu'il faut copier /bin/sh dans l'étape 2.

Les pièges que j'ai rencontrés

Piège #1 : Supprimer les *.dist-info

Au début j'ai fait:

find /opt/venv -name '*.dist-info' -exec rm -rf {} +
Enter fullscreen mode Exit fullscreen mode

Résultat ? Plus rien marchait. Python utilisait ces fichiers pour importer les packages.

Solution: Garder les métadonnées critiques, virer que le superflu.

Piège #2 : Oublier le dynamic linker

standard_init_linux.go:228: exec user process caused: no such file or directory
Enter fullscreen mode Exit fullscreen mode

Ce message cryptique = j'avais oublié de copier /lib/ld-musl-*.so.1.

Piège #3 : MariaDB qui fait des siennes

MariaDB a besoin de plein de libs partagées. J'utilisais ldd pour les trouver:

ldd /usr/lib/libmariadb.so.3
Enter fullscreen mode Exit fullscreen mode

Et je copiais toutes les dépendances dans /rootfs/lib.

Comparaison avant/après

Avant (image basique)

FROM python:3

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . /app
WORKDIR /app

CMD ["gunicorn", "app:app"]
Enter fullscreen mode Exit fullscreen mode

Taille: 847 MB

Layers: 12

Temps de pull: 2min 34s (sur mon cluster)

Après (optimisée)

Taille: 127 MB

Layers: 7

Temps de pull: 14s

Le gain de temps sur les déploiements est ÉNORME.

Le docker-entrypoint.sh

Petit bonus, mon script d'entrypoint fait les migrations automatiquement:

#!/bin/bash
set -e

echo "Running migrations..."
python3 manage.py makemigrations api
python3 manage.py migrate --noinput

echo "Collecting static..."
python3 manage.py collectstatic --noinput

echo "Starting Gunicorn..."
exec gunicorn project.wsgi:application --bind 0.0.0.0:8000
Enter fullscreen mode Exit fullscreen mode

Métriques finales

Métrique Avant Après Gain
Taille image 847 MB 127 MB -85%
Temps de build 4min 12s 3min 8s -25%
Temps de pull 2min 34s 14s -91%
Layers 12 7 -42%

Conclusion

Est-ce que ça vaut le coup ? Pour moi, OUI:

  • Déploiements plus rapides
  • Moins de bande passante
  • Images plus sécurisées (surface d'attaque réduite)

Par contre, c'est plus complexe à débugger. Pas de apt install, pas de curl, rien. Si vous avez besoin de débugger, utilisez une image de dev normale.

Le code complet est sur mon GitHub : [lien vers le repo]

Des questions ? Ping moi sur Twitter [@tonhandle]

Top comments (0)