Aller au contenu principal

Vous suivez ce tutoriel et vous n'avez pas encore de VPS ? Nos VPS Linux sont disponibles à partir de 2,99€/mois avec support inclus.

Voir nos VPS

Déployer Docker Swarm sur un cluster de VPS

Vous avez un ou plusieurs projets qui tournent en Docker Compose sur un VPS, et vous commencez à sentir la limite : un redémarrage du serveur coupe tout, mettre à jour une application signifie quelques secondes d'indisponibilité, et vous n'avez aucun moyen simple de répartir la charge sur une deuxième machine.

Docker Swarm est la réponse la plus simple à ce problème. Il est intégré à Docker, se configure en trois commandes, et réutilise vos fichiers Compose presque tels quels. Ce guide monte un cluster de trois VPS YorkHost (un manager, deux workers) avec un réseau privé chiffré, un reverse proxy Traefik qui gère les certificats automatiquement, et tout ce qu'il faut pour l'exploiter au quotidien.

Swarm ou Kubernetes ?

La question revient systématiquement, alors tranchons-la d'emblée.

Docker SwarmKubernetes (k3s, k8s)
Courbe d'apprentissageQuelques heuresPlusieurs semaines
Fichiers de déploiementDocker Compose (que vous connaissez déjà)Manifests YAML spécifiques, Helm
Taille idéale2 à 20 nœuds, quelques dizaines de servicesDes centaines de nœuds et de services
ÉcosystèmeModeste, stableImmense, en mouvement constant
Autoscaling, opérateurs, CRDNonOui

Pour une équipe de 1 à 5 personnes qui veut de la résilience et des déploiements sans coupure sans y consacrer un poste à temps plein, Swarm est le bon choix. Si vous visez des centaines de microservices ou un écosystème d'outils cloud-native, partez directement sur Kubernetes.

Architecture cible

                    Internet

┌────────┴────────┐
│ Anti-DDoS YorkHost │
└────────┬────────┘
┌───────────────┼───────────────┐
│ │ │
┌────┴─────┐ ┌─────┴────┐ ┌─────┴────┐
│ swarm-1 │ │ swarm-2 │ │ swarm-3 │
│ manager │ │ worker │ │ worker │
│ Traefik │ │ apps │ │ apps │
└────┬─────┘ └─────┬────┘ └─────┬────┘
└───────────────┴───────────────┘
Réseau privé WireGuard 10.10.0.0/24
(trafic Swarm + overlay chiffré)
  • swarm-1 est le manager : il maintient l'état du cluster (base Raft) et planifie les conteneurs. Il héberge aussi Traefik, point d'entrée HTTP/HTTPS unique.
  • swarm-2 et swarm-3 sont des workers : ils exécutent les applications.
  • Les trois communiquent par un tunnel WireGuard : le trafic de gestion du cluster ne transite jamais en clair sur Internet.
Pourquoi trois VPS chez le même hébergeur ?

Un cluster Swarm échange en permanence des battements de cœur et du trafic réseau overlay entre ses nœuds. Trois VPS Linux YorkHost dans le même datacenter se parlent avec une latence inférieure à la milliseconde sur un réseau 1 à 10 Gbps, ce qui rend l'overlay quasi transparent. Chaque nœud bénéficie de l'Anti-DDoS Stormwall & Gcore inclus, et des sauvegardes 7 jours glissants. Pour des workloads exigeants, la gamme VPS Ultimate sur NVMe et réseau 10 Gbps est particulièrement adaptée aux nœuds qui hébergent des bases de données.

Prérequis

  • Trois VPS Debian 12 ou Ubuntu 24.04, VPS-4 minimum pour les workers, VPS-8 recommandé pour le manager si Traefik et des applications y tournent
  • Accès root ou sudo sur chacun, SSH sécurisé par clé
  • Un nom de domaine dont vous contrôlez la zone DNS
  • Les IP publiques des trois VPS ; dans ce guide : 203.0.113.11, 203.0.113.12, 203.0.113.13

Étape 1 : Préparer chaque VPS

Sur les trois machines :

sudo apt update && sudo apt upgrade -y
sudo hostnamectl set-hostname swarm-1 # swarm-2, swarm-3 sur les autres
sudo timedatectl set-timezone Europe/Paris

Installez Docker Engine avec le dépôt officiel (la version Debian est trop ancienne). La procédure complète est dans Installer Docker ; en résumé :

sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
docker --version

Sur Ubuntu, remplacez debian par ubuntu dans les deux URL.

Étape 2 : Réseau privé WireGuard entre les nœuds

Swarm chiffre son plan de contrôle (Raft, TLS mutuel), mais pas le trafic réseau entre conteneurs par défaut, et ses ports de gestion ne devraient de toute façon jamais être accessibles depuis Internet. Un tunnel WireGuard règle les deux points en une fois : tout le trafic Swarm passe par des adresses privées 10.10.0.x.

Sur les trois machines :

sudo apt install -y wireguard
wg genkey | sudo tee /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key
sudo chmod 600 /etc/wireguard/private.key
cat /etc/wireguard/public.key

Notez les trois clés publiques. Puis créez la configuration sur swarm-1 :

sudo nano /etc/wireguard/wg0.conf
[Interface]
Address = 10.10.0.1/24
ListenPort = 51820
PrivateKey = <clé privée de swarm-1>

[Peer]
# swarm-2
PublicKey = <clé publique de swarm-2>
AllowedIPs = 10.10.0.2/32
Endpoint = 203.0.113.12:51820
PersistentKeepalive = 25

[Peer]
# swarm-3
PublicKey = <clé publique de swarm-3>
AllowedIPs = 10.10.0.3/32
Endpoint = 203.0.113.13:51820
PersistentKeepalive = 25

Sur swarm-2 (adaptez pour swarm-3 avec 10.10.0.3 et les bons pairs) :

[Interface]
Address = 10.10.0.2/24
ListenPort = 51820
PrivateKey = <clé privée de swarm-2>

[Peer]
# swarm-1
PublicKey = <clé publique de swarm-1>
AllowedIPs = 10.10.0.1/32
Endpoint = 203.0.113.11:51820
PersistentKeepalive = 25

[Peer]
# swarm-3
PublicKey = <clé publique de swarm-3>
AllowedIPs = 10.10.0.3/32
Endpoint = 203.0.113.13:51820
PersistentKeepalive = 25

Activez le tunnel partout et testez :

sudo chmod 600 /etc/wireguard/wg0.conf
sudo systemctl enable --now wg-quick@wg0
sudo wg show
ping -c 2 10.10.0.2 # depuis swarm-1

Étape 3 : Firewall

Le principe : Internet ne voit que SSH, HTTP, HTTPS et WireGuard. Tout le reste passe par le tunnel. Sur chaque nœud :

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # ou votre port SSH personnalisé
sudo ufw allow 51820/udp # WireGuard

# Ports Swarm, uniquement depuis le réseau privé
sudo ufw allow from 10.10.0.0/24 to any port 2377 proto tcp # gestion du cluster
sudo ufw allow from 10.10.0.0/24 to any port 7946 proto tcp # découverte des nœuds
sudo ufw allow from 10.10.0.0/24 to any port 7946 proto udp
sudo ufw allow from 10.10.0.0/24 to any port 4789 proto udp # réseau overlay VXLAN

sudo ufw enable

Sur swarm-1 uniquement (Traefik) :

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Docker et UFW

Docker manipule directement iptables et contourne UFW pour les ports publiés avec -p ou ports:. C'est pour cela que dans ce guide, seul Traefik publie des ports (80 et 443) ; les applications restent sur le réseau overlay interne, jamais exposées directement. Si vous publiez un port d'application « pour tester », sachez qu'il sera ouvert sur Internet même si UFW dit le contraire.

Étape 4 : Initialiser le cluster

Sur swarm-1, en indiquant explicitement l'adresse privée :

sudo docker swarm init --advertise-addr 10.10.0.1 --listen-addr 10.10.0.1:2377 --data-path-addr 10.10.0.1

Le paramètre --data-path-addr est important : il force le trafic overlay entre conteneurs à passer par WireGuard plutôt que par les IP publiques.

La commande affiche un jeton de jonction pour les workers. Vous pouvez le réafficher à tout moment :

sudo docker swarm join-token worker

Sur swarm-2 et swarm-3 :

sudo docker swarm join --token SWMTKN-1-xxxxxxxx --advertise-addr 10.10.0.2 --data-path-addr 10.10.0.2 10.10.0.1:2377
# (10.10.0.3 sur swarm-3)

Vérifiez depuis le manager :

sudo docker node ls
ID          HOSTNAME   STATUS   AVAILABILITY   MANAGER STATUS   ENGINE VERSION
abc123 * swarm-1 Ready Active Leader 27.x
def456 swarm-2 Ready Active 27.x
ghi789 swarm-3 Ready Active 27.x
Un ou trois managers ?

Le manager stocke l'état du cluster dans une base Raft. Avec un seul manager, sa perte gèle le cluster (les conteneurs continuent de tourner, mais plus aucun déploiement ni réparation automatique jusqu'à sa restauration). Avec trois managers, le cluster tolère la perte d'un nœud. Ne mettez jamais deux managers : deux ne valent pas mieux qu'un (il faut une majorité pour décider), et quatre ne valent pas mieux que trois.

Pour promouvoir les workers en managers quand vous serez prêt : docker node promote swarm-2 swarm-3. Sur un petit cluster, les managers peuvent aussi exécuter des applications, c'est le comportement par défaut.

Étiqueter les nœuds

Les labels permettent de contraindre certains services à certains nœuds (une base de données sur le nœud qui a le disque NVMe, par exemple) :

sudo docker node update --label-add role=proxy swarm-1
sudo docker node update --label-add role=app swarm-2
sudo docker node update --label-add role=app swarm-3
sudo docker node update --label-add storage=nvme swarm-2

Étape 5 : Le réseau overlay et Traefik

Créez un réseau overlay chiffré partagé par Traefik et vos applications. Avec WireGuard, le chiffrement overlay est une seconde couche ; il coûte un peu de CPU, gardez-le si vos nœuds venaient un jour à communiquer hors tunnel.

sudo docker network create --driver overlay --attachable --opt encrypted proxy

Traefik v3 détecte les services Swarm et obtient automatiquement des certificats Let's Encrypt. Créez le fichier de stack sur le manager :

sudo mkdir -p /opt/swarm/traefik && cd /opt/swarm/traefik
sudo touch acme.json && sudo chmod 600 acme.json
sudo nano traefik.yml
services:
traefik:
image: traefik:v3.1
command:
- "--providers.swarm=true"
- "--providers.swarm.endpoint=unix:///var/run/docker.sock"
- "--providers.swarm.exposedByDefault=false"
- "--providers.swarm.network=proxy"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.email=vous@example.com"
- "--certificatesresolvers.le.acme.storage=/acme.json"
- "--certificatesresolvers.le.acme.tlschallenge=true"
- "--api.dashboard=true"
- "--log.level=INFO"
ports:
- target: 80
published: 80
mode: host
- target: 443
published: 443
mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /opt/swarm/traefik/acme.json:/acme.json
networks:
- proxy
deploy:
mode: global
placement:
constraints:
- node.labels.role == proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=le"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=dash-auth"
# Générez le hash avec : htpasswd -nb admin 'motdepasse' | sed -e 's/\$/\$\$/g'
- "traefik.http.middlewares.dash-auth.basicauth.users=admin:$$apr1$$xxxxxxxx$$yyyyyyyyyyyyyyyyyyyyyy"
- "traefik.http.services.dashboard.loadbalancer.server.port=9999"

networks:
proxy:
external: true

Deux détails qui comptent :

  • mode: host sur les ports fait que Traefik reçoit l'IP réelle du visiteur. Avec le mode par défaut (ingress), toutes les requêtes semblent venir de 10.0.0.x, ce qui rend les logs et les limitations de débit inutilisables.
  • mode: global + contrainte role == proxy place exactement une instance de Traefik sur chaque nœud étiqueté proxy. Pour l'instant swarm-1, et il suffira d'étiqueter un second nœud (et de pointer un enregistrement DNS supplémentaire) pour avoir deux points d'entrée.

Déployez :

sudo docker stack deploy -c traefik.yml traefik
sudo docker service ls
sudo docker service logs -f traefik_traefik

Créez l'enregistrement DNS traefik.example.com → 203.0.113.11 et ouvrez le tableau de bord en HTTPS après une minute, le temps que Let's Encrypt délivre le certificat.

Étape 6 : Déployer une première application

Prenons une application web avec sa base de données. Le fichier est du Docker Compose classique, plus une section deploy par service :

sudo mkdir -p /opt/swarm/monapp && cd /opt/swarm/monapp
sudo nano monapp.yml
services:
web:
image: ghcr.io/votre-org/monapp:1.4.2
environment:
DATABASE_URL: postgres://monapp:motdepasse@db:5432/monapp
networks:
- proxy
- internal
deploy:
replicas: 3
placement:
constraints:
- node.labels.role == app
update_config:
parallelism: 1
delay: 10s
order: start-first
failure_action: rollback
rollback_config:
parallelism: 1
order: start-first
restart_policy:
condition: on-failure
max_attempts: 3
resources:
limits:
cpus: "1.0"
memory: 512M
labels:
- "traefik.enable=true"
- "traefik.http.routers.monapp.rule=Host(`app.example.com`)"
- "traefik.http.routers.monapp.entrypoints=websecure"
- "traefik.http.routers.monapp.tls.certresolver=le"
- "traefik.http.services.monapp.loadbalancer.server.port=3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 20s

db:
image: postgres:17
environment:
POSTGRES_DB: monapp
POSTGRES_USER: monapp
POSTGRES_PASSWORD: motdepasse
volumes:
- db_data:/var/lib/postgresql/data
networks:
- internal
deploy:
replicas: 1
placement:
constraints:
- node.labels.storage == nvme # toujours sur swarm-2, là où sont ses données

networks:
proxy:
external: true
internal:
driver: overlay
driver_opts:
encrypted: "true"

volumes:
db_data:
sudo docker stack deploy -c monapp.yml monapp
sudo docker stack services monapp
sudo docker service ps monapp_web

Ce qui se passe : trois répliques de web sont réparties sur swarm-2 et swarm-3, Traefik les découvre et équilibre la charge entre elles, la base tourne sur le seul nœud qui a son volume. Si un conteneur web plante ou si swarm-3 tombe, Swarm relance les répliques manquantes ailleurs.

Mise à jour sans coupure

Changez le tag d'image dans le fichier, puis redéployez la même commande :

sudo docker stack deploy -c monapp.yml monapp
sudo docker service ps monapp_web # observez le remplacement progressif

Grâce à order: start-first et parallelism: 1, chaque nouvelle réplique démarre et passe son healthcheck avant que l'ancienne soit arrêtée. Si la nouvelle version échoue, failure_action: rollback remet l'ancienne automatiquement. Pour revenir en arrière manuellement :

sudo docker service rollback monapp_web

Ajuster le nombre de répliques

sudo docker service scale monapp_web=5

Étape 7 : Secrets et configurations

Le mot de passe de la base en clair dans le YAML de l'exemple précédent est acceptable pour un test, pas pour la production. Swarm dispose d'un mécanisme de secrets chiffrés dans Raft et montés dans les conteneurs sous forme de fichiers :

openssl rand -base64 32 | sudo docker secret create db_password -
sudo docker secret ls

Dans la stack :

services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password

secrets:
db_password:
external: true

La plupart des images officielles acceptent une variable *_FILE. Pour vos propres applications, lisez /run/secrets/<nom> au démarrage.

Le même mécanisme existe pour les fichiers de configuration non sensibles (docker config create nginx_conf ./nginx.conf), très pratique pour distribuer une configuration Nginx ou un fichier .env sur tous les nœuds sans volume partagé.

Étape 8 : Le problème des volumes

C'est le point le plus important à comprendre avec Swarm. Un volume Docker est local au nœud. Si la base de données de l'exemple est replanifiée sur un autre nœud, elle y démarrera avec un volume vide. D'où la contrainte de placement storage == nvme qui la cloue sur swarm-2.

Trois approches selon vos besoins :

  1. Épingler le service à un nœud (ce que fait le guide). Simple, performant, mais ce nœud devient un point de défaillance pour ce service. Acceptable si vous avez de bonnes sauvegardes.
  2. Stockage réseau partagé : un export NFS depuis un VPS de stockage monté sur tous les nœuds, avec un volume de type nfs. Fonctionne bien pour des fichiers (uploads, médias), mal pour des bases de données.
  3. Réplication au niveau applicatif : PostgreSQL avec réplication en flux, MariaDB Galera, Redis Sentinel. Chaque instance a son propre volume local, c'est l'application qui assure la redondance. C'est la solution robuste, et c'est du travail.

Exemple de volume NFS pour des fichiers partagés, avec un VPS Storage YorkHost comme serveur NFS accessible via le tunnel WireGuard :

volumes:
uploads:
driver: local
driver_opts:
type: nfs
o: addr=10.10.0.10,rw,nfsvers=4,soft,timeo=30
device: ":/srv/nfs/uploads"
Ne réinventez pas la haute disponibilité des bases

Si votre base de données est ce qui compte le plus dans votre projet, sachez qu'un cluster PostgreSQL réellement tolérant aux pannes implique bien plus qu'une réplica : élection de leader, protection contre le split-brain, routage des connexions. Nous avons détaillé le sujet dans PostgreSQL en haute disponibilité, et c'est précisément ce que l'Infrastructure Managée YorkHost opère pour vous.

Étape 9 : Exploitation au quotidien

Commandes essentielles

ActionCommande
État des nœudsdocker node ls
Services et répliquesdocker service ls
Où tourne chaque conteneurdocker service ps <service>
Logs d'un service (tous nœuds)docker service logs -f <service>
Déployer / mettre à jour une stackdocker stack deploy -c fichier.yml <stack>
Supprimer une stackdocker stack rm <stack>
Inspecter l'échec d'une tâchedocker service ps --no-trunc <service>

Maintenance d'un nœud

Avant de redémarrer un VPS pour une mise à jour du noyau, videz-le : Swarm déplace ses conteneurs ailleurs proprement.

sudo docker node update --availability drain swarm-3
# ... mise à jour, redémarrage ...
sudo docker node update --availability active swarm-3

Sauvegarder le cluster

L'état du cluster (services, secrets, configurations, réseaux) vit dans /var/lib/docker/swarm sur les managers. Sauvegardez-le régulièrement, il permet de reconstruire un manager perdu :

sudo systemctl stop docker
sudo tar -czf /var/backups/swarm-$(date +%F).tar.gz /var/lib/docker/swarm
sudo systemctl start docker

Et surtout, sauvegardez les données des volumes (dump de la base, fichiers) hors du cluster. Le guide Backup automatique offsite avec Restic ou Borg s'applique directement.

Supervision

Au minimum, un docker service ls dans un cron qui alerte si REPLICAS affiche 2/3. Pour aller plus loin, une stack Prometheus + Grafana + cAdvisor ou Netdata déployée en mode global couvre tous les nœuds ; voir Monitoring serveur. Portainer (édition Community) offre aussi une interface graphique complète pour Swarm.

Nettoyage

Les images des anciennes versions s'accumulent sur chaque nœud. Un cron hebdomadaire sur chaque VPS :

0 4 * * 0 docker system prune -af --filter "until=168h" > /dev/null 2>&1

Checklist de mise en production

  • Docker installé depuis le dépôt officiel sur tous les nœuds, même version
  • WireGuard actif, ping entre les IP privées
  • UFW : ports Swarm (2377, 7946, 4789) autorisés uniquement depuis 10.10.0.0/24
  • swarm init et join avec --advertise-addr et --data-path-addr sur les IP privées
  • Traefik déployé en mode: host, certificats Let's Encrypt obtenus
  • Aucune application ne publie de port directement ; tout passe par Traefik
  • Mots de passe dans des secrets, pas dans le YAML
  • healthcheck et update_config avec order: start-first sur chaque service web
  • Services à état épinglés à un nœud ou sur stockage adapté
  • Sauvegarde de /var/lib/docker/swarm et des données des volumes hors du cluster
  • Trois managers si le cluster doit survivre à la perte d'un nœud

Dépannage

Un worker apparaît Down alors que le VPS est allumé

Le tunnel WireGuard est tombé ou UFW bloque un port. sudo wg show sur les deux nœuds (le champ latest handshake doit dater de moins de 2 minutes), puis sudo ufw status numbered. Un systemctl restart wg-quick@wg0 puis systemctl restart docker sur le worker suffit généralement.

Les conteneurs de nœuds différents ne se voient pas

Le port 4789/udp (VXLAN) est bloqué, ou le --data-path-addr n'a pas été fourni au join. Vérifiez avec docker node inspect swarm-2 --format '{{.Status.Addr}}' que l'adresse est bien la privée. Si ce n'est pas le cas, faites quitter le nœud (docker swarm leave dessus, docker node rm sur le manager) et refaites le join avec les bons paramètres.

Une tâche reste en Pending

docker service ps --no-trunc <service> affiche la raison : contrainte de placement impossible à satisfaire (label manquant), ressources insuffisantes, ou image introuvable depuis ce nœud (registre privé sans identifiants : ajoutez --with-registry-auth au stack deploy).

Traefik ne délivre pas de certificat

Le DNS ne pointe pas encore vers swarm-1, ou le port 80 n'est pas accessible (Let's Encrypt doit pouvoir joindre le serveur). docker service logs traefik_traefik | grep -i acme donne la cause exacte. Vérifiez aussi que acme.json est en chmod 600, sinon Traefik refuse de l'utiliser.

Le manager est perdu

Avec un seul manager, restaurez la sauvegarde de /var/lib/docker/swarm sur un nouveau VPS ayant la même IP privée, puis docker swarm init --force-new-cluster --advertise-addr 10.10.0.1. Les workers se reconnectent seuls. C'est la raison de passer à trois managers dès que le projet le justifie.

Cluster prêt en quelques minutes

Les VPS Linux YorkHost sont livrés en quelques minutes, sans engagement, tous dans le même datacenter avec un réseau interne rapide. Vous pouvez donc commencer avec un cluster de trois petits nœuds et ajouter des workers au fil de la croissance du projet, sans réinstaller quoi que ce soit : un swarm join et le nouveau nœud reçoit ses premiers conteneurs. Si vous préférez une machine physique complète pour héberger vos propres VM, le guide Proxmox et KVM sur serveur dédié montre comment créer vos nœuds vous-même.