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
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.
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 /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 base | Contenu | Quand |
|---|---|---|
aspnet:10.0 | Debian/Ubuntu complet + runtime ASP.NET | Besoin d'outils dans l'image (débogage) |
aspnet:10.0-noble-chiseled | Minimal, non-root, sans shell, sans ICU ni tzdata | Applis en mode globalisation invariante |
aspnet:10.0-noble-chiseled-extra | Chiseled + ICU + tzdata | Défaut conseillé pour une appli en français (dates, tri, fuseaux) |
runtime-deps:10.0-… | Uniquement les dépendances natives | Appli self-contained ou Native AOT |
…-alpine | musl au lieu de glibc | Image 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.ProcessorCountreflète le quota CPU. Avec moins d'un cœur, Server GC apporte peu ; DATAS l'atténue. - Écoute réseau :
ASPNETCORE_HTTP_PORTS=8080par défaut, sur toutes les interfaces. Une appli liée àlocalhostest injoignable depuis l'extérieur du conteneur. - Arrêt propre : Kubernetes envoie
SIGTERMpuisSIGKILLaprèsterminationGracePeriodSeconds(30 s par défaut). L'hôte .NET déclencheStopAsync;HostOptions.ShutdownTimeoutvaut 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 }
| Sonde | Question posée | Échec → | Ne doit pas vérifier |
|---|---|---|---|
| startup | L'appli a-t-elle fini de démarrer ? | Redémarrage après le seuil | — |
| liveness | Le processus est-il bloqué ? | Redémarrage du conteneur | La base ou les API externes (sinon redémarrages en cascade) |
| readiness | Peut-il servir du trafic maintenant ? | Retiré du load balancing | — |
- Image chiseled sans ICU :
CultureInfo("fr-FR")lève une exception ou retombe sur la culture invariante, etTimeZoneInfo.FindSystemTimeZoneById("Europe/Paris")échoue sans tzdata. Prendre la variante-extra. - Secrets dans l'image : un
ENVou unappsettings.Production.jsoncopié 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
GCHeapHardLimitau-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.