Installer wg-easy WireGuard avec Docker Compose
Déployez WireGuard avec l’interface web wg-easy dans Docker Compose : ports, NET_ADMIN, sysctls requis et QR codes pour connecter vos téléphones.
Ce que vous allez mettre en place
wg-easy est WireGuard avec une interface web, exécuté dans un seul conteneur Docker. Il gère l’interface WireGuard à votre place et ajoute une interface web pour créer des clients. Chaque client créé reçoit un fichier de configuration et un QR code. Un téléphone rejoint ainsi le VPN en visant l’écran avec son appareil photo.
Le tunnel lui-même est un WireGuard standard. Le module du noyau achemine les paquets. Le débit est donc identique à celui d’une configuration écrite manuellement. Vous gagnez la gestion du cycle de vie des clients : ajout, désactivation et suppression des peers sans modifier un fichier de configuration en SSH. En contrepartie, vous perdez le contrôle direct de ce fichier. Ce point est traité dans la configuration manuelle de WireGuard sur un VPS.
Vous avez besoin d’un hôte Linux que vous pouvez administrer, par exemple un VPS KVM, avec une adresse IP publique ou un nom de domaine qui pointe vers celui-ci, un processeur x86_64 ou arm64, Docker Engine avec le plugin Compose, et un accès root. La virtualisation de conteneurs qui partage le noyau de l’hôte, comme OpenVZ ou LXC, ne peut généralement pas charger le module WireGuard. Le conteneur échoue alors à démarrer l’interface.
La version 15 a déplacé les paramètres hors de l’environnement
La plupart des guides disponibles ont été écrits pour wg-easy 14. Ils indiquent de définir WG_HOST avec l’adresse de votre serveur et PASSWORD_HASH avec un hash bcrypt du mot de passe administrateur, tous deux comme variables d’environnement. La version 15 est une réécriture. Les notes officielles de migration indiquent clairement que v15 n’utilise pas les mêmes variables d’environnement que v14 et que la plupart d’entre elles ont été déplacées dans le panneau d’administration de l’interface web.
WG_HOST et PASSWORD_HASH n’ont donc plus aucun effet. Si vous copiez un ancien fichier Compose, le conteneur démarre, ignore ces lignes, puis vous demande de créer un compte administrateur dans le navigateur. Ce n’est pas un bug. C’est le nouveau processus de configuration.
En octobre 2026, le tag majeur à utiliser est 15. Épinglez la version majeure au lieu d’utiliser latest, car une mise à niveau majeure modifie le format de la configuration sur disque et ne permet pas un retour arrière propre. Le passage à la version 15 a remplacé le fichier JSON de v14 par une base de données SQLite.
Le fichier compose
Créez un répertoire pour la stack et écrivez-y le fichier compose officiel. Il s’agit du fichier upstream, sans les lignes facultatives commentées.
sudo mkdir -p /etc/docker/containers/wg-easy
sudo curl -o /etc/docker/containers/wg-easy/docker-compose.yml \
https://raw.githubusercontent.com/wg-easy/wg-easy/master/docker-compose.ymlLe contenu ressemble à ceci :
volumes:
etc_wireguard:
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy:15
container_name: wg-easy
networks:
wg:
ipv4_address: 10.42.42.42
ipv6_address: fdcc:ad94:bacf:61a3::2a
volumes:
- etc_wireguard:/etc/wireguard
- /lib/modules:/lib/modules:ro
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv6.conf.all.forwarding=1
- net.ipv6.conf.default.forwarding=1
networks:
wg:
driver: bridge
enable_ipv6: true
ipam:
driver: default
config:
- subnet: 10.42.42.0/24
- subnet: fdcc:ad94:bacf:61a3::/64etc_wireguard est un volume nommé qui contient la clé du serveur et tous les clients que vous créez, enregistrés dans la base SQLite wg-easy.db. Sauvegardez ce volume, sinon une reconstruction supprime tous vos peers. Si vous préférez voir ces fichiers dans le système de fichiers de l’hôte, remplacez-le par un bind mount et lisez la différence entre les bind mounts et les volumes nommés avant de le faire, car les permissions se comportent différemment.
Pourquoi le conteneur a besoin de NET_ADMIN, de SYS_MODULE et de ces paramètres sysctl
Par défaut, un conteneur ne peut pas modifier la pile réseau. Chacune de ces lignes lève un blocage précis.
NET_ADMIN permet au conteneur de créer l’interface wg0, de lui attribuer une adresse et d’ajouter des routes. Sans cette capability, le conteneur démarre puis s’arrête pendant l’activation de l’interface, car ip link add wg0 type wireguard renvoie Operation not permitted.
SYS_MODULE, associé au montage en lecture seule de /lib/modules, permet au conteneur de charger le module kernel WireGuard si l’hôte ne l’a pas déjà chargé. Le module réside dans le kernel de l’hôte, et non dans l’image. Le répertoire de l’hôte doit donc être visible. Sur un kernel récent, le module est généralement intégré. Vous pouvez le vérifier avec sudo modprobe wireguard && echo ok sur l’hôte.
net.ipv4.ip_forward=1 indique au kernel de forwarder les paquets qui ne sont pas destinés à la machine elle-même. Sans ce paramètre, un client se connecte, le handshake réussit, puis chaque paquet à destination d’Internet est rejeté. ping 1.1.1.1 finit alors par expirer, alors que le VPN semble connecté.
net.ipv4.conf.all.src_valid_mark=1 est le paramètre qui surprend le plus souvent. WireGuard marque ses propres paquets sortants afin qu’ils ne soient pas routés à nouveau dans le tunnel. Le reverse path filtering strict voit un paquet dont l’adresse source ne correspond pas à la route attendue et le rejette. Ce sysctl indique au kernel d’accepter les paquets marqués. C’est ce qui empêche un full tunnel de se casser lui-même.
Démarrer le service et créer le compte administrateur
cd /etc/docker/containers/wg-easy
sudo docker compose up -d
sudo docker compose logs -fUtilisez docker compose up et docker compose down, pas start ni stop. start redémarre uniquement les conteneurs qui existent déjà. Les modifications apportées au fichier Compose, notamment les paramètres réseau et sysctl, ne sont donc pas appliquées. up recrée en revanche les éléments modifiés. Si vous voulez que la stack soit relancée après un redémarrage, restart: unless-stopped le fait déjà. Le comportement au démarrage des services Compose explique ce que cette politique garantit ou non.
L’interface web écoute sur TCP 51821. Lors de la première visite, elle affiche une page de configuration. Vous y créez le compte administrateur et confirmez l’adresse de l’hôte que les clients utiliseront pour joindre le serveur. Cette adresse est inscrite dans la ligne Endpoint de la configuration de chaque client. Elle doit donc correspondre à l’adresse IP publique ou au nom DNS du VPS. Si elle est incorrecte, le QR code transmis à un téléphone pointe vers une adresse inaccessible et le handshake n’aboutit jamais.
Un dernier point concernant ce port : wg-easy ne fournit jamais lui-même TLS et ne refuse pas les connexions HTTP simples. Avec http://, l’interface reste accessible et affiche une bannière d’avertissement. La modification par défaut concerne le cookie de session, qui porte l’attribut Secure. Un navigateur qui accède à l’interface en HTTP simple sur une adresse publique le rejette donc, et la connexion ne reste pas ouverte. INSECURE=true supprime cet attribut et ne fait rien d’autre. Placer un reverse proxy devant wg-easy pour terminer TLS, même avec un certificat autosigné, ne nécessite aucune modification de wg-easy.
N’exposez pas le port de l’interface web sur Internet
Le fichier Compose publie le port 51821 sur toutes les interfaces. Il s’agit de la page de connexion d’un équipement capable d’acheminer votre trafic. Ce port ne doit donc pas être ouvert à Internet. La publication d’un port dans Docker ajoute des règles à la chaîne DOCKER, évaluée avant ufw. Une règle de refus ufw ne suffit donc pas à le fermer. Ce piège mérite d’être compris séparément. L’article Pourquoi les ports publiés par Docker ignorent ufw l’explique en détail.
La solution simple consiste à lier l’interface web à l’interface loopback et à y accéder par un tunnel SSH :
ports:
- "51820:51820/udp"
- "127.0.0.1:51821:51821/tcp"
environment:
- INSECURE=trueExécutez ensuite cette commande depuis votre ordinateur portable :
ssh -L 51821:127.0.0.1:51821 youruser@your.server.addressOuvrez http://127.0.0.1:51821 dans le navigateur de votre ordinateur portable. Le trafic est chiffré par SSH et personne d’autre ne peut accéder à ce port. INSECURE=true retire uniquement l’attribut Secure du cookie de session afin que le navigateur conserve votre session sur cette adresse HTTP non chiffrée. Cette configuration est sûre ici, car la connexion HTTP non chiffrée ne quitte jamais l’interface loopback.
Ouvrez le port UDP 51820 et vérifiez les deux pare-feu
WireGuard doit pouvoir joindre le port UDP 51820 depuis Internet. Docker publie ce port, mais de nombreux fournisseurs placent un pare-feu réseau distinct devant le VPS, que Docker ne connaît pas. Ouvrez le port aux deux endroits. Si vous gérez le pare-feu de l’hôte avec ufw, les règles ufw de base pour un VPS sont plus rapides à mettre en place que d’écrire manuellement des règles nftables.
Vérifiez que le conteneur écoute réellement :
sudo ss -ulnp | grep 51820Vous devez voir un socket UDP en écoute. Si cette ligne n’affiche rien, le conteneur n’a jamais activé l’interface, et sudo docker compose logs wg-easy indiquera la cause.
Créer un client et le scanner sur un téléphone
Dans l’interface, créez un client et donnez-lui un nom que vous reconnaîtrez plus tard, par exemple celui de l’appareil auquel il est associé. wg-easy attribue la prochaine adresse de tunnel disponible et génère la paire de clés pour vous. Chaque ligne de client propose un code QR et un fichier .conf téléchargeable.
Installez l’application officielle WireGuard sur le téléphone, choisissez d’ajouter un tunnel à partir d’un code QR, puis dirigez l’appareil photo vers le code affiché à l’écran. Le tunnel apparaît avec le nom que vous avez saisi. Activez-le. La ligne du client dans l’interface commence alors à afficher les compteurs de transfert et l’heure de la dernière négociation. Une fois le téléphone connecté au tunnel, il peut atteindre des services que vous n’avez jamais publiés sur Internet. C’est ainsi qu’un téléphone peut continuer à envoyer des photos vers un serveur photo auto-hébergé depuis n’importe où, sans que ce serveur ait un seul port ouvert sur Internet. Le même principe fonctionne pour les contenus multimédias, et une bibliothèque Jellyfin transformée en vidéoclub des années 90 est agréable à parcourir depuis une chambre d’hôtel tout en restant aussi privée que sur votre réseau local. Les alertes fonctionnent dans l’autre sens sur le même tunnel : un serveur ntfy auto-hébergé peut envoyer un message sur ce téléphone dès qu’une tâche de sauvegarde échoue, sans jamais répondre à une requête provenant de l’Internet public.
Un client qui n’affiche aucune négociation après son activation n’atteint pas du tout le serveur. Le problème vient alors du port UDP 51820, soit au niveau du firewall du fournisseur, soit au niveau de l’adresse de l’endpoint intégrée à la configuration. Un client qui affiche une négociation, mais qui n’a pas accès à Internet, indique plutôt un problème de forwarding ou de DNS.
Sur un poste de travail, téléchargez le fichier .conf et importez-le dans le client WireGuard au lieu de le saisir à nouveau. La clé privée de ce fichier est générée une seule fois et affichée une seule fois. Traitez ce fichier comme une clé privée SSH.
Quand dépasser l’interface web
wg-easy est l’outil adapté tant que vos peers sont des personnes et des téléphones. L’interface web est plus rapide que la modification de fichiers de configuration, et la révocation d’un téléphone perdu se fait en un clic.
Vous atteindrez ses limites lorsque vous voudrez quelque chose que l’interface web ne permet pas de modéliser. Le routage site à site, dans lequel le AllowedIPs d’un peer couvre tout un sous-réseau distant plutôt qu’une seule adresse, constitue généralement le premier obstacle. Les split tunnels avec des règles de routage par peer, ou une configuration générée par votre outil de provisioning, viennent ensuite. À ce stade, la configuration écrite à la main n’est pas plus difficile. Elle est simplement différente, et le guide WireGuard classique montre comment construire le même tunnel à partir de wg0.conf. Si vous préférez ne plus exécuter le control plane, WireGuard comparé à Tailscale présente l’option managed. Le bien-fondé de ce choix dépend de ce que le serveur de coordination peut réellement atteindre, et le modèle de confiance de Tailscale mérite d’être lu avant de lui confier votre réseau. Le coût est généralement la question suivante, et ce que couvre réellement le forfait gratuit de Tailscale est limité à six utilisateurs sans frais, pour un usage non commercial uniquement. Un foyer ne paie donc rien, tandis qu’une petite équipe d’entreprise n’est pas éligible, quel que soit son nombre d’utilisateurs. Au-delà, la facturation compte les utilisateurs et non les appareils. C’est un modèle de facturation différent de celui d’un VPS que vous payez déjà, donc ce que coûte Tailscale lorsque vous dépassez le forfait gratuit est le chiffre à vérifier avant de migrer une équipe. Le tunnel complet que vous venez de créer possède un équivalent direct, car annoncer le VPS comme exit node Tailscale vous fournit le même accès sortant via le serveur. L’autorisation se fait dans la console d’administration au lieu d’être écrite dans la configuration de chaque client. Le problème du sous-réseau a également un équivalent, car annoncer tout un réseau privé depuis le VPS transmet ce réseau à chaque appareil du tailnet, sans devoir modifier le AllowedIPs de chaque peer, ce qui vous avait fait dépasser les possibilités de l’interface web. Si vous voulez ce dashboard et le routage mesh automatique, sans utiliser le serveur de coordination de quelqu’un d’autre, exécuter votre propre serveur NetBird sur un VPS conserve le control plane sur du matériel que vous possédez, au prix de la configuration DNS et TLS que wg-easy ne vous demandait pas.
Si la syntaxe Compose ci-dessus était la partie inhabituelle, plutôt que WireGuard, les bases de Docker Compose sur un VPS explique le format du fichier et les commandes courantes.
FAQ
Pourquoi wg-easy ignore-t-il mes variables WG_HOST et PASSWORD_HASH ?
Ces variables appartiennent à wg-easy 14. La version 15 est une réécriture, et le projet upstream a déplacé presque toute la configuration dans le panneau d’administration de l’interface web. Le conteneur ne lit aucune de ces variables. Il démarre donc normalement, puis vous demande de créer un compte administrateur lors de la première visite. Définissez l’adresse d’hôte utilisée par les clients sur cette page de configuration.
Ai-je besoin de SYS_MODULE si mon kernel intègre déjà WireGuard ?
Non. SYS_MODULE et le montage /lib/modules permettent au conteneur de charger le module lorsque l’hôte ne l’a pas. Sur un hôte où sudo modprobe wireguard réussit déjà, cette capability est inutilisée. La supprimer est une mesure de hardening raisonnable, et NET_ADMIN reste nécessaire dans tous les cas.
Le client se connecte, mais il n’a pas accès à Internet. Quel est le problème ?
Un handshake sans trafic indique presque toujours un problème de forwarding. Vérifiez que net.ipv4.ip_forward=1 et net.ipv4.conf.all.src_valid_mark=1 figurent toujours dans le fichier compose, car une copie modifiée manuellement les perd souvent. Si le forwarding est activé, vérifiez le serveur DNS reçu par le client. Un tunnel qui envoie tout le trafic via le VPN, mais qui pointe vers un serveur DNS qu’il ne peut plus joindre, ressemble exactement à une connexion inactive dans un navigateur.
Comment sauvegarder mes clients ?
Tout se trouve dans le named volume etc_wireguard. Dans la version 15, cela correspond à une base SQLite située dans /etc/wireguard/wg-easy.db à l’intérieur du conteneur. La documentation de la v15 ne décrit aucune fonction de sauvegarde ou d’export. Sauvegardez donc directement le volume : exécutez docker compose down, copiez le contenu du volume vers un emplacement situé hors du serveur, puis redémarrez la stack avec docker compose up -d. Faites-le avant toute mise à niveau. Le fichier wg0.json mentionné dans les anciennes documentations appartient à wg-easy 14. C’est le fichier exporté par le bouton de sauvegarde de la v14. Dans la v15, il sert uniquement de fichier à téléverser dans l’assistant de configuration lorsque vous migrez une ancienne installation.
Puis-je exécuter wg-easy derrière un reverse proxy ?
Oui. Placez le proxy devant le port de l’interface web, terminez TLS à cet endroit et laissez INSECURE non défini. Le conteneur accepte directement le saut HTTP en clair fourni par le proxy, et les exemples officiels pour Caddy et Traefik ne définissent aucune variable INSECURE. Comme le navigateur communique en HTTPS avec le proxy, le cookie de session doit conserver son flag Secure. C’est le seul élément supprimé par INSECURE=true. Laissez le port UDP 51820 publié directement, car le trafic VPN utilise UDP et ne passe pas par un proxy HTTP.