LangueFR
Français
English
Retour au sommaire du cours

Docker, Compose et Kubernetes

« Ça marche chez moi. » Ce module traite l'outil qui a rendu cette phrase obsolète : le conteneur. On part de ce qu'est réellement un conteneur — et de ce qui le distingue d'une machine virtuelle —, puis on construit une image pour un projet Python, on orchestre une pile complète avec Docker Compose, et on termine par ce qu'il faut savoir de Kubernetes pour lire un manifeste et comprendre ce que fait une équipe plateforme.

1. Le problème que Docker résout

Un projet Python ne dépend pas que de Python. Il dépend d'une version de l'interpréteur, de bibliothèques compilées (BLAS, libpq, ffmpeg), de paquets système, de variables d'environnement, parfois d'un pilote GPU et d'une version de CUDA. Un environnement virtuel (module 1) fige les bibliothèques Python ; il ne fige rien de tout le reste. C'est pourquoi le même code fonctionne sur un portable et échoue sur le serveur.

Un conteneur emporte tout ce dont l'application a besoin — code, dépendances Python, bibliothèques système, fichiers de configuration — dans une unité qui s'exécute à l'identique partout où tourne un moteur de conteneurs. Le bénéfice ne se limite pas au déploiement : il rend un résultat reproductible dans six mois, ce qui est exactement ce qu'on cherche en data science (module 26).

2. Conteneur ou machine virtuelle ?

Les deux isolent une application, mais à des niveaux très différents.

deux façons d'isoler
        MACHINE VIRTUELLE                       CONTENEUR
  ┌───────────────────────────┐        ┌───────────────────────────┐
  │  App A    │    App B      │        │  App A    │    App B      │
  ├───────────┼───────────────┤        ├───────────┼───────────────┤
  │ Biblioth. │  Biblioth.    │        │ Biblioth. │  Biblioth.    │
  ├───────────┼───────────────┤        ├───────────┴───────────────┤
  │ Noyau OS  │  Noyau OS     │ ←──┐   │   moteur de conteneurs    │
  │  invité   │   invité      │    │   ├───────────────────────────┤
  ├───────────┴───────────────┤    │   │   NOYAU OS DE L'HÔTE      │ ← partagé
  │        Hyperviseur        │    │   ├───────────────────────────┤
  ├───────────────────────────┤    │   │        Matériel           │
  │      Noyau OS hôte        │    │   └───────────────────────────┘
  ├───────────────────────────┤    │
  │        Matériel           │    └── chaque VM embarque un système
  └───────────────────────────┘        d'exploitation complet

Une machine virtuelle simule un ordinateur entier : l'hyperviseur lui présente un processeur, de la mémoire et des disques virtuels, sur lesquels on installe un système d'exploitation complet avec son propre noyau. Isolation très forte, mais coût élevé — plusieurs gigaoctets sur disque, plusieurs centaines de mégaoctets de RAM, des dizaines de secondes de démarrage.

Un conteneur ne simule rien. C'est un processus ordinaire du système hôte, à qui le noyau ment sur ce qu'il voit :

  • les namespaces cloisonnent sa vision — il ne voit que ses propres processus, sa propre arborescence de fichiers, ses propres interfaces réseau ;
  • les cgroups (control groups) limitent ce qu'il consomme — tant de CPU, tant de mémoire.

Puisqu'il n'y a pas de noyau invité, le coût est celui d'un processus : quelques mégaoctets, un démarrage en quelques dizaines de millisecondes. L'analogie la plus juste : une VM est une maison individuelle avec ses propres fondations ; un conteneur est un appartement dans un immeuble — murs séparés, mais fondations et structure partagées.

La conséquence à connaître. Le noyau étant partagé, un conteneur Linux ne peut pas tourner nativement sur un noyau Windows ou macOS : Docker Desktop y fait en réalité tourner une petite machine virtuelle Linux, ce qui explique les performances d'accès disque parfois décevantes sur ces systèmes. Autre conséquence : l'isolation d'un conteneur est réelle mais moins forte que celle d'une VM, puisqu'une faille du noyau les concerne tous. Pour exécuter du code non fiable, une VM reste plus sûre.

3. Image, conteneur, couche, registre

  • Une image est un modèle figé, en lecture seule : un système de fichiers complet plus des métadonnées (commande à lancer, variables d'environnement, port exposé). C'est l'équivalent d'une classe.
  • Un conteneur est une instance en cours d'exécution de cette image, avec une fine couche inscriptible par-dessus. C'est l'équivalent d'un objet. On peut lancer vingt conteneurs à partir de la même image.
  • Une couche (layer) est une strate de l'image, produite par une instruction du Dockerfile. Les couches sont empilées, partagées entre images et mises en cache individuellement — c'est ce qui rend les reconstructions rapides et les téléchargements légers.
  • Un registre est un entrepôt d'images : Docker Hub (public), GitHub Container Registry, ou un registre privé d'entreprise.

4. Les commandes du quotidien

bash — lancer et inspecter
# lancer un conteneur (télécharge l'image si nécessaire)
docker run hello-world

# interactif : un shell dans un conteneur Python jetable
docker run -it --rm python:3.12-slim bash
#          │   └─ --rm : supprime le conteneur à la sortie (sinon il s'accumule)
#          └─ -it : mode interactif avec terminal

# en arrière-plan, avec un nom et une redirection de port
docker run -d --name mon-api -p 8000:8000 mon-image:1.0
#          │              └─ port 8000 de la MACHINE → port 8000 du CONTENEUR
#          └─ -d : detached, rend la main immédiatement

# voir ce qui tourne
docker ps                    # conteneurs en cours
docker ps -a                 # y compris ceux qui sont arrêtés
docker stats                 # consommation CPU / mémoire en direct

# lire les journaux
docker logs mon-api          # tout depuis le début
docker logs -f --tail 50 mon-api    # suivre en direct les 50 dernières lignes

# entrer dans un conteneur qui tourne (LE réflexe de débogage)
docker exec -it mon-api bash
docker exec mon-api python -c "import torch; print(torch.cuda.is_available())"

# inspecter en détail (configuration, réseaux, montages)
docker inspect mon-api
docker port mon-api          # quelles redirections de ports sont actives
bash — construire, partager, nettoyer
# construire une image à partir du Dockerfile du répertoire courant
docker build -t mon-api:1.0 .
docker build -t mon-api:1.0 --build-arg VERSION=1.0 -f docker/Dockerfile .

# lister, étiqueter, publier
docker images
docker tag mon-api:1.0 ghcr.io/nils/mon-api:1.0
docker login ghcr.io && docker push ghcr.io/nils/mon-api:1.0
docker pull postgres:16

# arrêter et supprimer
docker stop mon-api && docker rm mon-api
docker rmi mon-api:1.0

# faire de la place (Docker consomme beaucoup de disque avec le temps)
docker system df             # ce qui occupe l'espace
docker container prune       # conteneurs arrêtés
docker image prune -a        # images non utilisées
docker system prune -a --volumes    # grand nettoyage — supprime aussi les VOLUMES

5. Écrire un Dockerfile

Un Dockerfile est une recette : chaque instruction produit une couche. Voici les instructions à connaître, avant l'exemple complet.

les instructions essentielles
FROM python:3.12-slim      image de départ ("slim" = sans les outils de compilation)
WORKDIR /app               répertoire de travail ; le crée si besoin
COPY source destination    copie depuis la machine vers l'image
RUN commande               exécute PENDANT LA CONSTRUCTION (installer des paquets…)
ENV CLE=valeur             variable d'environnement, présente à l'exécution
ARG CLE=valeur             variable disponible seulement pendant la construction
EXPOSE 8000                documentation du port utilisé (ne publie rien par lui-même)
USER app                   change d'utilisateur : ne pas rester root
HEALTHCHECK …              commande que Docker lance pour vérifier la santé
CMD ["uvicorn", "…"]       commande lancée AU DÉMARRAGE du conteneur
ENTRYPOINT ["python"]      exécutable fixe ; CMD fournit alors ses arguments

Le cache des couches : la règle qui divise les temps de build par dix

Docker met en cache chaque couche et la réutilise tant que ni l'instruction ni les fichiers qu'elle copie n'ont changé. Dès qu'une couche est invalidée, toutes les suivantes le sont aussi. D'où la règle d'or : ordonner du plus stable au plus volatil. Les dépendances changent une fois par mois, le code change à chaque commit. Si l'on copie tout le projet avant d'installer les dépendances, la moindre modification d'une ligne de code réinstalle l'intégralité de l'environnement — plusieurs minutes à chaque build. En copiant d'abord pyproject.toml et uv.lock, cette couche reste valide tant que les dépendances ne bougent pas.

Dockerfile — projet Python avec UV, construction en deux étapes
# ════════ étape 1 : construire l'environnement ════════
FROM python:3.12-slim AS builder

# on récupère le binaire uv depuis son image officielle
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

WORKDIR /app
ENV UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy

# ── couche STABLE : les dépendances ──
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project --no-dev
#   --frozen : interdit de modifier le lock (échoue si désynchronisé)
#   --no-dev : pas de pytest/ruff dans l'image de production

# ── couche VOLATILE : le code ──
COPY src/ src/
RUN uv sync --frozen --no-dev

# ════════ étape 2 : image finale, sans outils de build ════════
FROM python:3.12-slim

WORKDIR /app
# on ne recopie que le résultat : l'environnement et le code
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/src /app/src
ENV PATH="/app/.venv/bin:$PATH"

# un utilisateur non privilégié : si l'application est compromise,
# l'attaquant n'est pas root dans le conteneur
RUN useradd --create-home app && chown -R app /app
USER app

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["uvicorn", "mon_projet.api:app", "--host", "0.0.0.0", "--port", "8000"]

La construction en deux étapes (multi-stage) est le point clé : compilateurs, caches et fichiers intermédiaires restent dans l'étape jetable, et seul le résultat utile est recopié dans l'image finale. On passe typiquement de 1,2 Go à 200 Mo — ce qui accélère les déploiements et réduit la surface d'attaque.

Deux détails qui comptent : --host 0.0.0.0 est obligatoire, car écouter sur 127.0.0.1 depuis un conteneur signifie « uniquement depuis l'intérieur du conteneur », et la redirection de port ne verrait rien. Et le fichier .dockerignore est indispensable :

.dockerignore
.git
.venv
data/
notebooks/
models/
__pycache__/
*.parquet
*.csv
.pytest_cache
.ruff_cache

Sans lui, la commande docker build envoie l'intégralité du répertoire au démon Docker avant même de commencer — y compris les 10 Go de données et l'historique Git.

6. Volumes : faire survivre les données

Un conteneur est éphémère : tout ce qu'il écrit dans son système de fichiers disparaît quand on le supprime. C'est voulu — un conteneur doit être jetable et remplaçable — mais il faut donc un mécanisme pour les données qui doivent survivre. Deux formes de montage :

bash — volume nommé et bind mount
# VOLUME NOMMÉ : géré par Docker, stocké dans son espace. Pour les DONNÉES.
docker volume create donnees-pg
docker run -d --name pg -v donnees-pg:/var/lib/postgresql/data postgres:16
#                       └─ le volume survit à la suppression du conteneur

docker volume ls
docker volume inspect donnees-pg
docker volume rm donnees-pg          # là seulement, les données disparaissent

# BIND MOUNT : un dossier de la MACHINE est monté dans le conteneur.
# Pour le DÉVELOPPEMENT : le code modifié est visible immédiatement.
docker run -it --rm -v "$(pwd)":/app -w /app python:3.12-slim python script.py

# montage en lecture seule : un jeu de données que le conteneur ne doit pas modifier
docker run -v /data/brut:/data:ro mon-image

La distinction pratique : volume nommé pour ce que produit l'application (base de données, modèles entraînés, fichiers téléversés) ; bind mount pour ce qui vient de la machine (code source en développement, données brutes en lecture seule). Et attention aux permissions : un fichier créé par le conteneur appartient à l'utilisateur du conteneur, ce qui peut donner des fichiers root sur la machine hôte.

7. Réseaux et variables d'environnement

Chaque conteneur reçoit sa propre interface réseau. Trois choses à savoir :

bash — réseaux
# créer un réseau : les conteneurs qui y sont attachés se voient PAR LEUR NOM
docker network create app-net

docker run -d --name base --network app-net postgres:16
docker run -d --name api  --network app-net -p 8000:8000 mon-api:1.0
# depuis "api", la base est joignable à l'adresse "base:5432" — Docker fournit
# une résolution DNS interne, il n'y a aucune adresse IP à connaître

# publier un port vers l'extérieur : -p HOTE:CONTENEUR
docker run -p 8080:8000 mon-api     # http://localhost:8080 → port 8000 du conteneur
docker run -p 127.0.0.1:8000:8000 mon-api   # accessible uniquement en local

docker network ls
docker network inspect app-net

Sans réseau explicite, les conteneurs sont sur le réseau bridge par défaut, où la résolution par nom ne fonctionne pas : c'est la cause la plus fréquente des « connection refused » entre deux conteneurs.

bash — configuration et secrets
docker run -e DATABASE_URL="postgresql://user:pass@base:5432/db" mon-api
docker run --env-file .env mon-api         # toutes les variables d'un fichier

# GPU (nécessite le NVIDIA Container Toolkit sur l'hôte)
docker run --gpus all nvidia/cuda:12.4.0-base nvidia-smi
Jamais de secret dans une image. Une clé d'API écrite dans un ENV ou copiée dans un fichier reste dans les couches de l'image : quiconque la télécharge peut l'extraire, même si une instruction ultérieure a « supprimé » le fichier — les couches précédentes sont toujours là. Les secrets passent par des variables d'environnement à l'exécution, un fichier monté, ou un gestionnaire de secrets. Et pour le GPU : le pilote reste sur la machine hôte, seules les bibliothèques CUDA sont dans l'image — c'est ce qui explique les incompatibilités de version entre image et pilote, comme dans mon projet NAS.

8. Docker Compose

À quoi ça sert

Une application réelle n'est jamais un seul conteneur : une API, une base de données, un cache, parfois un serveur de tâches de fond. Les lancer à la main impose de retenir l'ordre de démarrage, de créer le réseau, de nommer les volumes, de recopier les bonnes variables d'environnement — une longue série de commandes fragiles, à refaire à chaque redémarrage.

Docker Compose décrit toute cette pile dans un fichier unique, versionné avec le code. Une commande démarre l'ensemble, une autre l'arrête. C'est l'outil qui permet à quelqu'un de cloner un dépôt et d'avoir l'application complète qui tourne en une minute — y compris la base de données, avec le bon schéma.

compose.yaml — API + PostgreSQL + cache, commenté
services:

  api:
    build: .                        # construire depuis le Dockerfile local
    # image: ghcr.io/nils/mon-api:1.0   ← en production, on utilise une image publiée
    ports:
      - "8000:8000"                 # exposé sur la machine
    environment:
      DATABASE_URL: postgresql://app:${POSTGRES_PASSWORD}@base:5432/annonces
      REDIS_URL: redis://cache:6379/0
      LOG_LEVEL: info
    depends_on:
      base:
        condition: service_healthy  # attend que la base réponde VRAIMENT
      cache:
        condition: service_started
    volumes:
      - ./src:/app/src              # bind mount : rechargement à chaud en dev
    restart: unless-stopped         # redémarre en cas de crash, sauf arrêt manuel

  base:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}   # lu dans le fichier .env
      POSTGRES_DB: annonces
    volumes:
      - donnees-pg:/var/lib/postgresql/data     # LES DONNÉES SURVIVENT ICI
      - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro   # schéma au 1er démarrage
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d annonces"]
      interval: 5s
      timeout: 3s
      retries: 10
    # pas de "ports:" → la base n'est PAS exposée à l'extérieur, seulement à l'api

  cache:
    image: redis:7-alpine
    command: redis-server --save 60 1 --loglevel warning

volumes:
  donnees-pg:                       # volume nommé, géré par Docker
bash — les commandes de Compose
docker compose up -d              # démarre toute la pile en arrière-plan
docker compose up --build         # reconstruit les images avant de démarrer
docker compose ps                 # état des services
docker compose logs -f api        # suivre les journaux d'un service
docker compose exec api bash      # ouvrir un shell dans le service "api"
docker compose exec base psql -U app annonces      # un client SQL dans la base
docker compose restart api
docker compose stop               # arrête sans supprimer
docker compose down               # arrête ET supprime conteneurs + réseau
docker compose down -v            # … et SUPPRIME LES VOLUMES (donc les données)

docker compose run --rm api pytest         # lancer une commande ponctuelle
docker compose --profile dev up            # démarrer un sous-ensemble de services

Trois points méritent d'être soulignés dans ce fichier. Le healthcheck combiné à depends_on: condition: service_healthy évite l'erreur la plus courante : l'API démarre avant que PostgreSQL n'accepte les connexions et plante immédiatement. L'absence de section ports sur la base signifie qu'elle n'est joignable que depuis le réseau interne — une bonne pratique de sécurité, et suffisante puisque l'API l'atteint par le nom base. Enfin, le script monté dans /docker-entrypoint-initdb.d/ est exécuté automatiquement au tout premier démarrage : c'est ainsi qu'on crée le schéma SQL du module 4 sans intervention manuelle. C'est exactement la structure utilisée dans le projet final (module 28).

9. Kubernetes

Le problème que Compose ne résout pas

Compose orchestre des conteneurs sur une machine. Dès qu'il faut plusieurs machines, la question change de nature : que se passe-t-il si un serveur tombe ? Comment répartir la charge ? Comment déployer une nouvelle version sans coupure ? Comment redémarrer automatiquement ce qui a planté à 3 h du matin ?

Kubernetes (souvent abrégé « K8s ») répond à cela par un principe simple à énoncer : on lui décrit l'état souhaité — « je veux trois exemplaires de cette image, chacun avec 2 Go de mémoire, accessibles derrière une adresse stable » — et il s'emploie en permanence à faire correspondre l'état réel à cette description. Si un serveur meurt, il replace ailleurs les conteneurs perdus, sans intervention. C'est ce qu'on appelle une approche déclarative : on ne donne pas d'ordres, on décrit une cible.

L'architecture, en deux moitiés

de quoi est fait un cluster
PLAN DE CONTRÔLE (le cerveau)          NŒUDS DE TRAVAIL (les bras)
┌────────────────────────────┐         ┌──────────────┐  ┌──────────────┐
│ API server  ← tout passe   │         │   kubelet    │  │   kubelet    │
│              par lui       │◄───────►│      │       │  │      │       │
│ etcd        ← l'état       │         │   ┌──┴───┐   │  │   ┌──┴───┐   │
│              souhaité      │         │   │ Pod  │   │  │   │ Pod  │   │
│ scheduler   ← place les    │         │   │ Pod  │   │  │   │ Pod  │   │
│              pods          │         │   └──────┘   │  │   └──────┘   │
│ controllers ← corrigent    │         │ kube-proxy   │  │ kube-proxy   │
│              les écarts    │         └──────────────┘  └──────────────┘
└────────────────────────────┘

On parle au cluster via l'API server, presque toujours avec la commande kubectl. L'état souhaité est stocké dans etcd. Le scheduler décide sur quel nœud placer chaque pod en fonction des ressources demandées. Les contrôleurs comparent en boucle l'état réel à l'état souhaité et agissent pour combler l'écart — c'est là que se trouve toute l'intelligence du système.

Les objets, du plus petit au plus grand

Pod — la plus petite unité déployable. Un pod contient un ou plusieurs conteneurs qui partagent la même adresse IP et les mêmes volumes ; ils sont toujours placés ensemble sur le même nœud. Dans 90 % des cas, un pod = un conteneur ; les cas à plusieurs conteneurs servent au motif sidecar (un conteneur annexe qui collecte les journaux, par exemple). Point essentiel : un pod est jetable. Il n'a pas d'adresse stable, il peut être détruit et recréé ailleurs à tout moment. On ne crée donc jamais un pod à la main.

ReplicaSet — maintient un nombre donné de pods identiques. Si l'un disparaît, il en recrée un. On ne le manipule pas directement non plus.

Deployment — l'objet que l'on écrit réellement. Il gère un ReplicaSet et, surtout, les mises à jour progressives : lors d'un changement de version, il crée les nouveaux pods, attend qu'ils soient prêts, puis retire les anciens, un par un. Pas de coupure de service, et retour arrière en une commande.

Service — puisque les pods vont et viennent avec des adresses changeantes, le Service fournit une adresse et un nom stables devant un groupe de pods, et répartit la charge entre eux. Trois variantes : ClusterIP (joignable uniquement dans le cluster, le défaut), NodePort (ouvre un port sur chaque nœud), LoadBalancer (demande un répartiteur au fournisseur cloud).

Ingress — l'entrée HTTP depuis l'extérieur : routage par nom de domaine et par chemin, terminaison TLS. C'est le reverse proxy du cluster.

ConfigMap et Secret — la configuration, séparée de l'image : paramètres non sensibles pour le premier, identifiants et clés pour le second. Cela permet de déployer la même image en test et en production avec des réglages différents.

PersistentVolumeClaim — une demande de stockage durable, l'équivalent du volume nommé de Compose.

Job et CronJob — une tâche qui se termine (un entraînement, un import) et sa version planifiée (un ré-entraînement hebdomadaire, module 26).

Namespace — un cloisonnement logique du cluster, pour séparer les équipes ou les environnements.

HorizontalPodAutoscaler — ajuste automatiquement le nombre de répliques selon la charge CPU ou une métrique personnalisée.

deployment.yaml — un service d'inférence, commenté
apiVersion: apps/v1
kind: Deployment
metadata:
  name: model-api
  labels: { app: model-api }
spec:
  replicas: 3                          # trois exemplaires en permanence
  selector:
    matchLabels: { app: model-api }    # quels pods ce Deployment gère-t-il
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                      # au plus 1 pod en trop pendant la mise à jour
      maxUnavailable: 0                # aucun pod indisponible : zéro coupure
  template:                            # ── le modèle de pod à créer ──
    metadata:
      labels: { app: model-api }       # doit correspondre au selector ci-dessus
    spec:
      containers:
        - name: api
          image: ghcr.io/nils/model-api:1.4.2    # JAMAIS :latest en production
          ports:
            - containerPort: 8000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef: { name: db-credentials, key: url }
          resources:
            requests:                  # RÉSERVÉ : sert au scheduler pour le placement
              cpu: "500m"              # 500 millicœurs = 0,5 cœur
              memory: "1Gi"
            limits:                    # PLAFOND : au-delà, le conteneur est bridé (CPU)
              cpu: "2"                 #           ou tué (mémoire : OOMKilled)
              memory: "4Gi"
              nvidia.com/gpu: 1        # un GPU entier (non partageable par défaut)
          readinessProbe:              # "puis-je recevoir du trafic ?"
            httpGet: { path: /health, port: 8000 }
            initialDelaySeconds: 20    # laisse le temps de charger le modèle
            periodSeconds: 10
          livenessProbe:               # "suis-je encore vivant ?" sinon → redémarrage
            httpGet: { path: /health, port: 8000 }
            initialDelaySeconds: 60
            periodSeconds: 30
---
apiVersion: v1
kind: Service                          # l'adresse stable devant les 3 pods
metadata:
  name: model-api
spec:
  selector: { app: model-api }         # sélectionne les pods par leur étiquette
  ports:
    - port: 80                         # port du Service
      targetPort: 8000                 # port du conteneur
  type: ClusterIP

Les trois points qui comptent vraiment pour du ML

  • requests contre limits — la première est une réservation utilisée pour décider où placer le pod, la seconde un plafond appliqué à l'exécution. Un modèle qui charge 3 Go de poids doit le déclarer, sinon il sera tué (OOMKilled) dès le premier pic de charge.
  • readinessProbe — sans elle, Kubernetes envoie du trafic à un pod dont le modèle n'est pas encore chargé en mémoire, et les premières requêtes échouent à chaque déploiement.
  • livenessProbe — elle redémarre un conteneur bloqué. Mal réglée (délai trop court), elle redémarre en boucle un service qui démarre lentement : c'est la cause classique du CrashLoopBackOff.
bash — kubectl, les commandes utiles
kubectl apply -f deployment.yaml      # applique l'état souhaité (créé ou mis à jour)
kubectl get pods                      # lister ; -w pour suivre en direct
kubectl get all -n mon-namespace
kubectl describe pod model-api-7d9f   # événements : LA commande de diagnostic
kubectl logs -f model-api-7d9f        # journaux ; --previous après un crash
kubectl exec -it model-api-7d9f -- bash
kubectl port-forward svc/model-api 8080:80    # tester en local sans exposer

kubectl rollout status deploy/model-api       # suivre une mise à jour
kubectl rollout undo deploy/model-api         # RETOUR ARRIÈRE immédiat
kubectl scale deploy/model-api --replicas=5
kubectl top pods                              # consommation réelle
kubectl delete -f deployment.yaml

En avez-vous besoin ?

Honnêtement, rarement pour un projet personnel ou un prototype : la complexité opérationnelle est réelle, et un conteneur derrière un reverse proxy couvre la grande majorité des besoins. Kubernetes devient pertinent lorsqu'il faut plusieurs machines, de l'autoscaling, des déploiements sans coupure, ou le partage d'un parc de GPU entre plusieurs équipes. La valeur immédiate, pour un data scientist, est de savoir lire un manifeste et déclarer correctement ses ressources : c'est la langue commune des équipes plateforme. Pour s'exercer sans cluster, k3s, kind ou minikube font tourner un cluster complet sur un portable, et kubectl s'y comporte exactement de la même façon.

Ce qu'il faut retenir du module

  • Un conteneur est un processus isolé par le noyau de l'hôte (namespaces + cgroups) ; une VM embarque un noyau complet.
  • Image = modèle figé, conteneur = instance ; les couches sont mises en cache, d'où l'ordre du plus stable au plus volatil.
  • Construction en deux étapes, utilisateur non root, .dockerignore, jamais de secret dans l'image, --host 0.0.0.0.
  • Volume nommé pour les données produites, bind mount pour le code en développement.
  • Un réseau Docker explicite permet aux conteneurs de se joindre par leur nom.
  • Compose décrit une pile complète dans un fichier ; healthcheck + depends_on évitent les démarrages ratés.
  • Kubernetes maintient un état déclaré : Deployment pour les répliques et les mises à jour, Service pour l'adresse stable, probes et ressources pour un service de modèle qui tient debout.

DockerComposeKubernetesVolumeskubectl