Configuring IPv6 for Docker Containers: A Step-by-Step Dual-Stack Guide

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

One morning while deploying to a new server, I discovered the hosting provider had assigned a proper IPv6 address to the VPS — but all my Docker containers were still running on pure IPv4. What a waste. After about two hours of reading docs and trial-and-error, I finally understood why Docker doesn’t automatically use IPv6 even when the host already has it.

If your VPS has IPv6 but your containers are still only communicating over IPv4, this article addresses exactly that problem — from enabling the daemon to setting up a complete dual-stack configuration in Docker Compose.

Quick Start: Enable IPv6 for Docker in 5 Minutes

If you just need to get it done fast, here are the 3 minimal steps:

Step 1: Edit daemon.json

Open (or create) the file /etc/docker/daemon.json:

sudo nano /etc/docker/daemon.json

Paste the following (or merge if content already exists):

{
  "ipv6": true,
  "fixed-cidr-v6": "fd00::/80"
}

Step 2: Restart Docker

sudo systemctl restart docker

Step 3: Verify

docker network inspect bridge | grep -A 5 "EnableIPv6"

Seeing "EnableIPv6": true means Docker has picked up the configuration. Newly created containers in the default bridge network will now automatically be assigned an IPv6 address.

Going Deeper: How Does Docker Handle IPv6?

When I first used Docker Compose for a real project, I made quite a few rookie mistakes. One of the most classic: enabling IPv6 and then wondering why the container still couldn’t connect. The reason is that Docker has multiple network types, and each one requires its own configuration.

Docker Network Types

  • bridge (default): Containers on the same host communicate via NAT
  • host: Container uses the host’s network stack; IPv6 works if the host has it
  • overlay: Used in Docker Swarm, requires separate configuration
  • macvlan: Containers get their own MAC/IP address on the physical network

Setting ipv6: true in daemon.json only affects the default bridge network. Any custom network you create in Docker Compose — and this is what many people miss — must declare IPv6 support separately.

Which IPv6 Address Range to Use

For internal environments (no external routing needed), use the ULA (Unique Local Address) range — analogous to 192.168.x.x in IPv4:

fd00::/8   ← Private, suitable for internal use

Common examples:

  • fd00::/80 — for the default bridge
  • fd00:db8:1::/64, fd00:db8:2::/64 — for custom networks

/64 is the standard IPv6 subnet size per RFC. /80 works, but some networking tools and routers mishandle prefixes that aren’t /64. For production, stick with /64 to be safe.

Dual-Stack: Running IPv4 and IPv6 Simultaneously

Dual-stack simply means a container has both: IPv4 for backward compatibility with older systems, and IPv6 for future readiness. You don’t have to pick one — and this is the setup you should use by default.

Create a Dual-Stack Custom Network via CLI

docker network create \
  --driver bridge \
  --subnet 172.20.0.0/24 \
  --gateway 172.20.0.1 \
  --ipv6 \
  --subnet fd00:db8::/64 \
  --gateway fd00:db8::1 \
  myapp-network

Verify the result:

docker network inspect myapp-network | grep -A 20 '"IPAM"'

The output will contain both IPv4 and IPv6 config blocks:

"IPAM": {
    "Driver": "default",
    "Config": [
        {
            "Subnet": "172.20.0.0/24",
            "Gateway": "172.20.0.1"
        },
        {
            "Subnet": "fd00:db8::/64",
            "Gateway": "fd00:db8::1"
        }
    ]
}

View a Container’s IPv6 Address

docker run -d --name web --network myapp-network nginx:alpine

# Option 1: inspect
docker inspect web | grep -A 5 "GlobalIPv6"

# Option 2: check from inside the container
docker exec web ip -6 addr show

Configuring IPv6 in Docker Compose

In practice, I rarely use the docker network create command manually. Managing networks through Docker Compose is much cleaner:

services:
  web:
    image: nginx:alpine
    networks:
      - frontend

  api:
    image: node:18-alpine
    networks:
      - frontend
      - backend

  db:
    image: postgres:15
    networks:
      - backend

networks:
  frontend:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: 172.20.0.0/24
          gateway: 172.20.0.1
        - subnet: fd00:db8:1::/64
          gateway: fd00:db8:1::1

  backend:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: 172.21.0.0/24
          gateway: 172.21.0.1
        - subnet: fd00:db8:2::/64
          gateway: fd00:db8:2::1

Start the stack and test ping between services:

docker compose up -d
docker compose exec web ping6 api

Advanced: Exposing Containers via IPv6 Externally

Want external clients to connect to your containers over IPv6? There are two approaches depending on your needs.

Option 1: Port Mapping (Simplest)

In docker-compose.yml:

services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"       # IPv4
      - "[::]:80:80"  # IPv6 explicit

If the host already has IPv6 forwarding enabled and daemon.json has ipv6: true, then 80:80 usually maps both IPv4 and IPv6 automatically — the second line isn’t strictly necessary.

Option 2: NDP Proxy (For VPS with Real Public IPv6)

If your VPS has a public IPv6 range from the hosting provider (e.g. 2001:db8::/48), you can use NDP proxy to give containers a real public IPv6 address — no NAT involved.

Enable NDP proxy on the host:

sudo sysctl net.ipv6.conf.all.proxy_ndp=1

Add a proxy entry for the container:

sudo ip -6 neigh add proxy 2001:db8::2 dev eth0

This approach is more complex, but it pays off when containers genuinely need a real public IPv6 address. No NAT, no port mapping — traffic goes directly into the container. Best suited for bare metal or dedicated servers with a public IPv6 block.

Practical Tips

1. Test IPv6 Connectivity from Inside a Container

docker exec -it mycontainer ping6 google.com
docker exec -it mycontainer curl -6 https://ipv6.google.com

2. Avoid Subnet Conflicts

Each network must have its own subnet — for both IPv4 and IPv6. Docker reports an error on conflicts, but the message can be hard to read. Quickly view all subnets currently in use:

docker network ls -q | xargs docker network inspect | grep '"Subnet"'

3. Persist IPv6 Forwarding Across Reboots

Add this to /etc/sysctl.conf so the setting survives a reboot:

net.ipv6.conf.all.forwarding=1

Apply immediately without rebooting:

sudo sysctl -p

4. Nginx Inside a Container Needs to Listen on IPv6 Explicitly

If a container runs Nginx, add this line to the server block:

server {
    listen 80;
    listen [::]:80;  # add this line for IPv6

    server_name example.com;
}

5. Debug Traffic with tcpdump

sudo tcpdump -i docker0 ip6

Run this command when you suspect IPv6 traffic isn’t flowing correctly — you’ll immediately see whether any IPv6 packets are passing through the interface.

Summary

There are three layers to keep in mind when configuring IPv6 for Docker: the daemon, the network, and the container. Each layer is independent — enabling it at the daemon level does not mean custom networks automatically get IPv6. For most projects, setting ipv6: true in daemon.json and enable_ipv6: true in the Compose file is all you need.

I typically set up dual-stack from the start on every project — it’s just a few extra lines of config, but it saves a lot of headaches later when a hosting provider changes their IPv4 policy, or when you need to debug network issues between containers.

Share: