Vous avez un NAS Synology, un client BitTorrent (Transmission) et un abonnement VPN. Vous voulez que vos torrents passent par le VPN, et uniquement eux. Le reste du NAS — DSM, DDNS, QuickConnect, Secure SignIn, vos autres services — doit continuer à sortir avec votre IP normale.
Ce tutoriel explique comment y parvenir avec trois conteneurs Docker : gluetun (le tunnel VPN + pare-feu), Transmission (le client torrent) et autoheal (la rustine qui rend l’ensemble fiable dans la durée).
Pourquoi pas le VPN intégré de DSM ?
DSM propose nativement un client VPN (Panneau de configuration → Réseau → Interface réseau). Le problème : une fois connecté, ce VPN devient la route par défaut de tout le NAS. Conséquences vécues :
- le DDNS enregistre l’adresse IP du serveur VPN au lieu de votre IP publique → vos accès externes pointent dans le vide ;
- Secure SignIn et QuickConnect deviennent erratiques ;
- tous les services du NAS sortent par le VPN, même ceux qui n’en ont aucun besoin.
Le routage sélectif (« policy routing ») est possible en bidouillant les tables de routage à la main, mais c’est fragile et écrasé aux mises à jour de DSM. La bonne solution, c’est d’isoler le VPN dans un conteneur.
Le principe
┌─────────────────────────── NAS Synology ───────────────────────────┐ │ │ │ DSM, DDNS, autres services ──────────────► Internet (IP normale) │ │ │ │ ┌────────── réseau du conteneur gluetun ──────────┐ │ │ │ │ │ │ │ transmission ──► gluetun ──► tunnel VPN ───────┼─► Internet │ │ │ (kill switch) │ (IP du VPN) │ │ └─────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────────┘
- gluetun est le seul conteneur à monter le tunnel VPN. Il intègre un pare-feu strict : si le tunnel tombe, rien ne sort. C’est un kill switch natif, aucune fuite d’IP possible.
- Transmission n’a pas de réseau à lui : grâce à
network_mode: "service:gluetun", il partage la pile réseau de gluetun. Tout son trafic passe donc obligatoirement par le tunnel, sans rien configurer côté Transmission. - autoheal surveille la santé de Transmission et le redémarre automatiquement quand il perd le réseau (on verra plus bas pourquoi ça arrive forcément un jour).
Prérequis
- Un Synology avec Container Manager installé (Centre de paquets) ;
- Un abonnement VPN fournissant une configuration OpenVPN (fichier
.ovpn+ identifiants). Ici, l’exemple utilise un fournisseur en mode « custom », mais gluetun connaît nativement des dizaines de fournisseurs (NordVPN, ProtonVPN, Mullvad, Surfshark…) — dans ce cas, la configuration est encore plus simple ; - Un accès SSH au NAS (ou l’éditeur de fichiers de File Station).
1. L’arborescence
Créez un dossier de projet dans le partage docker :
/volume1/docker/transmission-vpn/ ├── docker-compose.yml ├── gluetun/ │ ├── custom.ovpn # config OpenVPN du fournisseur │ ├── openvpn_user # identifiant VPN (une ligne, rien d'autre) │ └── openvpn_password # mot de passe VPN (une ligne, rien d'autre) └── config/ # config de Transmission (à créer vide)
mkdir -p /volume1/docker/transmission-vpn/{gluetun,config}
⚠️ Créez bien le dossier config/ avant le premier lancement : contrairement au Docker standard qui crée silencieusement les dossiers manquants d’un bind mount, celui de Synology refuse de démarrer le conteneur avec l’erreur Bind mount failed: '…/config' does not exist.
Les identifiants sont dans des fichiers séparés plutôt qu’en variables d’environnement dans le compose. Deux raisons : on peut versionner ou partager le docker-compose.yml sans exposer de secret, et on évite les mauvaises surprises de l’interpolation docker-compose (un $ ou un caractère spécial dans un mot de passe passé en variable d’environnement peut être interprété silencieusement).
chmod 600 gluetun/openvpn_user gluetun/openvpn_password
2. Préparer le fichier .ovpn (fournisseur « custom »)
Deux pièges qui font perdre des heures :
Piège n° 1 — utilisez une IP, pas un nom de domaine. Dans le fichier .ovpn, la ligne remote contient généralement un nom d’hôte :
remote monfournisseur-vpn.example.net 443
Au démarrage, gluetun verrouille son pare-feu avant d’établir le tunnel — et à ce stade, la résolution DNS peut échouer ou être bloquée. Remplacez le nom par son adresse IP :
nslookup monfournisseur-vpn.example.net # puis dans custom.ovpn : remote 203.0.113.37 443
Si un jour le conteneur devient unhealthy sans raison apparente, re-résolvez le nom : le fournisseur a probablement changé l’IP du serveur.
Piège n° 2 — supprimez la ligne auth-user-pass du .ovpn. Gluetun injecte lui-même cette directive avec le chemin de ses fichiers d’identifiants. Si votre .ovpn contient déjà un auth-user-pass nu (sans chemin), OpenVPN tente de demander les identifiants de façon interactive dans un conteneur sans terminal → échec immédiat.
3. Le docker-compose.yml
Voici le fichier complet, commenté ensuite bloc par bloc :
services:
gluetun:
image: qmcgaw/gluetun
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=custom
- VPN_TYPE=openvpn
- OPENVPN_CUSTOM_CONFIG=/gluetun/custom.ovpn
- OPENVPN_USER_SECRETFILE=/gluetun/openvpn_user
- OPENVPN_PASSWORD_SECRETFILE=/gluetun/openvpn_password
- FIREWALL_INPUT_PORTS=9091
- FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24,172.18.0.0/16
- TZ=Europe/Paris
volumes:
- ./gluetun:/gluetun
ports:
- "9091:9091"
restart: unless-stopped
transmission:
image: lscr.io/linuxserver/transmission:latest
container_name: transmission
network_mode: "service:gluetun"
depends_on:
- gluetun
environment:
- PUID=1026 # UID : le propriétaire des fichiers téléchargés (voir ci-dessous)
- PGID=100 # GID : le groupe de ces fichiers
- USER=admin # identifiant de connexion à l'interface web (port 9091)
- PASS=ChangezMoi # mot de passe de l'interface web — se change ICI, pas dans settings.json
- TZ=Europe/Paris
volumes:
- ./config:/config
- /volume1/downloads:/volume1/downloads
labels:
- autoheal=true
healthcheck:
test: ["CMD-SHELL", "curl -sf -m 8 -o /dev/null https://api.ipify.org || exit 1"]
interval: 30s
timeout: 12s
retries: 2
start_period: 60s
restart: unless-stopped
autoheal:
image: willfarrell/autoheal:latest
container_name: autoheal
environment:
- AUTOHEAL_CONTAINER_LABEL=autoheal
- AUTOHEAL_INTERVAL=15
- AUTOHEAL_START_PERIOD=45
- TZ=Europe/Paris
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
Bloc gluetun : le tunnel et le pare-feu
cap_add: NET_ADMINet/dev/net/tun: indispensables pour créer une interface VPN dans un conteneur.FIREWALL_INPUT_PORTS=9091: le pare-feu de gluetun bloque tout par défaut, y compris en entrée. Cette ligne autorise l’accès à l’interface web de Transmission (port 9091). C’est sur gluetun que le port est publié (ports:), pas sur Transmission — logique, puisque Transmission vit dans le réseau de gluetun.FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24,172.18.0.0/16: autorise le trafic vers le réseau local en dehors du tunnel. Sans cette ligne, le NAS lui-même ne peut pas joindre le port publié — symptôme typique : un reverse proxy DSM devant Transmission qui renvoie une erreur 502. Adaptez192.168.1.0/24à votre réseau local (le172.18.0.0/16correspond au réseau Docker par défaut du projet).
Bloc transmission : tout passe par le tunnel
network_mode: "service:gluetun": la ligne magique de tout le montage. Transmission n’a aucune interface réseau propre, il utilise celles de gluetun. Impossible de contourner le VPN, même par erreur de configuration.PUID/PGID: un conteneur n’a pas ses propres utilisateurs — le processus Transmission écrit sur le disque du NAS avec un numéro d’utilisateur (UID) et de groupe (GID). Ces deux variables déterminent donc à qui appartiendront les fichiers téléchargés dans/volume1/downloads— et donc qui pourra les lire, les déplacer ou les supprimer ensuite (via File Station, SMB, etc.). Pour récupérer les vôtres, en SSH sur le NAS :$ id votreuser uid=1026(votreuser) gid=100(users) groups=100(users),101(administrators)
Reportez le
uiddansPUIDet legiddansPGID. Sur un Synology, le premier utilisateur créé porte généralement l’UID1026et appartient au groupeusers(GID100) — d’où les valeurs de l’exemple. Si vous migrez depuis une installation Transmission existante, utilisez plutôt l’UID/GID de l’ancien utilisateur (visible avecls -lan /volume1/downloads) pour reprendre vos fichiers sans toucher aux permissions.USER/PASS: les identifiants de connexion à l’interface web de Transmission. Attention, piège de l’image linuxserver — au démarrage, elle réécrit la configuration RPC de Transmission. Si ces deux variables sont absentes, elle désactive purement et simplement l’authentification de l’interface web, même si vous l’aviez activée danssettings.json. C’est donc ici, et uniquement ici, qu’on définit ou change ces identifiants.- Le
healthcheckmérite une explication à part (section suivante).
Bloc autoheal : la fiabilité dans la durée
Le talon d’Achille de network_mode: "service:gluetun" : quand gluetun redémarre (mise à jour d’image, incident, reboot partiel), son espace réseau est détruit puis recréé. Transmission, lui, reste attaché à l’ancien espace réseau, désormais orphelin : le processus tourne, le conteneur est « Up », mais il n’a plus aucune connectivité. Docker ne le redémarre pas tout seul, puisque de son point de vue tout va bien.
La solution tient en deux morceaux :
- Un healthcheck qui teste la connectivité externe :
curl https://api.ipify.org. Piège subtil : un healthcheck surhttp://localhost:9091ne détecte pas le problème, car le loopback continue de fonctionner dans un espace réseau orphelin. Il faut tester une sortie réelle vers Internet. - autoheal, un micro-conteneur qui surveille le socket Docker et redémarre automatiquement tout conteneur portant le label
autoheal=truedès qu’il passeunhealthy.
Résultat : après un redémarrage de gluetun, Transmission retrouve son réseau tout seul en ~90 secondes, sans intervention.
4. Lancement et vérification
cd /volume1/docker/transmission-vpn docker compose up -d
(Sur Synology, le binaire complet est /var/packages/ContainerManager/target/usr/bin/docker si docker n’est pas dans votre PATH. Vous pouvez aussi importer le projet dans l’interface de Container Manager.)
Note sur les images : l’onglet « Registre » de Container Manager ne cherche que sur Docker Hub. Vous y trouverez qmcgaw/gluetun et willfarrell/autoheal, mais pas lscr.io/linuxserver/transmission, hébergée sur le registre de linuxserver (lscr.io) : inutile de la chercher dans l’interface, elle n’y apparaîtra jamais. Ce n’est pas un problème — docker compose up -d télécharge chaque image directement depuis le bon registre, celui indiqué dans son nom complet. C’est une des raisons de préférer la ligne de commande à l’interface pour ce projet.
La vérification qui compte — comparer l’IP de sortie du conteneur et celle du NAS :
# IP vue par Transmission (doit être celle du serveur VPN) docker exec transmission curl -s https://api.ipify.org # IP vue par le NAS (doit être votre IP publique normale) curl -s https://api.ipify.org
Si les deux adresses diffèrent, mission accomplie. Testez aussi le kill switch :
docker stop gluetun docker exec transmission curl -s -m 5 https://api.ipify.org # doit échouer docker start gluetun # ~90 s plus tard, autoheal a redémarré transmission, tout refonctionne
L’interface web est accessible sur http://ip-du-nas:9091 avec les identifiants USER/PASS du compose.
Si l’interface ne répond pas du tout (connexion refusée, alors que le conteneur est healthy) : regardez config/settings.json. Sur une installation neuve, l’image linuxserver génère "rpc-bind-address": "[::]" — une adresse d’écoute IPv6. Or gluetun désactive l’IPv6 dans son espace réseau quand le tunnel n’en fournit pas : le bind échoue en silence et rien n’écoute sur 9091 (vérifiable avec docker exec transmission netstat -tln : le port pair 51413 est là, pas le 9091). Le correctif :
docker stop transmission sed -i 's/"rpc-bind-address": "\[::\]"/"rpc-bind-address": "0.0.0.0"/' config/settings.json docker start transmission
Ce réglage n’est pas réécrit par l’image au démarrage (contrairement à USER/PASS), la correction est donc définitive.
5. Mises à jour
Les tags :latest ne se mettent pas à jour tout seuls. Un petit script suffit :
#!/bin/bash # Met à jour les images de la pile transmission-vpn set -uo pipefail cd /volume1/docker/transmission-vpn || exit 1 DOCKER=/var/packages/ContainerManager/target/usr/bin/docker $DOCKER compose pull $DOCKER compose up -d $DOCKER image prune -f $DOCKER compose ps
La configuration (docker-compose.yml, gluetun/, config/) n’est jamais touchée par une mise à jour d’images. Et si la mise à jour redémarre gluetun — c’est le cas — autoheal se charge de remettre Transmission sur pied.
Récapitulatif des pièges
| Piège | Symptôme | Solution |
|---|---|---|
| VPN système DSM | DDNS/QuickConnect/Secure SignIn cassés | VPN conteneurisé (ce tutoriel) |
remote avec un nom d’hôte |
gluetun ne démarre pas ou devient unhealthy | Mettre l’IP en dur dans le .ovpn |
auth-user-pass dans le .ovpn |
OpenVPN demande les identifiants et plante | Supprimer la ligne, gluetun gère |
| Restart de gluetun | Transmission « Up » mais sans réseau | healthcheck externe + autoheal |
Healthcheck sur localhost:9091 |
Passe au vert même sans réseau | Tester une URL externe (api.ipify.org) |
USER/PASS absents du compose |
Interface web sans authentification | Toujours les définir dans le compose |
| Reverse proxy → 502 | Le NAS ne joint pas le port publié | FIREWALL_OUTBOUND_SUBNETS |
| Secrets en variables d’environnement | Interpolation $ imprévisible |
OPENVPN_*_SECRETFILE |
Dossier config/ absent |
Bind mount failed au premier lancement |
mkdir config avant le up -d (Docker Synology ne le crée pas) |
rpc-bind-address "[::]" (install neuve) |
Conteneur healthy mais interface web injoignable | Mettre 0.0.0.0 dans settings.json |
| Mêmes identifiants VPN sur deux machines | Déconnexions en boucle (ping-restart) toutes les ~2 min |
Un jeu d’identifiants (ou au moins un serveur) par machine |
Conclusion
Trois conteneurs, un seul fichier compose, et un cloisonnement propre : les torrents passent par le VPN avec un kill switch garanti, le NAS garde son IP publique pour tout le reste, et l’ensemble survit aux redémarrages et aux mises à jour sans intervention. Exactement ce que le client VPN intégré de DSM ne sait pas faire.