14.4 — Conteneurs en production
🎯 Objectif : passer du
docker runlocal à des conteneurs robustes en prod. Docker pour packager, Kubernetes pour orchestrer (quand tu en as besoin) — sans tomber dans le piège du « K8s pour tout, tout de suite ».
À l'issue de cet axe, tu sauras :
- Écrire un Dockerfile multi-stage propre, petit, non-root et reproductible
- Signer et scanner ses images avant de les déployer
- Comprendre les briques K8s : pod, deployment, service, ingress, HPA, secret
- Choisir entre K8s managé et alternative plus simple
- Mettre en place du GitOps avec Argo CD ou Flux
Confirmé
D’abord — as-tu vraiment besoin de Kubernetes ?
Section intitulée « D’abord — as-tu vraiment besoin de Kubernetes ? »K8s est un excellent outil. Il est aussi excessivement coûteux en complexité pour 80 % des projets.
| Tu as | Tu n’as pas besoin de K8s |
|---|---|
| 1-3 services à scaler | ECS Fargate, Cloud Run, Render, Fly suffisent |
| Une équipe < 5 devs | Le coût cognitif K8s ralentit l’équipe |
| Un produit en MVP | K8s = 3 mois de setup au lieu de feature |
| < 100 req/s | Un VPS bien configuré tient |
Tu as besoin de K8s si :
- 20+ microservices avec interdépendances complexes.
- Multi-tenant avec isolation forte par namespace.
- Compliance qui exige des contrôles fins (NetworkPolicies, RBAC granulaire).
- Équipe ops dédiée.
- Charge variable et critique avec auto-scaling fin.
« Kubernetes est un système distribué qui transforme un autre système distribué en un troisième système distribué. » — un SRE anonyme
Cette section couvre les deux mondes : Docker industrialisé (la base utile à tout le monde), puis Kubernetes (quand le besoin est avéré).
Dockerfile production-ready
Section intitulée « Dockerfile production-ready »Anti-patterns à éviter
Section intitulée « Anti-patterns à éviter »# ❌ — image massive, root, npm install fullFROM node:24WORKDIR /appCOPY . .RUN npm installCMD ["node", "server.js"]Problèmes :
- Image > 1 GB (image Node full).
- Root par défaut (escalation possible).
npm installinstalle lesdevDependencies.COPY . .casse le cache à chaque modif.
Multi-stage propre
Section intitulée « Multi-stage propre »# ----- builder -----FROM node:24-alpine AS builderWORKDIR /appCOPY package*.json ./RUN --mount=type=cache,target=/root/.npm \ npm ciCOPY . .RUN npm run build && npm prune --omit=dev
# ----- runtime -----FROM node:24-alpine AS runtimeWORKDIR /app
# Utilisateur non-rootRUN addgroup -S app && adduser -S app -G appUSER app
ENV NODE_ENV=production \ NODE_OPTIONS="--enable-source-maps"
COPY --from=builder --chown=app:app /app/node_modules ./node_modulesCOPY --from=builder --chown=app:app /app/dist ./distCOPY --from=builder --chown=app:app /app/package.json ./
EXPOSE 3000HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ CMD wget -qO- http://127.0.0.1:3000/health || exit 1
CMD ["node", "dist/server.js"]Bénéfices :
| Levier | Effet |
|---|---|
| Multi-stage | L’image finale ne contient pas les outils de build |
| alpine | -200 à -800 MB |
--mount=type=cache | Cache npm/pnpm partagé entre builds |
Layer order : package*.json avant COPY . . | Cache hit sur dépendances inchangées |
USER app | Non-root → moins de surface d’attaque |
HEALTHCHECK | K8s / Render savent quand le container est prêt |
npm ci + --omit=dev | Pas de devDependencies en prod |
.dockerignore obligatoire
Section intitulée « .dockerignore obligatoire »node_modules.git.env.env.*dist*.log.DS_Storecoverage.nextSans lui, COPY . . envoie 500 MB de node_modules local au build context.
Distroless / Chainguard pour aller plus loin
Section intitulée « Distroless / Chainguard pour aller plus loin »FROM gcr.io/distroless/nodejs24-debian12:nonroot# ouFROM cgr.dev/chainguard/node:latestImages minimales sans shell, sans apt, sans utilitaires. Aucune surface d’attaque, image typiquement < 100 MB.
Inconvénient : pas de sh pour debug → tu utilises kubectl debug avec une image éphémère.
Registre — où vivent tes images
Section intitulée « Registre — où vivent tes images »| Registre | Force |
|---|---|
| GitHub Container Registry (ghcr.io) | Gratuit, intégré CI, par repo |
| Docker Hub | Le plus connu, mais quotas pull serrés |
| Amazon ECR | Intégré IAM, pas d’egress depuis EC2 |
| GCP Artifact Registry | Idem côté GCP |
| Cloudflare Registry | Pull depuis edge mondial |
| Self-hosted (Harbor) | Pour compliance fort |
Push depuis GitHub Actions vers ghcr.io
Section intitulée « Push depuis GitHub Actions vers ghcr.io »- uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6 with: context: . push: true tags: | ghcr.io/${{ github.repository }}:latest ghcr.io/${{ github.repository }}:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64,linux/arm64platforms: linux/amd64,linux/arm64 : multi-arch dès le départ — utile pour les Mac M-series locaux et serveurs ARM (Hetzner ARM Ampere, Graviton).
Sécurité d’image — scanner & signer
Section intitulée « Sécurité d’image — scanner & signer »Scan de vulnérabilités
Section intitulée « Scan de vulnérabilités »| Outil | Quoi |
|---|---|
| Trivy | OSS de référence, scan deps + OS + secrets + IaC |
| Grype | Idem, par Anchore |
| Snyk Container | Commercial, intégré DevSecOps |
| Docker Scout | Intégré Docker Desktop |
trivy image ghcr.io/myorg/myapp:1.2.0# CRITICAL: 0# HIGH: 2# ...Bloque la PR si un CRITICAL est détecté :
- run: trivy image --severity CRITICAL --exit-code 1 ghcr.io/myorg/myapp:${{ github.sha }}Signature — Sigstore / cosign
Section intitulée « Signature — Sigstore / cosign »Signer une image prouve qu’elle vient bien de toi (et pas d’un attaquant qui a compromis le registre).
cosign sign --yes ghcr.io/myorg/myapp:1.2.0cosign verify --certificate-identity=... ghcr.io/myorg/myapp:1.2.0Avec OIDC GitHub Actions, la signature est faite sans clé long-lived — la chaîne keyless Sigstore est l’option moderne.
SBOM — Software Bill of Materials
Section intitulée « SBOM — Software Bill of Materials »Liste de toutes les dépendances de ton image. Obligatoire pour certains contextes (Cyber Resilience Act EU, US Executive Order).
syft ghcr.io/myorg/myapp:1.2.0 -o spdx-json > sbom.jsonKubernetes — les briques essentielles
Section intitulée « Kubernetes — les briques essentielles »flowchart TD
Ingress -->|HTTP| Service
Service -->|TCP| Pod
Deployment -->|gère| Pod
Pod -->|consomme| ConfigMap
Pod -->|consomme| Secret
HPA -->|scale| Deployment
PVC -->|monté dans| Pod L’unité de base : 1+ conteneurs co-localisés (même IP, même volume éphémère).
apiVersion: v1kind: Podmetadata: { name: myapp-xyz }spec: containers: - name: app image: ghcr.io/myorg/myapp:1.2.0 ports: [{ containerPort: 3000 }] resources: requests: { cpu: 100m, memory: 256Mi } limits: { cpu: 500m, memory: 512Mi }Tu n’écris jamais des pods directement en prod. Tu écris des Deployments qui les gèrent.
Deployment — pods qui se ré-créent
Section intitulée « Deployment — pods qui se ré-créent »apiVersion: apps/v1kind: Deploymentmetadata: { name: myapp }spec: replicas: 3 selector: { matchLabels: { app: myapp } } template: metadata: { labels: { app: myapp } } spec: containers: - name: app image: ghcr.io/myorg/myapp:1.2.0 ports: [{ containerPort: 3000 }] resources: requests: { cpu: 100m, memory: 256Mi } limits: { cpu: 500m, memory: 512Mi } readinessProbe: httpGet: { path: /health, port: 3000 } initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: { path: /health, port: 3000 } initialDelaySeconds: 30 periodSeconds: 30| Probe | Effet |
|---|---|
| readinessProbe | Tant que ❌ → le pod ne reçoit pas de trafic |
| livenessProbe | Si ❌ persistant → kill et redémarre le pod |
| startupProbe | Pour apps longues à démarrer (évite que liveness kill avant ready) |
Service — IP stable devant les pods
Section intitulée « Service — IP stable devant les pods »apiVersion: v1kind: Servicemetadata: { name: myapp }spec: type: ClusterIP selector: { app: myapp } ports: - port: 80 targetPort: 3000Le service expose myapp.namespace.svc.cluster.local, load-balance les pods sains. type: LoadBalancer provisionne un LB cloud (AWS NLB, GCP LB, etc.).
Ingress — routage HTTP
Section intitulée « Ingress — routage HTTP »apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: myapp annotations: cert-manager.io/cluster-issuer: letsencrypt-prodspec: ingressClassName: nginx tls: - hosts: [api.example.com] secretName: myapp-tls rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: { service: { name: myapp, port: { number: 80 } } }Composant indispensable : un ingress controller installé (NGINX, Traefik, HAProxy, ALB Controller). Et cert-manager pour TLS auto.
HPA — autoscaling horizontal
Section intitulée « HPA — autoscaling horizontal »apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: { name: myapp }spec: scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: myapp } minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: { type: Utilization, averageUtilization: 70 }Plus avancé : KEDA scale aussi sur des métriques custom (queue length, requêtes Prometheus).
ConfigMap & Secret
Section intitulée « ConfigMap & Secret »apiVersion: v1kind: ConfigMapmetadata: { name: myapp-config }data: LOG_LEVEL: info---apiVersion: v1kind: Secretmetadata: { name: myapp-secrets }type: OpaquestringData: DATABASE_URL: postgres://user:pw@db/myappLes Secrets K8s ne sont pas chiffrés par défaut, juste base64-encodés. Pour vraiment chiffrer : Sealed Secrets (Bitnami), External Secrets Operator (lit depuis Vault/AWS Secrets Manager), ou SOPS.
K8s managé — qui choisir
Section intitulée « K8s managé — qui choisir »| Service | Force |
|---|---|
| EKS (AWS) | Mature, intégré IAM/VPC, cher |
| GKE (GCP) | Le plus mûr (K8s vient de Google), Autopilot mode |
| AKS (Azure) | Bien intégré AD, support entreprise |
| DigitalOcean Kubernetes | Simple, ~30 €/mois pour un petit cluster |
| Civo | Cluster en 90 s, bon prix |
| Scaleway Kapsule | Souverain FR/UE |
Évite d’auto-héberger ton cluster K8s en 2026 sauf besoin précis. Le control plane managé coûte ~70 $/mois et te fait gagner des semaines.
Helm — packages réutilisables
Section intitulée « Helm — packages réutilisables »# Installerhelm install myapp ./chart --values values-prod.yaml
# Mettre à jourhelm upgrade myapp ./chart --values values-prod.yaml --atomic
# Rollbackhelm rollback myapp 1Un chart est un dossier YAML templaté + values.yaml :
chart/├── Chart.yaml├── values.yaml└── templates/ ├── deployment.yaml ├── service.yaml ├── ingress.yaml └── hpa.yaml# templates/deployment.yaml (extrait)spec: replicas: {{ .Values.replicas }} template: spec: containers: - name: app image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"Helm est standard pour distribuer des apps réutilisables (ingress controllers, monitoring stacks). Pour ta propre app, kustomize est souvent plus simple — pas de templating, juste des overlays.
GitOps — Argo CD ou Flux
Section intitulée « GitOps — Argo CD ou Flux »Modèle :
- Le état désiré vit dans un repo Git (manifests YAML / Helm).
- Argo CD / Flux observe ce repo et réconcilie le cluster vers cet état.
- Tout déploiement est un commit — auditable, reversible.
┌──────────────┐ git push ┌────────────┐│ Dev/CI │ ───────────> │ Repo Git │└──────────────┘ │ (manifests)│ └─────┬──────┘ │ pull (toutes les 3 min) ▼ ┌────────────┐ │ Argo CD │ └─────┬──────┘ │ kubectl apply ▼ ┌────────────┐ │ Cluster │ └────────────┘Avantages :
- Observabilité : drift visible dans le UI Argo CD.
- Rollback =
git revert. - Multi-cluster sans recréer de pipelines.
- Aucun secret cloud côté CI (le cluster pulle).
Application minimale Argo CD
Section intitulée « Application minimale Argo CD »apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: { name: myapp, namespace: argocd }spec: project: default source: repoURL: https://github.com/myorg/myapp-manifests targetRevision: HEAD path: chart helm: valueFiles: [values-prod.yaml] destination: server: https://kubernetes.default.svc namespace: myapp syncPolicy: automated: { prune: true, selfHeal: true }Resource limits — la base d’un cluster sain
Section intitulée « Resource limits — la base d’un cluster sain »Sans requests/limits, un pod peut consommer toute la RAM d’un node et tuer ses voisins. Toujours définir :
| Ressource | Effet |
|---|---|
requests.cpu | Réservé pour le scheduler |
requests.memory | Idem, + déclenche eviction si dépassé |
limits.cpu | Plafond — le pod est throttle si dépassé |
limits.memory | Plafond — OOMKilled si dépassé |
Trop bas → ton app rame. Trop haut → tu paies pour rien. Mesure avant de fixer (Prometheus + Grafana, Goldilocks).
Déploiements progressifs
Section intitulée « Déploiements progressifs »| Stratégie | Quoi |
|---|---|
| Rolling update (défaut Deployment) | Rollout pod par pod |
| Blue-Green | 2 versions en parallèle, switch ingress |
| Canary | 5 % du trafic sur la nouvelle, monitorer, ramp up |
| Feature flags | Découplage déploiement / activation (LaunchDarkly, Unleash) |
Pour faire du canary sans Istio, Argo Rollouts est l’outil de référence — déploiement piloté par métriques.
Auto-évaluation
Section intitulée « Auto-évaluation »Pour aller plus loin
Section intitulée « Pour aller plus loin »- Docker Best Practices — docs.docker.com/develop/develop-images/dockerfile_best-practices
- Kubernetes Patterns — Bilgin Ibryam (très bon livre)
- Argo CD docs — argo-cd.readthedocs.io
- Helm Charts — helm.sh
- Sigstore / cosign — sigstore.dev
- Trivy — aquasecurity.github.io/trivy
- Goldilocks — github.com/FairwindsOps/goldilocks
Suite : 14.5 — Observabilité pour voir ce qui se passe en prod.