WireGuard VPN selbst hosten: wg-easy als eigener Exit-Node
11. August 2026

Diesen Beitrag habe ich zum Großteil von KI schreiben lassen. Dies ermöglicht es mir, ältere Projekte aufzuarbeiten und zu dokumentieren.
Kurzfassung: WireGuard über wg-easy auf einem Netcup-VPS. Der Web-Traffic verlässt das Netz danach mit der Server-IP statt der eigenen. UDP-Port direkt auf dem Host, Verwaltungsoberfläche über Traefik — und ein NAT-Fehler, der den Tunnel funktionierend aussehen lässt, obwohl nichts durchkommt.
Der kürzeste Artikel dieser Serie. Das Setup ist an einem Abend fertig; interessant ist genau eine Stelle, an der ich zwei Stunden gesucht habe.
1. Was das hier ist — und was nicht
Der Zweck ist ein persönlicher Exit-Node: Mein Handy oder Laptop baut einen Tunnel zum eigenen Server auf, und der gesamte Web-Traffic verlässt das Internet mit der Server-IP. In fremden WLANs — Café, Flughafen, Hotel — ist damit niemand mehr im selben Netz in der Lage mitzulesen, und Webseiten sehen eine IP, die zu meinem Server gehört statt zu meinem Anschluss.
Was es ausdrücklich nicht ist: ein Fernzugriff auf ein privates Heimnetz. Das ist der andere übliche WireGuard-Anwendungsfall, und er verlangt eine andere Konfiguration — dazu unten mehr.
Was es auch nicht ist: Anonymität. Der Server gehört mir, er ist auf meinen Namen registriert, und mein Hoster weiß, wer ich bin. Ein selbst gehosteter VPN verlagert Vertrauen vom WLAN-Betreiber und vom Internetanbieter zum eigenen Server — er löst es nicht auf. Wer Anonymität gegenüber Behörden sucht, ist hier falsch.
2. Warum WireGuard und warum wg-easy
WireGuard statt OpenVPN: Minimale Angriffsfläche — ein einziger UDP-Port, kein komplexer Daemon-Unterbau. Dazu deutlich schneller, und auf jeder Plattform gibt es einen nativen oder offiziellen Client. Der Code ist um Größenordnungen kleiner als OpenVPNs, was bei sicherheitskritischer Software allein schon ein Argument ist.
wg-easy statt reinem WireGuard: Reines WireGuard bedeutet, wg0.conf von Hand zu editieren — pro Gerät ein Schlüsselpaar erzeugen, eintragen, Konfiguration aufs Gerät bringen. Das geht für ein Gerät. Bei fünf über zwei Jahre wird es zur Fehlerquelle.
wg-easy verwaltet Server-Keys, Client-Konfigurationen und QR-Codes über eine Weboberfläche. Neues Gerät heißt: Client anlegen, QR-Code scannen, fertig. Der Client selbst bleibt das offizielle WireGuard-Programm — wg-easy ist nur die Verwaltung, kein eigener Client.
3. Warum der VPN-Port nicht über den Reverse Proxy läuft
Auf diesem Server läuft alles hinter Traefik. Beim Mailserver habe ich dafür einigen Aufwand getrieben — TCP-Passthrough, PROXY Protocol, damit der Dienst die echte Client-IP sieht.
Bei WireGuard ist das alles nicht nötig, und das ist kein Sonderfall, sondern folgt aus dem Protokoll.
WireGuard macht seinen eigenen verschlüsselten Handshake auf UDP-Ebene. Es braucht keine TLS-Terminierung, weil es TLS gar nicht verwendet. Und es braucht kein PROXY Protocol, weil es die echte Absenderadresse ohnehin nativ sieht — UDP wird nicht durch einen HTTP-Reverse-Proxy im klassischen Sinne geroutet.
Deshalb die Aufteilung:
- Port
51820/udpwird direkt auf dem Host publiziert, komplett an Traefik vorbei. - Die Weboberfläche (Port 51821) läuft als ganz normaler HTTP-Upstream durch Traefik — mit TLS von Let's Encrypt, wie jeder andere Dienst. Kein Host-Port dafür.
Ein Dienst, zwei völlig verschiedene Wege nach draußen. Das ist die eine Architekturentscheidung, die man verstanden haben sollte, bevor man die Compose-Datei liest.
4. Die Compose-Datei
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy:15
container_name: vpn_wg_easy
restart: unless-stopped
volumes:
- ./config:/etc/wireguard
- /lib/modules:/lib/modules:ro
ports:
# Nur der WireGuard-UDP-Port. Die Web-UI läuft über Traefik.
- "51820:51820/udp"
networks:
wg:
interface_name: eth0
gw_priority: 1
ipv4_address: 10.42.42.42
ipv6_address: fdcc:ad94:bacf:61a3::2a
traefik-net:
interface_name: eth1
gw_priority: 0
ipv4_address: 172.20.0.8
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
labels:
- "traefik.enable=true"
- "traefik.http.routers.vpn.rule=Host(`vpn.tariwiencke.de`)"
- "traefik.http.routers.vpn.entrypoints=websecure"
- "traefik.http.routers.vpn.tls=true"
- "traefik.http.routers.vpn.tls.certresolver=myresolver"
- "traefik.http.services.vpn.loadbalancer.server.port=51821"
- "traefik.docker.network=traefik-net"
networks:
traefik-net:
external: true
wg:
driver: bridge
enable_ipv6: true
ipam:
driver: default
config:
- subnet: 10.42.42.0/24
- subnet: fdcc:ad94:bacf:61a3::/64
NET_ADMIN und SYS_MODULE braucht der Container, um das WireGuard-Interface anzulegen und iptables-Regeln zu setzen. Die sysctls schalten IP-Forwarding ein — ohne das leitet der Container gar nichts weiter.
Die interface_name- und gw_priority-Zeilen sind der Fix aus Kapitel 8. Wer neu aufsetzt, sollte sie von Anfang an drinhaben.
Deployment:
cd /opt/vpn
docker compose up -d
Keine .env nötig — in Version 15 gibt es keine Pflicht-Umgebungsvariablen mehr. ./config wird beim ersten Start angelegt und enthält danach den Server-Private-Key und alle Client-Konfigurationen.
5. Version 15 statt latest — und warum alte Anleitungen nicht mehr passen
Zwei Dinge, die zusammengehören.
Der latest-Tag zeigt bei wg-easy auf Version 14, nicht auf die neueste. Die offizielle Dokumentation empfiehlt ausdrücklich, stattdessen den Major-Tag zu verwenden — also 15. Innerhalb einer Hauptversion gibt es keine Breaking Changes.
Version 15 hat den Einrichtungsweg komplett umgestellt. Es gibt keine WG_HOST-, PASSWORD- oder PASSWORD_HASH-Umgebungsvariablen mehr. Host, Port, Admin-Benutzername und Passwort werden stattdessen über einen Assistenten in der Weboberfläche gesetzt.
Das ist der Grund, warum viele Anleitungen im Netz nicht mehr funktionieren: Sie zeigen eine .env mit WG_HOST=… und PASSWORD_HASH=…, und man sucht dann, warum der Container die Werte ignoriert. Er ignoriert sie nicht — er kennt sie nicht mehr.
Faustregel bei jeder Anleitung zu diesem Tool: Steht dort WG_HOST, beschreibt sie Version 14.
6. Firewall und DNS
Firewall: Bevor der erste Client sich zu verbinden versucht, prüfen, ob eine aktiv ist:
ufw status verbose # oder: nft list ruleset
Falls ja, 51820/udp freigeben. Falls keine aktiv ist — bei mir war das der Fall — ist das ein bereits bestehendes Problem, das nichts mit dem VPN zu tun hat. Dann lohnt es sich, grundsätzlich eine einzurichten (default-deny eingehend, nur die tatsächlich gebrauchten Ports) statt punktuell einen Port freizugeben.
Ein Punkt, der dabei oft übersehen wird: Docker schreibt seine Weiterleitungen direkt in iptables und umgeht ufw dabei. Eine ufw-Regel schützt also nicht vor einem Container-Port, der auf allen Interfaces publiziert ist. Wirksam sind entweder die Bindung in der Compose-Datei oder Regeln in der DOCKER-USER-Kette.
DNS: Bei mir war kein neuer Eintrag nötig — die Zone hat bereits einen Wildcard-Record auf die Server-IP, der vpn.<domain> mit auffängt. Vor dem ersten Aufruf prüfen, sonst versucht Traefik vergeblich, ein Zertifikat zu holen:
dig +short vpn.tariwiencke.de A
7. Ersteinrichtung
Die Weboberfläche unter https://vpn.<domain> öffnen. Der Assistent fragt vier Dinge ab:
- User Setup — Admin-Benutzername und Passwort. Gehört in den Passwortmanager, nicht in ein Notizfeld.
- Existing Setup — „No“, sofern du nicht von Version 14 migrierst.
- Host Setup — als Host den Hostnamen eintragen (
vpn.<domain>), nicht die rohe IP. Das bleibt stabil, falls sich die Server-IP je ändert; sonst müsstest du jede Client-Konfiguration neu ausrollen. Port bleibt51820. - Ersten Client anlegen und dabei Allowed IPs prüfen: Für einen Exit-Node muss dort
0.0.0.0/0, ::/0stehen — also „alles über den Tunnel“. Das sollte der Standard für neue Clients sein, aber einmal bestätigen kostet nichts.
Dann QR-Code scannen (Mobilgerät) oder .conf herunterladen (Desktop), im offiziellen WireGuard-Client importieren.

Die Client-Übersicht mit dem QR-Dialog. Der Code ist hier absichtlich abgedeckt: Er enthält den privaten Schlüssel des Clients im Klartext — wer ihn abfotografiert, ist im Tunnel.
Verifizieren, und zwar richtig:
curl ifconfig.me
Muss die Server-IP zeigen. Zeigt es weiterhin deine eigene, ist der Tunnel nicht aktiv oder nicht auf Full-Tunnel gestellt. Zeigt es gar nichts — dann kommt jetzt das interessante Kapitel.
8. Der Fehler: Handshake grün, Internet tot
Das Symptom war maximal verwirrend. Der Client verband sich, sudo wg show zeigte einen aktuellen Handshake, die Konfiguration stand auf Full-Tunnel. Und trotzdem lief keine einzige Webseite.
Der verräterische Hinweis stand im Traffic-Zähler: viel gesendet, fast nichts empfangen. Die Pakete gingen also raus — sie kamen nur nie zurück.
Die Diagnose
docker exec vpn_wg_easy iptables -t nat -L POSTROUTING -n -v
Die MASQUERADE-Regel für das Client-Subnetz 10.8.0.0/24 war da — wg-easy legt sie selbst an. Aber sie stand auf null gematchten Paketen, obwohl längst Traffic floss. Eine Regel, die es gibt und die nie greift.
Der Grund stand in der nächsten Zeile: Die Regel war an -o eth0 gebunden.
docker exec vpn_wg_easy ip route
Die tatsächliche Default-Route lief über eth1.
Die Ursache
wg-easy geht intern davon aus, dass sein WireGuard-Netz als eth0 mit der Default-Route auftaucht, und baut die NAT-Regel darauf auf. Diese Annahme stimmt, solange der Container an genau einem Docker-Netz hängt.
Meiner hängt an zweien — dem eigenen WireGuard-Netz und traefik-net für die Weboberfläche. Und Docker vergibt die Interface-Namen bei mehreren Netzen nicht deterministisch. Bei mir landete traefik-net auf eth0 und das WireGuard-Netz auf eth1. Die NAT-Regel zeigte damit auf das falsche Interface.
Die Folge: Client-Traffic verließ den Container mit seiner privaten 10.8.0.x-Absenderadresse statt mit der Server-IP. Im öffentlichen Internet ist diese Adresse nicht routbar — die Pakete kamen an, aber keine Antwort fand je zurück. Genau das Bild aus dem Traffic-Zähler.
Nachtrag zur Ehrlichkeit: Ich habe das selbst diagnostiziert, aber die Erklärung nicht selbst gefunden. Sie steht wörtlich im offiziellen FAQ unter „Clients lose connectivity after restarting the container when using multiple networks“. Ein Blick dorthin wäre schneller gewesen als zwei Stunden iptables-Ausgabe lesen.
Der Fix
Interface-Namen und Routen-Priorität explizit pro Netzwerk festlegen, statt sie Docker zu überlassen:
networks:
wg:
interface_name: eth0
gw_priority: 1
traefik-net:
interface_name: eth1
gw_priority: 0
Damit liegt das WireGuard-Netz fest auf eth0 und behält die Default-Route — unabhängig davon, in welcher Reihenfolge Docker die Netze beim nächsten Neustart anfasst.
⚠️ Voraussetzung: Docker Engine ab Version 28. interface_name und gw_priority sind neuere Felder der Compose-Spezifikation. Auf einem älteren Host werden sie stillschweigend ignoriert — und dann ist der Fehler wieder da, ohne dass eine Fehlermeldung darauf hinweist. Vor dem Deployment docker --version prüfen.
Die übertragbare Lehre
Der Fehler entsteht nicht durch wg-easy und nicht durch Docker allein, sondern durch die Kombination aus einer impliziten Annahme im Container („mein Netz heißt eth0“) und einem nicht-deterministischen Verhalten der Plattform. Beide Seiten sind für sich genommen in Ordnung.
Und er ist besonders unangenehm, weil er erst bei einem Neustart auftreten kann. Die Zuordnung kann beim ersten Start zufällig richtig sein und Wochen später beim Redeploy kippen. Wer den Fix nicht setzt, hat eine Zeitbombe mit unklarem Zünder.
9. Was noch offen ist
Kein Backup von ./config. Dort liegen der Server-Private-Key und alle Client-Konfigurationen. Geht das verloren, muss jedes Gerät neu eingerichtet werden. Kein Datenverlust im eigentlichen Sinne — Bilder oder Mails sind nicht betroffen — aber vermeidbarer Aufwand. Das Verzeichnis in einen bestehenden Backup-Lauf aufzunehmen ist eine Zeile.
Kein DNS-Filter im Tunnel. Der naheliegende nächste Schritt: einen DNS-Resolver mit Werbe- und Trackerfilter hinter den VPN hängen. Dann filtert der Tunnel nicht nur die Absenderadresse, sondern auch, was das Gerät überhaupt auflöst — auf allen Geräten, im Mobilfunk, ohne App. Wichtig dabei: Der Resolver darf ausschließlich über das VPN-Interface erreichbar sein. Ein offener DNS-Resolver im Internet wird binnen Stunden für Angriffe missbraucht.
10. Fazit
Der unaufwendigste Dienst in meinem Setup. Eine Compose-Datei, ein Assistent, fertig. Kein Vergleich zum Mailserver.
Was den Ausschlag gibt, ist die Protokollfrage. WireGuard braucht keinen Reverse Proxy, weil es seine eigene Verschlüsselung mitbringt — und wer das versteht, spart sich den Versuch, UDP durch einen HTTP-Proxy zu zwingen. Umgekehrt gehört die Weboberfläche sehr wohl dorthin. Ein Dienst, zwei Wege.
Und die eine Sache, die ich anders machen würde: zuerst ins FAQ des Projekts schauen, dann iptables lesen. Der Fehler, den ich zwei Stunden gejagt habe, stand dort wörtlich beschrieben.
Weiterführende Artikel
- Ein VPS, sieben Dienste, ein Repo — warum jeder andere Dienst über Traefik läuft und dieser nicht.
- Stalwart Mail Server einrichten — der Dienst, für den die Proxy-Kante gebaut wurde.