Retour au cours

infra / docker-compose

Scaling de services

Leçon 101 exercice

Explication

Ce que vous allez apprendre

  • Distinguer le scaling vertical (plus de ressources) du scaling horizontal (plus d'instances)
  • Lancer plusieurs instances identiques d'un service avec --scale ou deploy.replicas
  • Comprendre pourquoi un service scalé ne peut pas publier de port hôte fixe
  • Utiliser expose et un reverse proxy pour router le trafic vers plusieurs instances
  • Savoir que le DNS interne Docker fait du round-robin entre les replicas d'un service

Dans quel contexte ?

Le service worker du projet boutique-api, qui traite les images uploadées par les clients, commence à accumuler un retard visible dans sa file d'attente aux heures de pointe. Plutôt que de lui donner plus de CPU (scaling vertical, limité), l'équipe veut faire tourner cinq exemplaires identiques de ce worker en parallèle, qui se répartissent la charge automatiquement. C'est exactement ce que permet docker compose up --scale worker=5.

Le problème : une seule instance ne suffit plus

D'abord, imagine un service qui traite de plus en plus de requêtes. À un moment, une seule instance devient un goulot d'étranglement : elle sature, et les temps de réponse augmentent.

Deux façons de répondre à ce problème

Il existe deux approches. La première, le scaling vertical, consiste à rendre une seule instance plus puissante (plus de CPU, plus de mémoire). La seconde, le scaling horizontal, consiste à faire tourner PLUSIEURS instances identiques en parallèle, pour répartir la charge entre elles.

Ce que Compose permet de simuler

Compose permet de tester ce second comportement facilement en local, avec l'option --scale ou la clé deploy.replicas. On obtient alors plusieurs conteneurs identiques du même service.

Il reste un problème : le port fixe

Une fois qu'on a plusieurs instances (replicas) d'un service, un problème apparaît vite. Chaque replica est un conteneur séparé. Si on essaie de publier le même port hôte fixe, comme 3000:3000, pour chacun, ils entrent tous en conflit pour occuper la même adresse sur la machine.

La solution : exposer sans publier, et passer par un proxy

La solution consiste à ne PAS publier de port fixe sur le service scalé, en utilisant expose à la place. Un reverse proxy unique, lui, expose un seul port public et répartit ensuite le trafic vers les différentes instances.

Comment le trafic est réellement réparti

Maintenant que le proxy sait qu'il doit parler à plusieurs instances, comment fait-il ? Docker résout le nom d'un service scalé vers TOUTES les IP de ses replicas : c'est le DNS round-robin qui répartit naturellement les connexions, sans configuration supplémentaire.

ApprocheCe qu'elle changeLimite
Scaling verticalPlus de CPU/RAM sur une seule instancePlafonné par la machine hôte
Scaling horizontalPlusieurs instances identiques en parallèleNécessite un point d'entrée unique (proxy)

Le piège à connaître

Publier un port fixe sur un service scalé empêche tout simplement le démarrage au-delà d'une instance : Compose refuse de créer un deuxième conteneur qui voudrait occuper le même port.

Piège fréquent

Déclarer ports: - "3000:3000" sur un service avec deploy.replicas: 3 échoue au démarrage du deuxième conteneur avec une erreur "port is already allocated". Utilise expose à la place et laisse un reverse proxy (nginx, Traefik) publier le seul port réellement accessible depuis l'extérieur.

Ne pas confondre avec un vrai load balancing de production

Ce mécanisme reste un outil de test local. Pour un vrai load balancing en production, avec un contrôle fin de la santé des instances et une répartition pondérée, il faut un reverse proxy dédié comme nginx ou Traefik — ce qu'on abordera plus tard dans ce cours.

La prochaine leçon change de sujet : protéger les données sensibles, comme les mots de passe, avec les secrets Compose.

Commandes & code

Scaling de services

Compose peut lancer plusieurs instances (replicas) d'un même service, utile pour tester la répartition de charge en local.

yaml
services:
  worker:
    build: ./worker
    command: ["node", "worker.js"]
    deploy:
      replicas: 3            # utilisé par "docker compose up" (hors Swarm aussi, v2)
    depends_on:
      - queue

  api:
    build: ./api
    # Pas de "ports" fixe ici : avec plusieurs replicas, un port fixe entrerait en conflit
    expose:
      - "3000"

  nginx:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - api
nginx
# nginx.conf - répartit la charge entre les instances "api" via le DNS round-robin de Docker
upstream api_backend {
    server api:3000;   # Compose résout ce nom vers TOUTES les IP des replicas
}

server {
    listen 80;
    location / {
        proxy_pass http://api_backend;
    }
}
bash
# Démarrer avec un nombre de replicas donné en ligne de commande (surcharge "deploy.replicas")
docker compose up -d --scale worker=5 --scale api=3

# Vérifier les instances créées (suffixe -1, -2, -3...)
docker compose ps

# Erreur classique : "--scale" incompatible avec un "ports" fixe (host:container)
# -> utiliser "expose" ou un port dynamique côté hôte

Résumé

  • --scale <service>=N ou deploy.replicas crée plusieurs instances d'un service.
  • Un service scalé ne doit pas publier de port hôte fixe (ports:) sans plage dynamique.
  • Le DNS interne Docker fait du round-robin entre les replicas d'un même nom de service.
  • Pour un vrai load balancing en prod, préférer un reverse proxy (nginx/traefik) devant.

Exercices pratiques

1 disponible
1

Mission : scaler l'API sans faire exploser les ports

Objectif : Corriger un conflit de port sur un service scalé et router le trafic via un reverse proxy.

Contexte

Le service worker de boutique-api accumule du retard aux heures de pointe. L'équipe veut lancer docker compose up -d --scale api=3, mais le service api déclare encore ports: - "3000:3000". Elle veut aussi comprendre comment un nginx frontal pourra router le trafic vers les trois instances sans connaître leurs IP.

Corrige la configuration pour rendre le scaling possible.

Résoudre l’exercice →