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 Swarm | Kubernetes (k3s, k8s) | |
|---|---|---|
| Courbe d'apprentissage | Quelques heures | Plusieurs semaines |
| Fichiers de déploiement | Docker Compose (que vous connaissez déjà) | Manifests YAML spécifiques, Helm |
| Taille idéale | 2 à 20 nœuds, quelques dizaines de services | Des centaines de nœuds et de services |
| Écosystème | Modeste, stable | Immense, en mouvement constant |
| Autoscaling, opérateurs, CRD | Non | Oui |
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.
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 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
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: hostsur 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 de10.0.0.x, ce qui rend les logs et les limitations de débit inutilisables.mode: global+ contrainterole == proxyplace 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 :
- É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.
- 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. - 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"
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
| Action | Commande |
|---|---|
| État des nœuds | docker node ls |
| Services et répliques | docker service ls |
| Où tourne chaque conteneur | docker service ps <service> |
| Logs d'un service (tous nœuds) | docker service logs -f <service> |
| Déployer / mettre à jour une stack | docker stack deploy -c fichier.yml <stack> |
| Supprimer une stack | docker stack rm <stack> |
| Inspecter l'échec d'une tâche | docker 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,
pingentre les IP privées - UFW : ports Swarm (2377, 7946, 4789) autorisés uniquement depuis
10.10.0.0/24 -
swarm initetjoinavec--advertise-addret--data-path-addrsur 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 -
healthchecketupdate_configavecorder: start-firstsur chaque service web - Services à état épinglés à un nœud ou sur stockage adapté
- Sauvegarde de
/var/lib/docker/swarmet 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.
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.