← Toutes les fiches

Fiche 12 Parcours : DevOps Lecture : 30 minutes

Docker et l'orchestration

Empaqueter une application pour qu'elle tourne pareil partout, puis la faire vivre sur plusieurs serveurs.

« Ça marche sur ma machine. » Sur le serveur, la version de PHP est différente, une extension manque, la configuration n'est pas la même. Docker règle ce problème : il empaquette l'application avec tout ce dont elle a besoin pour tourner, et ce paquet s'exécute de la même façon sur ton poste, en intégration continue et en production.

Tu utilises déjà Docker dans ce projet : c'est lui qui exécute les exemples PHP des fiches et qui sert les pages. Cette fiche part de là, va jusqu'à une image prête pour la production, puis présente l'orchestration : faire tourner des conteneurs sur plusieurs serveurs, les redémarrer, les mettre à jour sans coupure. C'est la transition vers la fiche 13.

À propos des exemples de cette fiche

Les Dockerfile, les fichiers Compose, les manifestes Kubernetes et les commandes sont des illustrations : aucun n'a été exécuté ni construit pour préparer cette fiche, et aucune image n'a été téléchargée. Les noms d'images et les versions sont des exemples à adapter.

1.Image et conteneur

Deux mots reviennent sans cesse, et la comparaison avec PHP aide à les distinguer :

  1. Une image est un modèle figé : un système de fichiers minimal, PHP, les extensions, ton code, et la commande à lancer. C'est l'équivalent d'une classe.
  2. Un conteneur est une image en cours d'exécution, avec son propre état. C'est l'équivalent d'un objet : on peut lancer plusieurs conteneurs à partir de la même image.
  3. Un registre (Docker Hub, GitLab, GitHub, ou celui de ton hébergeur) stocke les images, comme Packagist stocke les paquets Composer.
Du Dockerfile aux conteneurs, en passant par le registre docker build transforme un Dockerfile en image. L'image est envoyée vers un registre ou récupérée depuis lui avec docker push et docker pull. docker run lance un ou plusieurs conteneurs à partir de la même image, comme on crée plusieurs objets à partir d'une classe. Dockerfile la recette docker build Image shop:1.8.2 comme une classe Registre Docker Hub, GitLab… push / pull conteneur 1 conteneur 2 docker run comme des objets
Schéma 12.1 · Du Dockerfile aux conteneurs, en passant par le registre

Un conteneur n'est pas une machine virtuelle. Une machine virtuelle simule un ordinateur complet, avec son propre système. Un conteneur partage le noyau Linux de l'hôte, et n'isole que ses processus, ses fichiers et son réseau. Il démarre donc en une fraction de seconde et pèse beaucoup moins lourd. En contrepartie, l'isolation est moins forte : c'est pourquoi la section 8 compte.

Machines virtuelles et conteneurs Chaque machine virtuelle embarque un système invité complet, au-dessus d'un hyperviseur. Les conteneurs, eux, partagent le noyau Linux du système hôte, et ne contiennent que l'application et ses dépendances : ils sont plus légers et démarrent plus vite. Machines virtuelles VM 1 PHP + appli système invité complet VM 2 PHP + appli système invité complet système hôte + hyperviseur matériel Conteneurs PHP + appli nginx PostgreSQL Docker système hôte : noyau Linux partagé matériel
Schéma 12.2 · Machines virtuelles et conteneurs

Une règle guide tout le reste : un conteneur est jetable. On le supprime et on en relance un autre à tout moment. Tout ce qui doit survivre (la base de données, les fichiers envoyés) vit en dehors, dans un volume ou un service externe.

2.Les commandes de base

exemples/12/commands.sh · illustration, non exécuté
docker run --rm -it php:8.4-cli php -v    # lancer un conteneur jetable, et le supprimer après
docker ps                                 # les conteneurs en cours (-a : aussi les arrêtés)
docker logs -f shop-php-1                 # suivre la sortie d'un conteneur
docker exec -it shop-php-1 sh             # ouvrir un terminal DANS un conteneur qui tourne
docker stop shop-php-1                    # l'arrêter
docker images                             # les images présentes sur la machine
docker system df                          # la place occupée par images, conteneurs, volumes

docker exec est précieux pour comprendre un problème : il ouvre un terminal dans le conteneur, où l'on retrouve les commandes de la fiche 11. Mais ne corrige jamais rien à la main dans un conteneur : la modification disparaît avec lui. On corrige l'image, puis on la reconstruit.

3.Écrire un Dockerfile

Une image se construit à partir d'un Dockerfile : une recette, lue de haut en bas.

exemples/12/Dockerfile.simple · illustration, non exécuté
# Une première image pour une application Symfony avec PHP-FPM
FROM php:8.4-fpm

# Les extensions PHP nécessaires (ici : PostgreSQL et OPcache)
RUN apt-get update \
    && apt-get install -y --no-install-recommends libpq-dev \
    && docker-php-ext-install pdo_pgsql opcache \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /var/www/app

# On copie d'abord les fichiers de dépendances, puis le reste du code :
# tant que composer.json ne change pas, cette étape reste en cache
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction

COPY . .

D'où l'ordre du fichier : on copie composer.json et composer.lock, on installe les dépendances, et seulement ensuite on copie le code. Modifier un contrôleur ne relance pas composer install, et la construction prend quelques secondes au lieu de quelques minutes.

Le fichier .dockerignore exclut ce qui ne doit pas entrer dans l'image. C'est aussi une protection : sans lui, COPY . . embarquerait ton .env.local et tout l'historique Git.

exemples/12/dockerignore · illustration, non exécuté
# .dockerignore : ce qui ne doit JAMAIS entrer dans l'image
.git
.env.local
.env.*.local
var/
vendor/
node_modules/
tests/

4.Une image de production

L'image de la section 3 fonctionne, mais contient Composer, qui n'a rien à faire en production, et tourne en root. Une image de production suit quelques règles de plus :

exemples/12/Dockerfile.prod · illustration, non exécuté
# Étape 1 : installer les dépendances, avec Composer (cet outil ne finira pas dans l'image)
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction \
        --optimize-autoloader

# Étape 2 : l'image finale, avec seulement ce qui sert à faire tourner l'application
FROM php:8.4-fpm AS app

RUN apt-get update \
    && apt-get install -y --no-install-recommends libpq-dev \
    && docker-php-ext-install pdo_pgsql opcache \
    && rm -rf /var/lib/apt/lists/*

COPY docker/php/prod.ini /usr/local/etc/php/conf.d/zz-prod.ini

WORKDIR /var/www/app
COPY --from=vendor /app/vendor ./vendor
COPY --chown=www-data:www-data . .

ENV APP_ENV=prod APP_DEBUG=0
RUN php bin/console cache:warmup && chown -R www-data:www-data var/

# Ne pas tourner en root : une faille dans l'application ne donne pas root dans le conteneur
USER www-data
  1. La construction en plusieurs étapes (multi-stage build) : une première étape installe les dépendances avec Composer, la seconde ne récupère que le dossier vendor/. L'image finale est plus petite, et contient moins d'outils qu'un attaquant pourrait utiliser.
  2. Un utilisateur sans privilèges (USER www-data) : une faille dans l'application ne donne pas les droits root dans le conteneur.
  3. Des versions précises : php:8.4-fpm plutôt que php:latest. Avec latest, la même recette peut produire une image différente d'un jour à l'autre.
  4. Le cache Symfony préparé à la construction (cache:warmup), avec APP_ENV=prod et APP_DEBUG=0 (fiche 10).
  5. Une image par version, étiquetée avec un numéro (shop:1.8.2) ou l'identifiant du commit Git. C'est ce qui rend le retour en arrière possible (fiche 13).
Ne pas tout réinventer

Le modèle symfony-docker, recommandé par la documentation de Symfony, fournit une configuration complète et maintenue, basée sur FrankenPHP (un serveur d'application PHP qui remplace nginx et PHP-FPM). Pour un nouveau projet, partir de ce modèle est souvent plus sûr que d'écrire son Dockerfile de zéro. Comprendre les sections précédentes reste indispensable pour l'adapter et le dépanner.

5.Les volumes : ce qui doit survivre

Tout ce qu'un conteneur écrit dans son propre système de fichiers disparaît quand on le supprime. Pour garder des données, on monte un volume dans le conteneur :

exemples/12/volumes.sh · illustration, non exécuté
# Volume nommé : géré par Docker, il survit à la suppression du conteneur
docker volume create shop_db_data
docker run -d --name db -v shop_db_data:/var/lib/postgresql/data postgres:17

# Montage d'un dossier de l'hôte (« bind mount ») : pratique en développement,
# le code modifié sur ta machine est visible tout de suite dans le conteneur
docker run --rm -v "$PWD":/app -w /app php:8.4-cli php bin/console about

docker volume ls                 # lister les volumes
Quand ne pas mettre une base de données dans Docker

En développement, PostgreSQL dans un conteneur est parfait. En production, une base gérée par l'hébergeur (sauvegardes automatiques, mises à jour, réplication) est souvent plus sûre qu'une base dans un conteneur que tu dois surveiller et sauvegarder toi-même (fiche 13). Si tu la gardes dans Docker, ses données doivent être sur un volume, et sauvegardées.

6.Les réseaux et les ports

Les conteneurs d'un même réseau Docker se parlent directement, en utilisant le nom du service comme nom d'hôte : depuis le conteneur PHP, la base de données s'appelle db. Docker joue le rôle d'un petit DNS (fiche 9).

Pour qu'un service soit joignable depuis l'extérieur de Docker, il faut publier un port : "8080:80" relie le port 8080 de la machine au port 80 du conteneur.

Le réseau d'une application Compose Seul nginx publie un port, lié à 127.0.0.1. PHP et PostgreSQL ne sont joignables que depuis le réseau interne de Docker, par leur nom de service. Publier le port 5432 de la base l'exposerait à tout le réseau, même si le pare-feu ufw affiche le port comme fermé. Navigateur La machine (ton poste ou le serveur) Réseau Docker interne 127.0.0.1:8000 nginx php db PostgreSQL port publié php:9000 db:5432 « 5432:5432 » ouvrirait la base à tout le réseau, même si ufw affiche le port comme fermé.
Schéma 12.3 · Le réseau d'une application Compose
Sécurité : un port publié est ouvert sur Internet
  1. "5432:5432" publie le port sur toutes les interfaces de la machine : si le serveur est sur Internet, ta base de données aussi.
  2. Docker modifie lui-même les règles réseau du système, et contourne le pare-feu ufw (fiche 11) : ufw affiche le port comme fermé alors qu'il est ouvert.
  3. La parade : ne publie que ce qui doit être public (le reverse proxy, ports 80 et 443), et lie le reste à la machine locale avec "127.0.0.1:8080:80", comme le fait le compose.yaml de ce projet. La base de données, elle, n'a besoin d'aucun port publié : les autres services y accèdent par le réseau interne.

7.Docker Compose

Une application réelle a plusieurs conteneurs : PHP, nginx, la base, parfois Redis ou un gestionnaire de messages. Docker Compose les décrit dans un seul fichier, et les démarre ensemble. Voici celui de ce projet de formation, que tu utilises déjà :

exemples/12/compose_formation.yaml · illustration, non exécuté
# Le compose.yaml de CE projet de formation, pour commencer simplement
services:
  php:
    image: php:8.4-cli
    working_dir: /app
    volumes:
      - .:/app                          # le dossier du projet, monté dans le conteneur
    user: "${UID:-1000}:${GID:-1000}"   # les fichiers créés gardent ton utilisateur
    profiles: [tools]                   # ne démarre pas avec « docker compose up »

  web:
    image: nginx:1.27-alpine
    volumes:
      - ./fiches:/usr/share/nginx/html:ro   # :ro = en lecture seule
    ports:
      - "127.0.0.1:8080:80"             # visible seulement depuis ta machine

Et celui d'une application Symfony plus complète :

exemples/12/compose_app.yaml · illustration, non exécuté
# compose.yaml d'une application Symfony : PHP-FPM, nginx et PostgreSQL
services:
  php:
    build: .
    # DATABASE_URL pointe vers « db », le nom du service : postgresql://app:…@db:5432/app
    env_file: .env.docker.local         # hors de Git : contient les vraies valeurs
    depends_on:
      db:
        condition: service_healthy      # attendre que PostgreSQL soit vraiment prêt

  nginx:
    image: nginx:1.27-alpine
    volumes:
      - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./public:/var/www/app/public:ro
    ports:
      - "127.0.0.1:8000:80"
    depends_on:
      - php

  db:
    image: postgres:17
    environment:
      POSTGRES_USER: app
      POSTGRES_DB: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "app"]
      interval: 5s
      retries: 10
    # Pas de « ports » : la base n'est joignable QUE par les autres services

volumes:
  db_data:

secrets:
  db_password:
    file: ./secrets/db_password.txt     # hors de Git (.gitignore)
  1. Chaque service part d'une image (image:) ou d'un Dockerfile (build:).
  2. depends_on avec service_healthy attend que PostgreSQL réponde vraiment, grâce à son healthcheck, et pas seulement que son conteneur ait démarré.
  3. La base n'a pas de ports : elle n'est joignable que par les autres services.
  4. Les valeurs sensibles restent hors de Git, dans .env.docker.local et dans le dossier secrets/ (section 8).
exemples/12/compose_commands.sh · illustration, non exécuté
docker compose up -d              # construire si besoin, puis démarrer en arrière-plan
docker compose ps                 # l'état de chaque service
docker compose logs -f php        # suivre les logs d'un service
docker compose exec php php bin/console doctrine:migrations:migrate
docker compose down               # arrêter et supprimer les conteneurs (les volumes restent)
docker compose down --volumes     # … et les volumes : les DONNÉES sont perdues
Compose en production ?

Pour une application sur un seul serveur, Compose en production est un choix raisonnable et simple à comprendre. Ses limites apparaissent quand il faut plusieurs serveurs, des mises à jour sans aucune coupure ou un redémarrage automatique sur une autre machine en cas de panne : c'est le rôle de l'orchestration (section 9).

8.Secrets et sécurité des conteneurs

Une image se partage : elle est poussée dans un registre, téléchargée sur plusieurs serveurs, parfois rendue publique par erreur. Rien de secret ne doit y entrer.

exemples/12/bad_secret.dockerfile · illustration, non exécuté
# ✗ À NE JAMAIS FAIRE : le secret est gravé dans l'image
ENV STRIPE_SECRET_KEY=exemple-de-cle-fictive
ARG DATABASE_PASSWORD
RUN echo "$DATABASE_PASSWORD" > /tmp/pass && ./configure.sh && rm /tmp/pass
# Même supprimé par « rm », le fichier reste lisible dans la couche précédente,
# et « docker history » montre les ENV et les ARG à quiconque possède l'image.
  1. Les secrets arrivent au démarrage, jamais à la construction : par des variables d'environnement, ou mieux, par des fichiers montés (secrets: de Compose, Secret de Kubernetes). Un fichier monté n'apparaît pas dans docker inspect, contrairement à une variable d'environnement.
  2. Une couche est éternelle : supprimer un fichier dans une instruction suivante ne l'efface pas des couches précédentes.
Sécurité : les autres règles des conteneurs

9.L'orchestration : quand un serveur ne suffit plus

Avec Compose sur un seul serveur, plusieurs questions restent sans réponse : que se passe-t-il si le serveur tombe ? Comment absorber un pic de trafic ? Comment mettre à jour sans couper le site ? Un orchestrateur répond à ces questions. On lui décrit l'état voulu (« trois copies de l'application, toujours en marche »), et il fait en permanence ce qu'il faut pour l'atteindre :

  1. Placer les conteneurs sur les serveurs disponibles, selon les ressources libres.
  2. Réparer : redémarrer un conteneur qui plante, le relancer ailleurs si un serveur disparaît.
  3. Répartir le trafic entre les copies, et n'envoyer des requêtes qu'à celles qui sont prêtes.
  4. Mettre à jour progressivement : remplacer les copies une par une, et revenir en arrière si les nouvelles ne démarrent pas.
  5. Adapter le nombre de copies à la charge.

Kubernetes est devenu l'orchestrateur de référence, et presque tous les fournisseurs de cloud en proposent une version gérée. Docker Swarm, intégré à Docker, est plus simple mais beaucoup moins utilisé. Les plateformes d'hébergement spécialisées (les « PaaS ») font souvent ce travail sans qu'on voie l'orchestrateur.

Quand ne pas orchestrer

Kubernetes demande d'apprendre beaucoup de notions, de maintenir un cluster (ou de le payer), et de surveiller davantage de choses. Pour une application qui tient sur un serveur, avec un peu de coupure acceptable lors d'un déploiement, un serveur avec Compose, ou une plateforme d'hébergement, coûte bien moins cher en temps et en argent. L'orchestration se justifie par un vrai besoin de disponibilité ou de montée en charge, pas parce que « tout le monde le fait ».

10.Kubernetes, les notions clés

Kubernetes a son vocabulaire. Voici les notions qu'on rencontre dès le premier jour :

NotionCe que c'estL'équivalent Compose
Cluster et nœudL'ensemble des machines, et chacune d'ellesLe serveur
PodLa plus petite unité : un ou plusieurs conteneurs qui vivent ensembleUn conteneur
Deployment« Je veux N copies de ce pod, dans cette version »Un service, avec replicas
ServiceUne adresse stable qui répartit le trafic entre les podsLe nom du service sur le réseau
IngressL'entrée HTTP(S) depuis l'extérieur, par nom de domaineLe reverse proxy et ses ports publiés
ConfigMap et SecretLa configuration et les secrets, injectés dans les podsenvironment, env_file, secrets
Sondes (probes)Les questions « es-tu vivant ? » et « es-tu prêt ? »healthcheck
Kubernetes : de l'Ingress aux pods Le trafic venu d'Internet entre par l'Ingress, qui le passe au Service. Le Service répartit les requêtes entre trois pods identiques, placés sur deux nœuds du cluster. Seuls les pods dont la sonde dit « prêt » reçoivent du trafic. Internet Ingress HTTPS, shop.example.com Service shop répartit le trafic Cluster Nœud 1 Nœud 2 pod shop:1.8.2 pod shop:1.8.2 pod shop:1.8.2 prêt ✓ vivant ✓ prêt ✓ vivant ✓
Schéma 12.4 · Kubernetes : de l'Ingress aux pods

Tout se décrit en YAML, dans des manifestes. Voici une application en trois copies :

exemples/12/deployment.yaml · illustration, non exécuté
# Kubernetes : « je veux 3 copies de mon application, toujours en marche »
apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop
spec:
  replicas: 3
  selector:
    matchLabels:
      app: shop
  strategy:
    type: RollingUpdate              # remplacer les copies une par une, sans coupure
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  template:
    metadata:
      labels:
        app: shop
    spec:
      containers:
        - name: app
          image: registry.example.com/shop:1.8.2   # une version précise, jamais « latest »
          ports:
            - containerPort: 8080
          envFrom:
            - secretRef:
                name: shop-secrets   # les secrets, injectés au démarrage
          readinessProbe:            # « prêt à recevoir du trafic ? »
            httpGet:
              path: /health
              port: 8080
          livenessProbe:             # « encore vivant ? » sinon, redémarré
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 10
          resources:
            requests: { cpu: 250m, memory: 256Mi }   # ce qu'il faut réserver
            limits: { memory: 512Mi }                # ce qu'il ne doit pas dépasser
  1. replicas: 3 : Kubernetes garde trois pods en marche. Si un pod plante, il est remplacé.
  2. La mise à jour progressive (RollingUpdate) : à chaque nouvelle version, un pod neuf démarre, et un ancien n'est retiré qu'une fois le neuf prêt. Aucune coupure, à condition que l'application le supporte (les migrations de base compatibles, fiche 13).
  3. La sonde readinessProbe : tant qu'elle échoue, le pod ne reçoit pas de trafic. La sonde livenessProbe : si elle échoue, le pod est redémarré. Une route /health légère (fiche 13) répond aux deux.
  4. Les ressources : requests réserve de la place sur un nœud, limits plafonne la mémoire. Sans limite, un pod qui fuit de la mémoire peut gêner tous ses voisins.
  5. L'image a une version précise. Revenir en arrière, c'est redéployer la version précédente.
Une mise à jour progressive, pod par pod Trois pods tournent en version 1. Un pod en version 2 démarre ; une fois prêt, un pod en version 1 est retiré. L'opération se répète jusqu'à ce que les trois pods soient en version 2. À chaque instant, au moins trois pods prêts reçoivent le trafic. t0 v1 v1 v1 état de départ t1 v1 v1 v1 v2… un pod v2 démarre (maxSurge: 1) t2 v1 v1 v2 v2 est prêt : un v1 est retiré t3 v1 v2 v2 on recommence t4 v2 v2 v2 terminé, sans coupure À chaque instant, au moins 3 pods prêts reçoivent le trafic (maxUnavailable: 0).
Schéma 12.5 · Une mise à jour progressive, pod par pod

Le Service et l'Ingress font entrer le trafic jusqu'aux pods :

exemples/12/service_ingress.yaml · illustration, non exécuté
# Service : une adresse stable devant les copies, qui répartit les requêtes
apiVersion: v1
kind: Service
metadata:
  name: shop
spec:
  selector:
    app: shop
  ports:
    - port: 80
      targetPort: 8080
---
# Ingress : faire entrer le trafic HTTPS de l'extérieur vers le Service
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop
spec:
  tls:
    - hosts: [shop.example.com]
      secretName: shop-tls
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: shop
                port:
                  number: 80

Comme l'image doit servir directement du HTTP pour que les sondes et le Service fonctionnent simplement, on utilise souvent FrankenPHP (section 4), ou un pod qui contient deux conteneurs : nginx et PHP-FPM.

Sécurité : ce qui change avec Kubernetes

Les Secret de Kubernetes ne sont, par défaut, qu'encodés en base64, pas chiffrés : quiconque peut lire les manifestes ou l'API du cluster les lit. Limite strictement les droits d'accès au cluster, ne commite jamais un manifeste Secret avec ses valeurs, et utilise un gestionnaire de secrets (celui du fournisseur de cloud, ou un outil dédié) quand c'est possible.

La suite logique, déployer une nouvelle version, la surveiller et revenir en arrière, est l'objet de la fiche 13.

Pièges fréquents

PiègeCe qui arriveLa parade
Modifier un conteneur à la mainLa correction disparaît au prochain redémarrageCorriger le Dockerfile et reconstruire
Des données dans le conteneur, sans volumePerdues à la suppression du conteneurUn volume, ou un service externe
COPY . . avant composer installChaque modification réinstalle toutes les dépendancesCopier composer.* d'abord
Pas de .dockerignore.env.local et .git dans l'imageUn .dockerignore dès le départ
Un secret dans ENV ou ARGLisible par quiconque possède l'imageSecrets injectés au démarrage
Un conteneur qui tourne en rootUne faille donne root dans le conteneurUSER sans privilèges
L'étiquette latestUne image différente d'un jour à l'autre, retour en arrière impossibleDes versions précises
"5432:5432" sur un serveur publicLa base exposée sur Internet, malgré ufwPas de port publié, ou 127.0.0.1:
docker compose down --volumes par réflexeLes données de la base sont effacéesdown seul, sauf intention claire
Kubernetes pour un petit projetBeaucoup de complexité pour peu de bénéficeCompose sur un serveur, ou une plateforme d'hébergement
Pas de sondes dans KubernetesDu trafic envoyé à des pods pas encore prêtsreadinessProbe et livenessProbe

Checklist d'une image de production

Conseils

  1. Pars du compose.yaml de ce projet. Lis-le ligne par ligne avec cette fiche : chaque ligne y a une raison d'être.
  2. Utilise docker compose exec pour comprendre, jamais pour réparer.
  3. Regarde la taille et l'historique d'une image avec docker images et docker history : ils montrent ce qui l'alourdit, et ce qu'elle révèle.
  4. Apprends Kubernetes seulement quand un projet en a besoin, et de préférence avec une version gérée par un hébergeur. Les notions de la section 10 suffisent pour lire des manifestes et discuter avec une équipe qui s'en occupe.
  5. Garde le développement et la production proches : la même image de base, les mêmes versions. C'est tout l'intérêt de Docker.

Documentation officielle