wg-easy : WireGuard avec interface web dans Docker
Déployez WireGuard avec wg-easy et Docker Compose : ports, NET_ADMIN, sysctls indispensables et QR codes pour connecter vos téléphones en quelques minutes.
Ce que vous allez mettre en place
wg-easy est WireGuard avec une interface web, exécuté dans un conteneur Docker unique. Il gère l’interface WireGuard pour vous et ajoute une interface accessible depuis un navigateur pour créer des clients. Chaque client que vous créez reçoit un fichier de configuration et un code QR. Un téléphone peut ainsi rejoindre le VPN en visant l’écran avec son appareil photo.
Le tunnel lui-même est un WireGuard classique. 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 de peers sans modifier un fichier de configuration via 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 VPS KVM avec une adresse IPv4 publique, de Docker Engine avec le plugin Compose et d’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 lors de l’activation de l’interface.
La version 15 a déplacé les paramètres hors de l’environnement
La plupart des guides que vous trouverez ont été écrits pour wg-easy 14. Vous y définissez 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 de migration officielles 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. Il s’agit du nouveau parcours de configuration.
En juillet 2026, le tag majeur à utiliser est 15. Épinglez la version majeure plutôt que d’utiliser latest, car une mise à niveau majeure modifie le format de la configuration sur disque et ne permet pas un rollback propre.
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 modification.
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. 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 consultez 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, et non start et stop. Le projet upstream avertit que start sur un conteneur créé avec d’autres paramètres laisse le réseau dans un état incohérent. Si vous voulez que la stack redémarre après un reboot, restart: unless-stopped le prend déjà en charge. Le comportement au boot des services Compose explique ce que cette policy garantit ou non.
L’interface web écoute sur le port 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 apparaît ensuite dans la ligne Endpoint de chaque configuration client. Elle doit donc être l’adresse IP publique ou le 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 15 refuse le HTTP en clair, sauf si vous définissez INSECURE=true. Vous pouvez y accéder en HTTPS avec un certificat non approuvé ou terminer TLS sur un reverse proxy placé devant le service. En revanche, l’accès via http:// avec les paramètres par défaut ne fonctionne pas.
Ne publiez 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 pas être ouvert à tous. La publication d’un port dans Docker ajoute des règles à la chaîne DOCKER, évaluée avant ufw. Une règle ufw deny 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 à loopback et à y accéder via 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, personne d’autre ne peut accéder au port et INSECURE=true est sûr dans ce cas, car la connexion HTTP en clair 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 l’interface ne suffit plus
wg-easy est l’outil adapté tant que vos pairs sont des personnes et des téléphones. L’interface 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 configurer un élément que l’interface ne prend pas en charge. Le routage site à site, où le AllowedIPs d’un pair couvre tout un sous-réseau distant plutôt qu’une seule adresse, constitue généralement le premier obstacle. Les tunnels partiels avec des règles de routage propres à chaque pair, ou une configuration générée par votre outil de provisionnement, viennent ensuite. À ce stade, la configuration manuelle n’est pas plus difficile ; elle est simplement différente. Le guide WireGuard classique montre comment créer le même tunnel à partir de wg0.conf. Si vous préférez ne plus exécuter le control plane vous-même, WireGuard comparé à Tailscale présente l’option managée. La pertinence de ce compromis dépend de ce que le serveur de coordination peut réellement atteindre. 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. Ce que couvre réellement l’offre gratuite de Tailscale suffit pour qu’un foyer ou une petite équipe ne paie rien. Au-delà, la facturation compte les utilisateurs plutôt que les appareils. Le montant évolue donc différemment de celui d’un VPS que vous payez déjà. Le coût de Tailscale après le dépassement de l’offre gratuite est le chiffre à vérifier avant de migrer une équipe. Le tunnel complet que vous venez de créer possède un équivalent direct dans Tailscale. Annoncer le VPS comme exit node Tailscale vous donne le même accès sortant via le serveur, avec une approbation dans la console d’administration au lieu d’une configuration écrite dans chaque client. Le routage vers un sous-réseau possède lui aussi un équivalent. Annoncer tout un réseau privé depuis le VPS transmet ce réseau à tous les appareils du tailnet, sans devoir modifier le AllowedIPs de chaque pair, ce qui vous avait poussé à abandonner l’interface. Si vous voulez ce dashboard et le routage mesh automatique, mais pas 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. En contrepartie, vous devez configurer le DNS et TLS, ce 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 deux 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 host 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 le host ne l’a pas. Sur un host 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 utilise un serveur DNS qu’il ne peut plus joindre, ressemble exactement à une connexion inactive dans un navigateur.
Comment sauvegarder mes clients ?
Toutes les données se trouvent dans le volume nommé etc_wireguard, dans un fichier wg0.json. L’interface contient également un bouton de sauvegarde qui exporte ces mêmes données. Copiez ce fichier ailleurs que sur le serveur avant toute mise à niveau. La restauration consiste à téléverser le fichier pendant l’étape de configuration d’un conteneur neuf.
Puis-je exécuter wg-easy derrière un reverse proxy ?
Oui. Placez le proxy devant TCP 51821, terminez TLS à cet endroit et définissez INSECURE=true sur le conteneur afin qu’il accepte la connexion HTTP en clair provenant du proxy. Publiez directement UDP 51820, car le trafic VPN utilise UDP et ne passe pas par un proxy HTTP.