IPv4 exhaustion is not theoretical. RIPE NCC, APNIC, and LACNIC have all run out of /8 blocks. ISPs in Latin America, Europe, and Asia are deploying IPv6 natively, and if your homelab runs behind a GPON or FTTH connection, you likely already have a /56 or /64 from your ISP.
Yet Docker defaults to IPv4-only. Every docker network create,
every Compose file, every container — pure IPv4 with NAT. Running
dual-stack in 2026 is not a nice-to-have; it is a prerequisite
for services that need to be reachable over IPv6 directly, for
containers that need to bypass carrier-grade NAT, and for staying
compatible with a rapidly v6-ing internet.
This guide covers everything: enabling IPv6 in the Docker daemon,
configuring address pools for automatic allocation, building
IPv6-ready Compose stacks, running Traefik with IPv6 entry points,
and securing it all with ip6tables.
Step 1 — Enable IPv6 in the Docker Daemon
The first step is telling the Docker daemon to allocate IPv6
addresses on the default bridge network. Edit or create
/etc/docker/daemon.json:
|
|
ipv6: true— Enables IPv6 on the default bridge.fixed-cidr-v6— The ULA (Unique Local Address) subnet for the default bridge. Use a ULA fromfd00::/8rather than guessing at GUA prefixes you do not own.ip6tables: true— Automatically manages IPv6 firewall rules for port publishing and network isolation. Enabled by default, but worth making explicit.
Restart the daemon:
|
|
Verify the default bridge now has an IPv6 address:
|
|
Run a quick test:
|
|
If you see IPv6 addresses in the output, the daemon is working.
Step 1b — Default Address Pools for Dynamic IPv6
Without explicit configuration, user-defined networks with
enable_ipv6: true fall back to Docker’s default address pools,
which include no IPv6 pools. This means every Compose stack
that needs IPv6 must specify an explicit subnet under ipam.
To make Docker auto-allocate IPv6 subnets just like it does for
IPv4, add pools to daemon.json:
|
|
This gives Docker 256 /64 IPv6 subnets to hand out dynamically
from the fd00:dead:beef::/48 ULA range. Each Compose stack
that sets enable_ipv6: true without an explicit subnet gets
its own /64 automatically.
Restart Docker again after editing:
|
|
Test it with a quick network create:
|
|
You should see a /64 ULA subnet allocated automatically.
Step 2 — Docker Compose with Explicit IPv6 Networks
For production stacks where you want predictable addressing, define the IPv6 subnet explicitly. Here is a complete Compose file for a web app with a PostgreSQL backend:
|
|
Key points:
- Static IPv6 addresses let you write firewall rules against known addresses and keep DNS records stable.
- The backend network is
internal: true— no external access, no default route. Only the app container can reach the database. This works identically for IPv6 traffic. - Each network gets its own /64. Docker bridges support multiple IPAM config blocks; the IPv4 and IPv6 entries are completely independent.
If your host has a public /64 from your ISP, you can skip ULA and use GUA (Global Unicast Addresses) directly. Replace the subnet with your delegated prefix:
|
|
Step 3 — Exposing Containers to the LAN over IPv6
The Docker bridge with port publishing (-p or ports: in
Compose) works for both IPv4 and IPv6. Publishing port 443
binds to [::]:443 on the host by default:
|
|
Internally, Docker creates ip6tables DNAT rules just like it
creates iptables rules for IPv4. Check them:
|
|
MACVLAN for Direct LAN Addressing
If you want containers to appear as first-class citizens on your LAN with their own IPv6 address (no host NAT), use a macvlan network. This is especially useful for services that need direct inbound connections — game servers, SIP phones, P2P nodes.
|
|
|
|
Caveat: MACVLAN bridges do not allow communication between the host and its own containers without a separate macvlan interface on the host. For most homelab setups, bridge + port publishing is simpler and sufficient.
Step 4 — Traefik with IPv6 Entry Points
If you run Traefik as your reverse proxy, configuring IPv6 entry
points is straightforward. The key is binding [::]:port instead
of 0.0.0.0:port.
Static configuration (traefik.yml):
|
|
Or via CLI arguments in a Compose stack:
|
|
Because the entry points bind to [::]:port and the host kernel
has net.ipv6.bindv6only=0 (default on most distros), IPv4
traffic also reaches these sockets via IPv4-mapped IPv6
addresses. You do not need separate IPv4 and IPv6 entry points.
Verifying Traefik IPv6
If you have the Traefik API enabled:
|
|
Test an actual request through Traefik over IPv6:
|
|
Check Traefik logs for the client IP source — it should show your real IPv6 address, not the Docker gateway:
|
|
Step 5 — IPv6 Firewall Rules with ip6tables
Docker’s ip6tables: true manages port publishing rules
automatically, but it does not restrict outbound or forwarded
traffic. For a homelab running public-facing services, add a
minimal ip6tables ruleset.
Create /etc/iptables/rules.v6 (on Debian/Ubuntu with
iptables-persistent):
|
|
Example ruleset:
# /etc/iptables/rules.v6
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:DOCKER - [0:0]
# Allow established connections
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow loopback
-A INPUT -i lo -j ACCEPT
# Allow ICMPv6 (Neighbor Discovery, MLD, etc.)
-A INPUT -p ipv6-icmp -j ACCEPT
# Allow SSH and Docker-published ports
-A INPUT -p tcp --dport 22 -j ACCEPT
-A INPUT -p tcp --dport 80 -j ACCEPT
-A INPUT -p tcp --dport 443 -j ACCEPT
# Allow Docker bridge traffic
-A INPUT -i docker0 -j ACCEPT
# Log and drop everything else
-A INPUT -j LOG --log-prefix "IPv6-DROP: "
-A INPUT -j DROP
# Forward rules to accept Docker NAT traffic
-A FORWARD -i docker0 -j ACCEPT
-A FORWARD -o docker0 -m state --state ESTABLISHED,RELATED -j ACCEPT
COMMIT
Apply:
|
|
Important: Docker inserts its own rules on restart. If you
use a custom ruleset, ensure Docker’s ip6tables rules chain
properly. The safest approach is to set your default policies
and let Docker manage its own chains — do not flush the DOCKER
chain.
Step 6 — DNS and Reverse DNS for Container IPv6
Containers on a user-defined bridge get DNS via Docker’s embedded DNS resolver (127.0.0.11), which handles both A and AAAA records seamlessly. But external DNS is your responsibility.
For public-facing containers, add AAAA records alongside your existing A records:
app.example.com. IN A 192.168.1.100
app.example.com. IN AAAA 2001:db8:1234:ab::100
If your ISP delegates a reverse zone (ip6.arpa delegation), set up PTR records too. Most homelab ISPs do not provide this, but you can run your own authoritative DNS internally with PowerDNS or Bind9 and delegate within your network.
For internal service discovery with Traefik, Docker’s DNS resolver handles AAAA queries natively. Containers on the same network can reach each other by service name over IPv6 or IPv4 — whichever the client requests.
Troubleshooting Common IPv6 Docker Issues
Container cannot reach the internet over IPv6
Check that the host has a working IPv6 default route:
|
|
If missing, enable SLAAC or DHCPv6 on the host. Docker bridges do not run their own router advertisements — containers rely on the host’s global connectivity.
Port publishing binds IPv4 only
Ensure daemon.json has "ipv6": true and Docker was restarted.
Without it, -p 80:80 only creates an IPv4 iptables rule.
Check:
|
|
If empty, IPv6 port publishing is not working.
Containers cannot ping each other’s IPv6 addresses
User-defined bridge networks forward traffic by default, but the container must have an IPv6 default route. On bridge networks, Docker adds a default route via the gateway address. Verify:
|
|
“IPv6 is not supported by the kernel” on older kernels
Docker IPv6 requires the ip6_tables kernel module. Load it:
|
|
This is rare on modern kernels but can surface in minimal Docker images running Docker-in-Docker.
Summary
Enabling IPv6 in your Docker homelab is a few lines of JSON and one daemon restart. The payoff is significant: containers get globally routable addresses, services bypass carrier-grade NAT, and your infrastructure is ready for an IPv6-dominant internet.
The complete setup checklist:
- Add
ipv6: trueand afixed-cidr-v6ULA todaemon.json - Optionally add IPv6
default-address-poolsfor auto-allocation - Configure Compose networks with
enable_ipv6: trueand explicit subnets - Set Traefik entry points to
[::]:80and[::]:443 - Lock down with
ip6tables— default DROP on INPUT - Add AAAA DNS records for public services
IPv4 is not going away tomorrow, but dual-stack is the standard today. Your homelab should speak both.