A macvlan container (ipvlan behaves the same) has no IPv6 by default: a link-local address and nothing else, no default route. The network is fine — Docker sets disable_ipv6 to 1 on the container interface whenever the network’s EnableIPv6 is false.
Here is the configuration that works on a home connection, where the ISP hands out the prefix dynamically.
Confirm that’s the problem
$ docker exec <container> ip -6 addr show eth0
inet6 fe80::ef5:b3ae:a64e:e782/64 scope link # link-local only
$ docker exec <container> cat /proc/sys/net/ipv6/conf/eth0/disable_ipv6
1 # administratively off
Do not declare a v6 subnet on the docker network
The official-looking version is wrong for residential broadband:
networks:
macvlan_net:
enable_ipv6: true
ipam:
config:
- subnet: 2408:xxxx:xxxx:xxx::/64 # ← don't
That /64 came from the ISP over DHCPv6-PD, and it changes. When it does, the container keeps an address carved out of a prefix that no longer routes to your house. IPv6 fails silently while the config file still shows an address that looks perfectly reasonable.
Pinning the subnet only makes sense when the prefix is static: your own PI allocation, a fixed assignment in a datacenter, or a ULA you intend to NPT.
Otherwise declare no v6 subnet at all. Just clear disable_ipv6 and let the kernel do SLAAC from the router’s RAs. The prefix changes, the kernel follows, and the config never moves.
The configuration
services:
myapp:
networks:
macvlan_net:
ipv4_address: 10.0.0.6
driver_opts:
com.docker.network.endpoint.sysctls: "net.ipv6.conf.IFNAME.disable_ipv6=0,net.ipv6.conf.IFNAME.accept_ra=2"
sysctls:
net.ipv6.conf.all.disable_ipv6: 0
net.ipv6.conf.all.forwarding: 1 # only if the container forwards traffic
networks:
macvlan_net:
driver: macvlan
driver_opts:
parent: enp1s0
ipam:
config:
- subnet: 10.0.0.0/24
gateway: 10.0.0.1
Four things that trip people up:
Interface-scoped sysctls cannot go under
sysctls:. Recent Docker rejects them outright:Error response from daemon: interface specific sysctl setting "net.ipv6.conf.eth0.accept_ra" must be supplied using driver option 'com.docker.network.endpoint.sysctls'The interface may not exist when the container is created, and a container can join several networks, so hardcoding
eth0is ambiguous. These settings belong to a network endpoint.IFNAMEis a literal placeholder. Do not substituteeth0. Docker replaces it with the real interface name as it moves the interface into the container — which is the entire point of the API.Separate multiple settings with commas inside one string.
Endpoint options live under
services.<name>.networks.<network>.driver_opts, not the top-leveldriver_optsin thenetworks:block. Those are different things: the top-level one configures the network driver (parent), this one configures the endpoint.
Settings without an interface name — net.ipv6.conf.all.*, net.ipv4.ip_forward — stay in sysctls: as before.
Why accept_ra must be 2
Skip this and everything else is wasted, with no error to tell you.
Linux assumes that a netns with IPv6 forwarding enabled is a router, and routers should not listen to other people’s RAs — so accept_ra=1 is silently ignored. A container acting as a gateway or transparent proxy almost certainly has forwarding on. The two collide and you end up staring at disable_ipv6 = 0 and accept_ra = 1 with still no address.
accept_ra=2 means exactly “accept RAs even while forwarding”. If you want both forwarding and SLAAC, it has to be 2.
Without forwarding, accept_ra=1 works fine — but 2 has no downside, so just always write 2.
Verify
$ docker exec <container> ip -6 addr show eth0 | grep global
inet6 2408:xxxx:xxxx:xxx:a4be:81ff:fead:d100/64 scope global dynamic
$ docker exec <container> ip -6 route | grep default
default via fe80::xxxx:xxxx:xxxx:xxxx dev eth0 metric 1024 expires 0sec
$ docker exec <container> ping -c4 <a v6 target>
4 packets transmitted, 4 packets received, 0% packet loss
Global address, default route, replies coming back. Done.
Ignore the alarming expires
inet6 2408:xxxx:xxxx:xxx:a4be:81ff:fead:d100/64 scope global dynamic
valid_lft 3061sec preferred_lft 2469sec
It reads like the address dies in 50 minutes. It doesn’t. That’s the lifetime carried in the RA, and every RA resets it. The router advertises far more often than the lifetime, so the counter keeps getting pushed back up. The host shows identical numbers, because both are listening to the same RA.
What happens when the prefix does change
This is the payoff. The line drops and redials, the router gets a new PD, it advertises the new prefix while setting the old one’s lifetime to 0, and the container’s kernel deprecates the old address and configures a new one. No config edit, no restart. The only cost is that established connections break once and reconnect.
With a pinned subnet, the same event means editing the compose file, recreating the network, and recreating the container — after first working out that this is what went wrong.
Wrap-up
- No IPv6 in a macvlan container is usually Docker’s
disable_ipv6=1, not a broken network - Residential prefixes are dynamic — keep the v6 subnet out of the docker network and let the container do SLAAC
- Interface-scoped sysctls must go through
com.docker.network.endpoint.sysctls, withIFNAMEas the placeholder - With forwarding on,
accept_ramust be 2 or SLAAC fails silently - The
valid_lftcountdown is refreshed by every RA — leave it alone
For the mihomo gateway this came out of, see Running mihomo as a LAN Gateway with Docker macvlan.