Images Docker multi-stage pour .NET + Angular
node_modules qui pèse souvent plusieurs centaines de mégaoctets. Aucun de ces outils ne sert à l'exécution. Les embarquer dans l'image qu'on déploie, c'est expédier une chaîne de compilation entière pour lancer un binaire qui n'en a plus besoin.
Le principe : compiler puis jeter
Dockerfile multi-stage déclare plusieurs FROM . Chaque FROM ouvre un stage isolé, avec sa propre image de base et son propre système de fichiers. Seul le dernier stage devient l'image livrée ; tous les précédents servent uniquement à produire des artefacts.
COPY --from . Le SDK, les sources, les caches de restauration restent en arrière et n'atteignent jamais l'image finale. Le stage de build fait le gros du travail ; le stage final ne reçoit que le résultat.
Le Dockerfile de l'API .NET
.csproj et restaurer avant de copier le code. Le restore n'est alors rejoué que si une dépendance change. Une modification dans un fichier C# n'invalide que le publish , qui repart d'un cache NuGet déjà chaud. Sur une base de code réelle, ça fait la différence entre un build de quelques secondes et une restauration complète à chaque commit.
--no-restore sur dotnet publish évite qu'il relance une restauration : la couche précédente s'en est déjà chargée. Attention au piège des solutions multi-projets : si Api référence Domain , il faut copier les deux .csproj à leur emplacement avant le restore , sinon la restauration échoue faute de retrouver le graphe de références.
app , UID 1654). Sur une image aspnet standard, le processus tourne en root sauf mention contraire ; on bascule alors explicitement avec USER $APP_UID , la variable que ces images définissent déjà.
Le Dockerfile du front Angular
npm ci n'est pas npm install . Il exige un package-lock.json présent et cohérent, supprime le node_modules existant, et installe exactement les versions verrouillées. C'est reproductible et plus rapide, ce qui est précisément ce qu'on veut dans une image. Un changement de composant n'invalide ni le lockfile ni la couche npm ci : l'install reste en cache, seul le build se rejoue.
ng build dépose le bundle dans dist/<app>/browser . Le stage final ne copie que ce dossier dans la racine nginx.
nginx lance son processus maître en root. La variante nginx-unprivileged tourne entièrement sous un compte non-root et écoute sur le port 8080, ce qui évite d'ouvrir un port privilégié dans le conteneur. La nginx.conf a besoin d'une seule directive côté application : try_files $uri $uri/ /index.html , pour que le routage Angular prenne le relais au lieu de renvoyer un 404 sur une URL profonde rechargée.
Le .dockerignore , sans lequel le cache ment
.dockerignore , sans lequel le cache ment Dockerfile , Docker envoie le contexte de build au démon. Sans filtre, ce contexte inclut votre bin/ , votre obj/ , votre node_modules local, le dossier .git . Des centaines de mégaoctets transférés pour rien, et pire : un COPY . . recopie ces artefacts de build de l'hôte dans le stage, où ils peuvent contredire ce que le conteneur vient de restaurer.
.gitignore . En excluant les répertoires de sortie et les artefacts locaux, on garantit que le build part des seules sources et que le cache reflète la réalité. Un obj/ traîné depuis la machine de dev est une cause classique de builds qui « marchent chez moi » et cassent en CI.
Orchestrer les deux en local
docker-compose.yml câble l'API et le front sur un même réseau et les construit d'un seul docker compose up .
--target , pour arrêter la construction à un stage précis (par exemple un stage de test lancé en CI sans produire l'image de runtime), et les montages de cache BuildKit ( RUN --mount=type=cache ) qui persistent les caches NuGet et npm entre deux builds.
Une image de production ne devrait contenir que ce qui s'exécute. Le multi-stage rend cette discipline gratuite : le SDK et Node restent dans les stages de build, jamais dans ce que vous déployez.