MVAESTECH
Guide .NET · 11 · Hébergement

Publié le · par Mathieu Vaes

Conteneurs & Kubernetes

Un conteneur n'est pas une VM légère : c'est un processus Linux isolé par des namespaces et limité par des cgroups. L'appli doit le savoir pour se comporter correctement.

Chapitres du guide
En clair

Un conteneur emballe l'appli avec tout ce dont elle a besoin (runtime, bibliothèques) pour qu'elle tourne de la même façon partout : sur votre PC, en test, en production. C'est un carton de déménagement standard : peu importe le camion, il rentre. Kubernetes est le chef de quai qui place, surveille et remplace les cartons.

VM et conteneur Junior

Machines virtuelles App A App B App C OS invité OS invité OS invité Hyperviseur Matériel Isolation forte · démarrage en minutes · Go de RAM par OS Conteneurs App A App B App C Runtime (containerd) · namespaces · cgroups Noyau Linux partagé Isolation de processus · démarrage en secondes · Mo de surcoût Le conteneur ne voit que ses cgroups : le runtime .NET y lit la limite CPU et mémoire réelle

Une image se construit en couches Junior

Chaque instruction du Dockerfile produit une couche en lecture seule, mise en cache. L'ordre compte : ce qui change rarement en bas, ce qui change à chaque commit en haut. Restaurer les packages avant de copier le code source permet de réutiliser la couche NuGet tant que les .csproj ne changent pas.

Image finale typique (ASP.NET Core, Ubuntu chiseled) : de l'ordre de 100 Mo, sans shell ni gestionnaire de paquets.

Ubuntu 24.04 chiseled (base)change rarement
Runtime ASP.NET Core 10patch mensuel
Dépendances NuGet publiéessi .csproj change
Vos assembliesà chaque commit

Dockerfile multi-stage

# --- Étape 1 : build (SDK complet, jetée ensuite) ---
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY Directory.Build.props Directory.Packages.props global.json ./
COPY src/Shop.Api/Shop.Api.csproj src/Shop.Api/
COPY src/Shop.Application/Shop.Application.csproj src/Shop.Application/
COPY src/Shop.Domain/Shop.Domain.csproj src/Shop.Domain/
COPY src/Shop.Infrastructure/Shop.Infrastructure.csproj src/Shop.Infrastructure/
RUN dotnet restore src/Shop.Api/Shop.Api.csproj          # couche mise en cache
COPY . .
RUN dotnet publish src/Shop.Api/Shop.Api.csproj -c Release -o /app --no-restore

# --- Étape 2 : runtime minimal ---
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra AS final
WORKDIR /app
COPY --from=build /app .
USER $APP_UID                     # non-root (déjà le défaut des images chiseled)
EXPOSE 8080                       # port par défaut depuis .NET 8
ENTRYPOINT ["dotnet", "Shop.Api.dll"]

Alternative sans Dockerfile : dotnet publish -t:PublishContainer construit l'image directement depuis le SDK (propriétés ContainerRepository, ContainerFamily), sans démon Docker. Pensez au .dockerignore (bin/, obj/, .git/, secrets locaux).

Image de baseContenuQuand
aspnet:10.0Debian/Ubuntu complet + runtime ASP.NETBesoin d'outils dans l'image (débogage)
aspnet:10.0-noble-chiseledMinimal, non-root, sans shell, sans ICU ni tzdataApplis en mode globalisation invariante
aspnet:10.0-noble-chiseled-extraChiseled + ICU + tzdataDéfaut conseillé pour une appli en français (dates, tri, fuseaux)
runtime-deps:10.0-…Uniquement les dépendances nativesAppli self-contained ou Native AOT
…-alpinemusl au lieu de glibcImage très petite ; tester les bibliothèques natives

Comportement de .NET dans un conteneur Senior

  • Mémoire : le GC lit la limite cgroup et fixe par défaut son plafond à 75 % de celle-ci. Laisser de la marge pour la mémoire native.
  • CPU : Environment.ProcessorCount reflète le quota CPU. Avec moins d'un cœur, Server GC apporte peu ; DATAS l'atténue.
  • Écoute réseau : ASPNETCORE_HTTP_PORTS=8080 par défaut, sur toutes les interfaces. Une appli liée à localhost est injoignable depuis l'extérieur du conteneur.
  • Arrêt propre : Kubernetes envoie SIGTERM puis SIGKILL après terminationGracePeriodSeconds (30 s par défaut). L'hôte .NET déclenche StopAsync ; HostOptions.ShutdownTimeout vaut 30 s depuis .NET 8.
  • Système de fichiers : éphémère. Logs sur stdout, fichiers dans un stockage objet, clés Data Protection dans un magasin partagé.

Déploiement Kubernetes (extrait)

apiVersion: apps/v1
kind: Deployment
metadata: { name: shop-api }
spec:
  replicas: 3
  selector: { matchLabels: { app: shop-api } }
  template:
    metadata: { labels: { app: shop-api } }
    spec:
      terminationGracePeriodSeconds: 40
      securityContext: { runAsNonRoot: true }
      containers:
        - name: api
          image: europe-west9-docker.pkg.dev/acme/shop/api:1.8.2   # tag immuable, jamais :latest
          ports: [{ containerPort: 8080 }]
          resources:
            requests: { cpu: "250m", memory: "256Mi" }
            limits:   { memory: "512Mi" }          # limite mémoire oui ; limite CPU souvent évitée (throttling)
          securityContext: { readOnlyRootFilesystem: true, allowPrivilegeEscalation: false }
          startupProbe:   { httpGet: { path: /health/live,  port: 8080 }, failureThreshold: 30, periodSeconds: 2 }
          livenessProbe:  { httpGet: { path: /health/live,  port: 8080 }, periodSeconds: 10 }
          readinessProbe: { httpGet: { path: /health/ready, port: 8080 }, periodSeconds: 5 }
SondeQuestion poséeÉchec →Ne doit pas vérifier
startupL'appli a-t-elle fini de démarrer ?Redémarrage après le seuil—
livenessLe processus est-il bloqué ?Redémarrage du conteneurLa base ou les API externes (sinon redémarrages en cascade)
readinessPeut-il servir du trafic maintenant ?Retiré du load balancing—
Piège
  • Image chiseled sans ICU : CultureInfo("fr-FR") lève une exception ou retombe sur la culture invariante, et TimeZoneInfo.FindSystemTimeZoneById("Europe/Paris") échoue sans tzdata. Prendre la variante -extra.
  • Secrets dans l'image : un ENV ou un appsettings.Production.json copié reste lisible dans les couches. Injecter à l'exécution.
  • :latest : déploiements non reproductibles, rollback impossible. Tag immuable (version ou SHA) et digest.
  • OOMKilled : limite mémoire trop proche de l'usage réel, ou GCHeapHardLimit au-dessus de la limite cgroup.
  • Readiness qui ne bascule pas à l'arrêt : des requêtes arrivent pendant le shutdown. Ajouter un court délai avant l'arrêt de Kestrel si nécessaire.

À retenir

  • Multi-stage, restore avant copie du code, image chiseled-extra non-root.
  • Port 8080, logs sur stdout, système de fichiers en lecture seule.
  • Trois sondes distinctes ; liveness ne dépend jamais d'un service externe.

Un projet .NET à cadrer ou à reprendre ?

API REST documentées et testées, applications ASP.NET MVC (y compris VB.NET), migration de services anciens vers une architecture claire : parlons de votre code.