How to Set Up IPv6 on a Home Server (Addresses, Firewall, Docker and the Reverse Proxy)

Most UK and EU ISPs now hand out IPv6 whether you asked or not. It changes one big thing: every device gets a globally routable address, so "behind NAT" stops being a defence and the firewall is the only wall. Done right, IPv6 gives you direct connections with no port forwarding (and an escape from CGNAT); done carelessly, it exposes services you thought were private. Here's how to tell what you have, set the server up properly, and when to just switch it off.
Quick reference
| Task | Command |
|---|---|
| Do I have it? | ip -6 a — a 2xxx: address on the LAN interface means yes |
| Test from the server | curl -6 https://ifconfig.co |
| Firewall | UFW handles v6 if IPV6=yes in /etc/default/ufw (default) |
| Stable address | Netplan ipv6-address-generation: stable-privacy or a static suffix |
| Router | enable the IPv6 firewall (block inbound by default) |
| Docker | "ipv6": true + a ULA subnet in daemon.json |
| Turn it off | net.ipv6.conf.all.disable_ipv6=1 in sysctl |
1. Do you have it?
ip -6 a show enp3s0
- fe80::... only: link-local, every machine has one, means nothing.
- 2a00:... or 2001:...: a global address from your ISP's prefix. You have IPv6.
- fd..:: a ULA (private) address, from a router that makes its own.
curl -6 https://ifconfig.co
Your public IPv6. Note the prefix — the first four groups (2a00:1234:5678:abcd::/64) — is your home's; the rest is the device.
2. The mental model
- No NAT. The router routes; each device's address is its own public address. The router's IPv6 firewall is what stops the internet reaching them. Check it is on (most are by default; some older routers aren't).
- Prefixes can change. Some ISPs give a stable prefix; many rotate it on reconnect. A device's suffix can be stable even when the prefix moves.
- Privacy extensions: clients generate random temporary addresses for outgoing connections. Servers should not.
3. A stable address for the server
In Netplan:
enp3s0:
dhcp4: true
dhcp6: false
accept-ra: true
ipv6-address-generation: stable-privacy
ipv6-privacy: false
stable-privacy gives the server the same suffix every boot, derived from a secret, without exposing the MAC address. If your prefix is stable too, the full address is stable; put it in DNS as an AAAA record alongside the A record. If the prefix rotates, dynamic DNS (tutorial 48) handles AAAA updates the same way (DuckDNS: add &ipv6= to the update URL).
sudo netplan try, then ip -6 a.
4. Firewall — this is the important part
UFW writes both v4 and v6 rules when IPV6=yes in /etc/default/ufw (the default). sudo ufw status verbose shows (v6) entries. Every ufw allow you've made applies to both, so the server's posture is the same on IPv6 as IPv4: deny in, allow SSH/80/443/WireGuard.
Now the rest of the house: the router's IPv6 firewall must default to deny inbound, or every camera and plug is on the internet. Look for "IPv6 firewall", "IPv6 security" or "Block inbound IPv6" and make sure it's on. To expose only the server's web ports, add a pinhole rule on the router for its address, ports 80/443 — the IPv6 equivalent of a port forward.
5. Nginx, Caddy and Docker
Nginx: add listen [::]:80; and listen [::]:443 ssl; beside the IPv4 listen lines (Caddy listens on both by itself). Certbot's challenge works over v6 once the AAAA record and pinhole exist.
Docker doesn't give containers IPv6 by default. If you want it (so the reverse proxy sees real v6 client addresses, say):
sudo nano /etc/docker/daemon.json
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef::/48",
"experimental": true,
"ip6tables": true
}
sudo systemctl restart docker. Containers get ULA addresses and Docker NATs v6 like v4 (ip6tables: true). For most home setups, leaving Docker on v4 internally and letting Nginx on the host take v6 is simpler.
6. Pi-hole and DNS
Clients with IPv6 may use the router's advertised v6 DNS and bypass Pi-hole. In Pi-hole's DHCP settings enable IPv6 (RA) so it advertises itself, or on the router set the IPv6 DNS to the server's address. Pi-hole handles AAAA queries and blocklists over v6 the same way.
7. Or turn it off
Legitimate choice if you don't need it and would rather not audit every device: on the router, disable IPv6; and on the server:
echo "net.ipv6.conf.all.disable_ipv6 = 1" | sudo tee /etc/sysctl.d/99-no-ipv6.conf
sudo sysctl --system
But if you're behind CGNAT (tutorial 47), IPv6 may be your only path to direct inbound connections, which makes it worth the care.
Troubleshooting
curl -6 fails but the address exists. Router isn't routing v6 (ISP not delivering it), or the default route is missing: ip -6 route.
Services unreachable over v6 from outside. No AAAA record, no pinhole on the router, or Nginx not listening on [::].
A device is reachable from the internet that shouldn't be. The router's v6 firewall is off. Turn it on now.
Intermittent slowness on some sites. Broken v6 path to that site; sysctl net.ipv6.conf.all.use_tempaddr=2 for clients, or prefer v4 via /etc/gai.conf on the server as a workaround.
Wrapping up
Check you have it, give the server a stable suffix, confirm UFW covers v6 and the router blocks inbound by default, pinhole only the web ports, add AAAA records. Every device is public on IPv6, so the firewall is the whole story — set that first, enjoy the direct connections after.
