Using Docker with IPv6

I found a tool extending Docker with IPv6 NAT and an issue about it being replaced by a builtin Docker functionality and I thought that if Docker natively (but experimentally) supports IPv6 then it will be easy to configure, so I chose to enable that for my Mastodon instance. It took slightly more work than I expected.

Unlike much more complex earlier guides that I have seen, this approach uses NAT, just like with IPv4. My containers do not expose any public services not proxied by an nginx on their host, so I don't need a more complex solution or a pool of public IPv6 addresses from my VPS provider. I assume a recent Docker version, I'm running 20.10.21.

Configuring the builtin IPv6 support I found several issues:

  • Due to an IP address parsing bug, an address pool is used for only one subnet. So I need a separate default-address-pools entry for each.

  • Not a bug, but I found no information on the default for default-address-pools and I copied that from an example in the Docker documentation, so I also had to update my iptables rules to allow Prometheus to access the host's node_exporter from a different IP address.

  • docker-compose.yml version 3 doesn't support enabling IPv6 (since it limits its functionality to what Docker Swarm supports; similarly it doesn't have memory limits without Swarm); so I upgraded to version 2.4.

  • docker-compose up -d and Ansible docker_compose module do not recreate networks, so their changes are not applied.

  • Mastodon failed to connect to PostgreSQL on IPv6; I don't need IPv6 for the internal network used for its PostgreSQL and Redis, while there is an issue about Redis on IPv6, so there is nothing here for me to debug and I keep using only IPv4 for that network.

So my /etc/docker/daemon.json evolved into this (with more single subnet address pools omitted):

{
  "default-address-pools": [
    {
      "base": "172.30.0.0/16",
      "size": 24
    },
    {
      "base": "172.31.0.0/16",
      "size": 24
    },
    {
      "base": "fd00:0000:0000:00::/64",
      "size": 64
    },
    {
      "base": "fd00:0000:0000:01::/64",
      "size": 64
    },
    {
      "base": "fd00:0000:0000:02::/64",
      "size": 64
    },
    {
      "base": "fd00:0000:0000:03::/64",
      "size": 64
    }
  ],
  "experimental": true,
  "features": {
    "buildkit": true
  },
  "fixed-cidr-v6": "fd00::/80",
  "ip6tables": true,
  "ipv6": true
}

(BuildKit is an unrelated feature for a much better image building experience, I use it on my laptop.)

I tested that containers support IPv6 (here with a large latency on my laptop):

$ docker run --rm busybox ping -c1 -6 mtjm.eu
PING mtjm.eu (2a01:7e01::f03c:91ff:fefb:b063): 56 data bytes
64 bytes from 2a01:7e01::f03c:91ff:fefb:b063: seq=0 ttl=49 time=47.265 ms

--- mtjm.eu ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 47.265/47.265/47.265 ms

Several docker-compose.yml files now have:

networks:
  default:
    enable_ipv6: true

while others specify IPv6 for external networks:

networks:
  external_network:
    enable_ipv6: true
  internal_network:
    internal: true

And in several directories I did docker-compose down and then docker-compose up -d.

I used this inelegant command to find containers not having IPv6:

for f in $( docker ps --format '{{.ID}}') ; \
  do docker inspect $f | grep fd00 || echo $f ; \
done

and it listed only the databases on internal networks.

So now all my containers talking to the outside world can access IPv6 services. But I don't know e.g. any IPv6-only Mastodon instance to check if mine can communicate with it.