Accueil › Qualité · perf · production › Chapitre 20
Publier et déployer
« Ça marche sur ma machine » n'est pas un état livrable. Ce chapitre couvre le passage du dossier de développement à quelque chose qui tourne ailleurs, se surveille, et se met à jour sans couper le service.
Publier : les quatre modes
# 1. Dépendant du framework (le défaut) — le runtime .NET doit être installé sur la cible
dotnet publish -c Release -o ./publication
# 2. Autonome (self-contained) — le runtime est embarqué, rien à installer
dotnet publish -c Release -r linux-x64 --self-contained -o ./publication
# 3. Fichier unique — un seul exécutable
dotnet publish -c Release -r win-x64 --self-contained \
-p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true
# 4. Native AOT — compilé en code machine, sans JIT
dotnet publish -c Release -r linux-x64 -p:PublishAot=true
| Mode | Taille | Démarrage | Quand |
|---|---|---|---|
| Dépendant du framework | ~5 Mo | ~80 ms | Par défaut. Idéal en conteneur (l'image fournit le runtime) |
| Autonome | ~70 Mo | ~80 ms | Machine sans .NET installé, poste client, outil distribué |
| Fichier unique | ~70 Mo | ~100 ms | Outil en ligne de commande à distribuer |
| Native AOT | ~15 Mo | ~10 ms | Fonctions serverless, mise à l'échelle à zéro, conteneurs très nombreux |
Pas de réflexion dynamique, pas de génération de code à l'exécution, pas de Assembly.Load.
Conséquences concrètes : la sérialisation JSON exige un JsonSerializerContext généré, les
contrôleurs MVC ne sont pas compatibles (les Minimal API le sont), et certaines bibliothèques refusent de
fonctionner. Vérifie avec <PublishAot>true</PublishAot> et les avertissements du
compilateur avant de t'engager. C'est un choix de fin de projet, motivé par une mesure.
Conteneuriser
# ---------- étape de compilation ----------
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# On copie d'abord les .csproj : la restauration est mise en cache tant qu'ils ne changent pas
COPY ["Boutique.Api/Boutique.Api.csproj", "Boutique.Api/"]
COPY ["Boutique.Domaine/Boutique.Domaine.csproj", "Boutique.Domaine/"]
RUN dotnet restore "Boutique.Api/Boutique.Api.csproj"
COPY . .
RUN dotnet publish "Boutique.Api/Boutique.Api.csproj" -c Release -o /app --no-restore
# ---------- étape d'exécution : image minimale ----------
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app .
# Ne jamais tourner en root
USER $APP_UID
ENV ASPNETCORE_URLS=http://+:8080 \
ASPNETCORE_ENVIRONMENT=Production \
DOTNET_gcServer=1
EXPOSE 8080
ENTRYPOINT ["dotnet", "Boutique.Api.dll"]
dotnet publish -c Release /t:PublishContainer
dotnet publish -c Release /t:PublishContainer -p:ContainerImageTag=1.2.3
# Variantes d'images disponibles :
# aspnet:10.0 → Debian, le défaut
# aspnet:10.0-alpine → beaucoup plus petite
# aspnet:10.0-noble-chiseled → sans shell ni gestionnaire de paquets : surface d'attaque minimale
- Multi-étapes : le SDK (≈ 1 Go) ne doit jamais se retrouver dans l'image finale.
- Copier les
.csprojavant le reste : la restauration NuGet reste en cache, les constructions passent de 3 minutes à 20 secondes. - Pas de root :
USER $APP_UID. - Aucun secret dans l'image : ils arrivent par variables d'environnement à l'exécution.
- Fixer les limites mémoire : sans elles, le ramasse-miettes croit disposer de toute la machine et l'orchestrateur finit par tuer le processus.
Configuration en production
# La même image, un comportement différent selon les variables (chapitre 12)
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://+:8080
ConnectionStrings__Defaut="Server=sql;Database=Boutique;User Id=app;Password=***"
Smtp__Hote=smtp.interne
Logging__LogLevel__Default=Warning
OTEL_EXPORTER_OTLP_ENDPOINT=http://collecteur:4317
| Oubli | Symptôme |
|---|---|
ASPNETCORE_ENVIRONMENT resté à Development | Piles d'appels exposées aux utilisateurs : fuite d'informations |
| Clés Data Protection non persistées | Utilisateurs déconnectés à chaque déploiement ou changement d'instance |
| Migrations non appliquées | Invalid object name 'Produits' au premier appel |
| Pas de contrôle de santé | L'orchestrateur route du trafic vers une instance non prête |
| Fuseau horaire / culture différents du poste de dev | Dates et formats de nombres décalés |
| En-têtes de proxy non configurés | Toutes les adresses IP journalisées identiques, redirections HTTPS en boucle |
Appliquer les migrations sans casser la production
| Stratégie | Verdict |
|---|---|
db.Database.Migrate() au démarrage | Pratique en développement. Dangereux en production : plusieurs instances migrent en même temps, et un échec empêche tout démarrage |
| Script SQL idempotent relu et appliqué par le pipeline | La référence. Relisible, validable, journalisé |
| Migration bundle (exécutable autonome) | Excellent : ne nécessite ni SDK ni code source sur la cible |
EF 11 : dotnet ef database update <nom> --add | Compile et applique en une étape ; pratique en conteneur, à réserver aux environnements non critiques |
Pendant un déploiement progressif, l'ancienne et la nouvelle version du code tournent en même temps. Une migration doit donc être compatible avec les deux. Pour renommer une colonne :
- Déploiement 1 : ajouter la nouvelle colonne, écrire dans les deux, lire l'ancienne.
- Déploiement 2 : lire la nouvelle.
- Déploiement 3 : supprimer l'ancienne.
Fastidieux, mais c'est le prix d'un déploiement sans coupure.
Contrôles de santé
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDb>("base", tags: ["pret"])
.AddCheck("smtp", () => HealthCheckResult.Healthy(), tags: ["pret"]);
// « Je suis vivant » : ne teste AUCUNE dépendance externe
app.MapHealthChecks("/sante/vivant", new() { Predicate = _ => false });
// « Je suis prêt à recevoir du trafic » : teste les dépendances
app.MapHealthChecks("/sante/pret", new() { Predicate = c => c.Tags.Contains("pret") });
Si ta sonde liveness vérifie la base de données, une coupure de base fait redémarrer en boucle toutes tes instances — alors qu'elles étaient parfaitement saines et n'avaient qu'à attendre. Vivant = le processus répond. Prêt = il peut servir des requêtes utiles.
Intégration et déploiement continus
name: CI
on:
push: { branches: [main] }
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with: { dotnet-version: '10.0.x' }
- run: dotnet restore
- run: dotnet build --no-restore -c Release -warnaserror
- run: dotnet test --no-build -c Release --collect:"XPlat Code Coverage"
- run: dotnet list package --vulnerable --include-transitive
- run: dotnet format --verify-no-changes # style respecté
- run: dotnet publish Boutique.Api -c Release /t:PublishContainer
| Où déployer | Points d'attention |
|---|---|
| Azure App Service | Le plus simple. Emplacements de préproduction pour un échange sans coupure ; paramètres d'application = variables d'environnement |
| Conteneurs (Container Apps, ECS, Kubernetes) | Sondes de santé, limites mémoire, arrêt propre (SIGTERM), sessions persistantes si Blazor Server |
| IIS (Windows) | Module ASP.NET Core, mode in-process, recyclage du pool d'applications |
| Linux nu | Service systemd + nginx en proxy inverse + Restart=always |
| Sans serveur (Azure Functions, Lambda) | Native AOT très rentable : démarrage à froid divisé par cinq |
- Arrêt propre : l'hôte écoute SIGTERM, arrête d'accepter de nouvelles requêtes, laisse finir les
requêtes en cours (délai réglable via
ShutdownTimeout), puis libère les services. Il faut que leCancellationTokensoit propagé partout (chapitre 8) et que l'orchestrateur laisse le temps nécessaire. - Déploiements bleu-vert / canari : deux environnements, bascule du trafic, retour arrière instantané. Suppose des migrations compatibles (voir plus haut).
- Version dans les logs : injecte le numéro de commit
(
-p:SourceRevisionId=$GITHUB_SHA) et journalise-le au démarrage. Savoir quelle version tourne est la première question de tout incident. - .NET Aspire : décrit l'ensemble (API, base, cache, files) dans un projet d'orchestration, fournit un tableau de bord de télémétrie en développement et génère les manifestes de déploiement. Excellent pour une architecture à plusieurs services ; superflu pour une application unique.
- Mises à jour de sécurité : reconstruis et redéploie régulièrement même sans changement de code — l'image de base reçoit des correctifs. Automatise la mise à jour des paquets (Dependabot, Renovate).
Vérifie que c'est passé
🎯 Quiz — 4 questions
1. Pourquoi copier les .csproj avant le reste du code dans un Dockerfile ?
COPY . . vient avant le
restore, la moindre modification de code invalide la restauration : plusieurs minutes perdues à
chaque construction.2. Ta sonde liveness vérifie la base de données. Que se passe-t-il si la base tombe 30 secondes ?
3. Comment appliquer les migrations en production ?
Migrate() au démarrage crée des courses
critiques, et un échec de migration empêche tout démarrage.4. Native AOT pour une API MVC classique avec beaucoup de réflexion ?
Fiches de révision
dotnet publish -c Release : dépendant du framework, ~5 Mo. Le runtime vient de l'image..csproj copiés d'abord + pas de root.ASPNETCORE_ENVIRONMENT=Production et les clés Data Protection partagées.- Une image, plusieurs environnements : tout ce qui change passe par des variables.
- Dockerfile multi-étapes, restauration mise en cache, exécution sans root.
- Migrations par script, jamais au démarrage de plusieurs instances.
- Deux sondes distinctes : vivant et prêt.
- Journalise la version déployée : première question de tout incident.