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
ModeTailleDémarrageQuand
Dépendant du framework~5 Mo~80 msPar défaut. Idéal en conteneur (l'image fournit le runtime)
Autonome~70 Mo~80 msMachine sans .NET installé, poste client, outil distribué
Fichier unique~70 Mo~100 msOutil en ligne de commande à distribuer
Native AOT~15 Mo~10 msFonctions serverless, mise à l'échelle à zéro, conteneurs très nombreux
Ce que Native AOT interdit

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

Dockerfile multi-étapes — le modèle à recopier
# ---------- é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"]
Sans écrire de Dockerfile : le SDK sait le faire
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
Les cinq réflexes conteneur
  1. Multi-étapes : le SDK (≈ 1 Go) ne doit jamais se retrouver dans l'image finale.
  2. Copier les .csproj avant le reste : la restauration NuGet reste en cache, les constructions passent de 3 minutes à 20 secondes.
  3. Pas de root : USER $APP_UID.
  4. Aucun secret dans l'image : ils arrivent par variables d'environnement à l'exécution.
  5. 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
Les oublis classiques du premier déploiement
OubliSymptôme
ASPNETCORE_ENVIRONMENT resté à DevelopmentPiles d'appels exposées aux utilisateurs : fuite d'informations
Clés Data Protection non persistéesUtilisateurs déconnectés à chaque déploiement ou changement d'instance
Migrations non appliquéesInvalid 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 devDates et formats de nombres décalés
En-têtes de proxy non configurésToutes les adresses IP journalisées identiques, redirections HTTPS en boucle

Appliquer les migrations sans casser la production

StratégieVerdict
db.Database.Migrate() au démarragePratique 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 pipelineLa 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> --addCompile et applique en une étape ; pratique en conteneur, à réserver aux environnements non critiques
Migrations compatibles : la règle des deux déploiements

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 :

  1. Déploiement 1 : ajouter la nouvelle colonne, écrire dans les deux, lire l'ancienne.
  2. Déploiement 2 : lire la nouvelle.
  3. 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") });
Ne confonds pas les deux sondes

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

.github/workflows/ci.yml
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éployerPoints d'attention
Azure App ServiceLe 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 nuService systemd + nginx en proxy inverse + Restart=always
Sans serveur (Azure Functions, Lambda)Native AOT très rentable : démarrage à froid divisé par cinq
Détails d'exploitation

Vérifie que c'est passé

🎯 Quiz — 4 questions

1. Pourquoi copier les .csproj avant le reste du code dans un Dockerfile ?

Docker met en cache par couche. Si le 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 ?

« Vivant » ne doit tester que le processus. Les dépendances vont dans « prêt », qui retire l'instance du routage sans la tuer.

3. Comment appliquer les migrations en production ?

Avec plusieurs instances, 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 ?

AOT convient aux Minimal API avec sérialisation générée. Le gain (démarrage, empreinte) doit justifier les contraintes.

Fiches de révision

Publication par défaut ?
dotnet publish -c Release : dépendant du framework, ~5 Mo. Le runtime vient de l'image.
Quand Native AOT ?
Serverless, mise à l'échelle à zéro, démarrage critique. Minimal API + JSON généré uniquement.
Dockerfile : la règle nº 1 ?
Multi-étapes + .csproj copiés d'abord + pas de root.
Vivant vs prêt ?
Vivant = le processus répond (aucune dépendance). Prêt = il peut servir (base, cache…).
Migrations sans coupure ?
Compatibles sur deux déploiements : ajouter, écrire double, puis supprimer.
Le premier oubli en production ?
ASPNETCORE_ENVIRONMENT=Production et les clés Data Protection partagées.
À retenir