Setting Up WireGuard with IPv6 Support in Docker on Ubuntu
This guide provides a streamlined method to deploy WireGuard in a Docker container on Ubuntu Server 24.04, enabling unique global IPv6 addresses for clients. It addresses a common gap in existing documentation, particularly for IPv6 delegation. The setup uses host networking for simplicity in managing static IPv6 routes, though a bridge networking alternative is included.
Key Goals:
- Assign unique global IPv6 addresses from a delegated prefix to each WireGuard client.
- Support delegation to downstream devices (e.g., via a travel router client).
Assumptions and Scope:
- Ubuntu Server 24.04 host.
- At least one /64 IPv6 subnet available from your delegated prefix (e.g., /56 or /48; /60 works but limits subnets).
- Static IPv6 prefix preferred; dynamic prefixes require manual reconfiguration (see IPv6 Prefix Changes).
- Use the LinuxServer.io WireGuard image, which lacks official IPv6 support but works with manual tweaks.
- Commands are for Ubuntu; adapt for other distributions.
Security Notes:
- Keep your IPv6 prefix private.
- Use a DDNS service or static IPv4 for the endpoint if your public IP changes.
Prerequisites
IPv6 Setup
- Obtain a delegated IPv6 prefix from your ISP (e.g., via router WAN settings).
- Request a /56 or /48 for flexibility; a /60 suffices but yields fewer /64 subnets.
- Carve out /64 subnets: one for WireGuard clients (e.g.,
2001:db8:b00b:42a::/64), and optionally another for delegation (e.g.,2001:db8:b00b:42b::/64).
Enable IPv6 Packet Forwarding
Edit /etc/sysctl.conf as root to enable forwarding:
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
Apply changes:
sudo sysctl -p
Install Dependencies
Update packages and install WireGuard tools (with optional qrencode for QR codes):
sudo apt update
sudo apt install wireguard-tools qrencode
Install Docker (official repository recommended over Ubuntu’s version):
# Add GPG key
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Add repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install docker-ce docker-compose-plugin
Add your user to the Docker group (log out and back in after):
sudo usermod -aG docker $USER
Deploy the WireGuard Container
Create a working directory:
sudo mkdir -p /srv/wireguard/config
cd /srv/wireguard
Create docker-compose.yaml:
services:
wireguard:
image: linuxserver/wireguard:latest
container_name: wireguard
network_mode: host
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
- PUID=1000
- PGID=1000
- TZ=America/Los_Angeles # Adjust to your timezone
- SERVERURL=your.ddns.example.com # Your public endpoint
- SERVERPORT=51820
- PEERS=phone,workphone,tablet,laptop,travelrouter # Comma-separated peer names
- PEERDNS=8.8.8.8,8.8.4.4,2001:4860:4860::8888,2001:4860:4860::8844
- INTERNAL_SUBNET=10.13.13.0/24
- ALLOWEDIPS=0.0.0.0/0,::/0
- PERSISTENTKEEPALIVE_PEERS=all
volumes:
- ./config:/config
- /lib/modules:/lib/modules
privileged: true
restart: unless-stopped
Launch the container:
sudo docker compose up -d
sudo docker compose logs wireguard # Check for errors
Note: If using Ubuntu’s Docker (docker-compose with hyphen), adjust commands accordingly.
Test IPv4 Connectivity
- Forward UDP port 51820 on your router to the host’s static IPv4.
- Allow the port on the host firewall (if using UFW):
sudo ufw allow 51820/udp - Generate and scan a client QR code (e.g., from
/srv/wireguard/config/peer_phone/peer_phone.png) using the WireGuard app. - Connect via mobile data (disable WiFi) and verify IPv4 internet access.
Enable IPv6 Support
Edit /srv/wireguard/config/wg_confs/wg0.conf to add IPv6. Include ip6tables rules in PostUp/PostDown for IPv6 forwarding (e.g., ip6tables -A FORWARD -i %i -j ACCEPT; ip6tables -A FORWARD -o %i -j ACCEPT).
Server Interface Example:
[Interface]
Address = 10.13.13.1/32, 2001:db8:b00b:42a::1/128
ListenPort = 51820
PrivateKey = <server-private-key>
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE; ip6tables -A FORWARD -i %i -j ACCEPT; ip6tables -A FORWARD -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth+ -j MASQUERADE; ip6tables -D FORWARD -i %i -j ACCEPT; ip6tables -D FORWARD -o %i -j ACCEPT
Peer Examples (add IPv6 to AllowedIPs):
[Peer]
# peer_phone
PublicKey = <public-key>
PresharedKey = <preshared-key>
AllowedIPs = 10.13.13.2/32, 2001:db8:b00b:42a::2/128
PersistentKeepalive = 25
[Peer]
# peer_travelrouter
PublicKey = <public-key>
PresharedKey = <preshared-key>
AllowedIPs = 10.13.13.6/32, 2001:db8:b00b:42a::6/128, 2001:db8:b00b:42b::/64
PersistentKeepalive = 25
Edit each client config in /srv/wireguard/config/peer_*/peer_*.conf (add IPv6 to Address):
Client Example:
[Interface]
Address = 10.13.13.2, 2001:db8:b00b:42a::2
PrivateKey = <client-private-key>
DNS = 8.8.8.8,8.8.4.4,2001:4860:4860::8888,2001:4860:4860::8844
[Peer]
PublicKey = <server-public-key>
PresharedKey = <preshared-key>
Endpoint = your.ddns.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
Restart and verify:
sudo docker compose restart wireguard
sudo docker compose logs wireguard
Regenerate QR codes (originals lack IPv6):
qrencode -o peer_phone.png < /srv/wireguard/config/peer_phone/peer_phone.conf
qrencode -t ANSI < /srv/wireguard/config/peer_phone/peer_phone.conf # Terminal display
Warning: Changes to docker-compose.yaml (e.g., peers, DNS) will overwrite configs; back them up post-edits.
Configure Static IPv6 Routes
-
Identify the host’s link-local IPv6 on the LAN interface (e.g.,
eth0):ip -c -6 -brief addr show dev eth0 | grep 'fe80::'Output: e.g.,
fe80::1234:5678:9abc:def0/64 scope link. -
On your router, add static routes for client subnets (e.g.,
2001:db8:b00b:42a::/64,2001:db8:b00b:42b::/64) via the link-local address on the LAN interface.
Test: Reconnect a client and verify IPv6 connectivity (e.g., ping6 ipv6.google.com).
Alternative: Docker Bridge Networking
For bridge mode (avoids host networking but adds route complexity):
Update docker-compose.yaml (add sysctls for IPv6):
networks:
wg6:
enable_ipv6: true
ipam:
driver: default
config:
- subnet: "2001:db8:b00b:421::/64"
services:
wireguard:
# ... (remove network_mode: host)
networks:
- wg6
ports:
- 51820:51820/udp
sysctls:
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv6.conf.all.forwarding=1
- net.ipv6.conf.default.forwarding=1
- net.ipv6.conf.eth0.proxy_ndp=1
# ... (rest unchanged)
-
Enable IPv6 in Docker daemon (
/etc/docker/daemon.json):{ "ipv6": true, "fixed-cidr-v6": "2001:db8:b00b:421::/64" }Restart Docker:
sudo systemctl restart docker. -
Restart:
sudo docker compose up -d. -
Get container IPv6:
sudo docker exec wireguard ip -c -6 -brief addr show dev eth0(e.g.,2001:db8:b00b:421::2). -
Add host routes (non-persistent):
sudo ip -6 route add 2001:db8:b00b:42a::/64 via 2001:db8:b00b:421::2 sudo ip -6 route add 2001:db8:b00b:42b::/64 via 2001:db8:b00b:421::2 -
Persist routes via systemd (create
/etc/systemd/system/wg-routes.service):[Unit] Description=WireGuard IPv6 Routes After=docker.service [Service] Type=oneshot RemainAfterExit=yes ExecStart=/bin/sh -c 'ip -6 route add 2001:db8:b00b:42a::/64 via 2001:db8:b00b:421::2; ip -6 route add 2001:db8:b00b:42b::/64 via 2001:db8:b00b:421::2' [Install] WantedBy=multi-user.targetEnable:
sudo systemctl enable wg-routes.service.
Handling IPv6 Prefix Changes
If your prefix changes (e.g., Xfinity residential), update:
/srv/wireguard/config/wg_confs/wg0.conf- All
/srv/wireguard/config/peer_*/peer_*.conf docker-compose.yaml(bridge mode only)- Router static routes
- Host/container routes (if bridge mode)
Restart the container and regenerate QR codes.
Troubleshooting
Common issues stem from Docker’s limited IPv6 handling, routing misconfigurations, or firewall rules. Use these steps to diagnose and resolve.
Container Startup Failures
- Privileges/Capabilities Missing: Ensure
privileged: true,cap_add: [NET_ADMIN, SYS_MODULE], and/lib/modulesvolume are set. Check logs for kernel module errors:sudo docker compose logs wireguard | grep module. - Docker Version Conflicts: Recent versions (e.g., 28.0.0+) may break IPv6 on Ubuntu 24.04. Verify with
docker --version; rollback if needed:sudo apt install docker-ce=5:27.5.1-1~ubuntu.24.04~noble(adjust per repo). - Module Loading: Run
sudo modprobe wireguardon host; restart container.
No IPv6 Connectivity in Container
- IPv6 Disabled: For bridge mode, confirm
/etc/docker/daemon.jsonhas"ipv6": trueand restart Docker. Inside container:sudo docker exec wireguard ip -6 route—should show routes likedefault via <gateway> dev eth0. - Sysctl Issues: Add
net.ipv6.conf.all.disable_ipv6=0to sysctls; verify host forwarding:sysctl net.ipv6.conf.all.forwarding. - Route Table Check: Run
sudo docker exec wireguard ip -c -6 route—look for wg0 entries and defaults. If missing, add ip6tables forwarding rules to PostUp.
Clients Not Receiving/Using IPv6 Addresses
- Config Overwrites: After edits, regenerate QR codes and rescan; verify client
Addressincludes IPv6 (e.g.,10.13.13.2, 2001:db8:b00b:42a::2). - AllowedIPs Errors: If clients fail to connect, temporarily remove
::/0from client AllowedIPs, test IPv4, then re-add. - DNS Resolution: Ensure PEERDNS includes IPv6 servers; test client:
nslookup ipv6.google.comover tunnel.
Routing and Forwarding Problems
- Static Routes Missing: Verify router routes via link-local; on host:
ip -6 route show. For bridge, add host-to-container routes. - Packet Tracing: Use
tcpdump -i any ip6on host/container/router to trace IPv6 packets—check if requests reach destination and replies return. - Policy-Based Routing for Endpoints: If tunnel traffic loops to endpoints, add to wg0.conf PostUp (symmetric on peers):
PreDown: Delete rules/routes. This bypasses tunnel for UDP handshakes.ip -6 rule add to <peer-endpoint-ipv6>/128 ipproto udp dport 51820 table 200 ip -6 route add default via fe80::<router-link-local> dev <lan-iface> table 200 - NDP Proxy: For bridge mode, add
net.ipv6.conf.eth0.proxy_ndp=1sysctl; install radvd on host to announce client prefixes.
Firewall and NAT Conflicts
- UFW/ip6tables Blocks: Run
sudo ufw status verboseandsudo ip6tables -L -v -n—allow 51820/udp and ::/0 forwarding. Addufw allow in on wg0if using UFW. - ISP/Router Blocks: Confirm prefix delegation; test host IPv6:
ping6 ipv6.google.com.
General Diagnostics
- WireGuard Status:
sudo docker exec -it wireguard wg show—check handshakes, allowed IPs. - Full Logs:
sudo docker compose logs -f wireguardfor real-time errors. - Reboot and Test: After changes, reboot host/router; incrementally test IPv4 → full IPv6.
If issues persist, share ip -6 route, wg show output, and logs on comments below will try to help