A /48 of IPv6 on your server is a block of 65,536 separate /64 networks, about 1.2 septillion addresses, that belongs to that one server. You can use it to give every website its own address, send outgoing requests from a chosen address, put Docker containers on real public IPv6 without NAT, and set reverse DNS per address for mail. RS Computers gives every VPS and VDS in Amsterdam and Dublin its own /48 IPv6 prefix. This guide is a set of short recipes for doing those things on Debian or Ubuntu. We ran every command in our lab first, and each recipe shows what we saw.
Key facts, checked on 9 October 2026:
- A /48 holds 65,536 subnets of /64 and 1,208,925,819,614,629,174,706,176 addresses (we counted them with Python's
ipaddressmodule below). - RFC 6177 (March 2011) says an end site should get at least a /64 and "in most cases significantly more". It dropped /48 as the one fixed default, so a whole /48 per server is generous.
- The prefix 2001:db8::/32 is reserved for documentation by RFC 3849. All example addresses in this article use it, so nothing you copy by mistake will hit a real network.
- Google's IPv6 statistics measured 46.57% of users reaching Google over IPv6 on Wednesday 7 October 2026, and 51.69% on Saturday 3 October. Per country it showed France 87.15%, Germany 75.69%, the United States 54.33%, the Netherlands 53.37% and Ireland 39.06%.
- APNIC Labs counted 43.56% of users worldwide as IPv6 capable on 7 October 2026, with 52.29% in the Netherlands and 43.65% in Ireland.
What a /48 is, in plain numbers
An IPv6 address has 128 bits, written as eight groups of four hex digits. The number after the slash says how many bits are fixed. In 2001:db8:abcd::/48 the first three groups are yours and fixed, and the other five groups are yours to fill in. The fourth group alone picks one of 65,536 subnets, and the last four groups pick an address inside that subnet.
| Prefix | What it is | How many |
|---|---|---|
| /128 | One single address | 1 |
| /64 | One standard subnet (one LAN, one Docker network, one VPN) | 18.4 quintillion addresses |
| /56 | What many home ISPs hand out | 256 subnets of /64 |
| /48 | What an RS Computers server in Amsterdam or Dublin gets | 65,536 subnets of /64 |
If the maths feels abstract, let Python count it for you (install it with apt install python3 if your server lacks it):
# as your normal user
python3 - <<EOF
import ipaddress
net = ipaddress.ip_network("2001:db8:abcd::/48")
subnets = list(net.subnets(new_prefix=64))
print(len(subnets), "subnets of /64")
print(subnets[0], subnets[1], subnets[0x10], subnets[-1])
print(f"{net.num_addresses:,} addresses in total")
EOF
Our output, with Python 3.13.5:
65536 subnets of /64
2001:db8:abcd::/64 2001:db8:abcd:1::/64 2001:db8:abcd:10::/64 2001:db8:abcd:ffff::/64
1,208,925,819,614,629,174,706,176 addresses in total
Write an address plan before you start, so an address in a log tells you what it is. The recipes below follow this one. Swap 2001:db8:abcd for the first three groups of your own prefix, which ip -6 addr shows.
| Range | Used for | Example |
|---|---|---|
| 2001:db8:abcd::/64 | The server itself: main address, websites, outgoing jobs | ::1 main, ::10 site A, ::11 site B, ::25 mail |
| 2001:db8:abcd:d0c::/64 and d0d::/64 | Docker networks with public IPv6, one /64 each | d0c::80 web container, d0d::90 Compose app |
| 2001:db8:abcd:20::/64 | A WireGuard VPN, so each device gets a real IPv6 | ::2 laptop, ::3 phone |
| 2001:db8:abcd:ff00::/56 | Kept free for later | Nothing yet |
How we tested this
We ran everything in containers on our Amsterdam test server (Debian 13), on a private bridge. A ULA prefix (a private IPv6 range from RFC 4193, the IPv6 version of 192.168.x.x) played your /48, and a second container played "the internet" so we could see where each request came from. In the outputs below we swapped our lab addresses for the 2001:db8:abcd examples so they match the commands. Versions: Debian 13 with systemd 257 and iproute2 6.15, Ubuntu 24.04 with netplan 1.1.2, nginx 1.26.3, Docker Engine 29.9.0, ufw 0.36.2 and nftables 1.1.3.
Recipe 1: add more addresses without changing your outgoing one
First find your interface name and the address you already have:
# as your normal user
ip -br link
ip -6 addr show dev eth0 scope global
The first command lists interfaces; use the one that is UP and is not lo. It was eth0 on our Amsterdam test server and may be ens3 or similar elsewhere. The second shows your main address and its prefix length, for example 2001:db8:abcd::1/48. Use the same prefix length for the new addresses.
Now add two addresses:
# as your sudo user
sudo ip -6 addr add 2001:db8:abcd::10/48 dev eth0
sudo ip -6 addr add 2001:db8:abcd::11/48 dev eth0
ip -6 -br addr show dev eth0
Both addresses showed up straight away, and a ping from the second machine got an answer from each one within a millisecond.
Then we found the catch most guides skip. We sent a plain request from the server, with no source address chosen, and the other side logged it as coming from ::11, the address we had added last. Linux had picked the newest address as the source for all outgoing IPv6 traffic. If your main address is whitelisted somewhere, or has a reverse DNS name, that change matters.
The fix is to mark extra addresses as "deprecated". They still accept incoming connections and you can still send from them on purpose, but Linux never picks them on its own:
# as your sudo user
sudo ip -6 addr change 2001:db8:abcd::10/48 dev eth0 preferred_lft 0
sudo ip -6 addr change 2001:db8:abcd::11/48 dev eth0 preferred_lft 0
ip -6 addr show dev eth0 scope global
Each extra address now shows deprecated and preferred_lft 0sec. In our test the next plain request left from the main address again, while requests that asked for ::10 or ::11 still used them. Addresses added with ip are gone after a reboot, which is what the next recipe fixes.
Recipe 2: keep the addresses after a reboot
Linux servers use one of three tools for network settings. Check which files exist on yours: /etc/network/interfaces means ifupdown (common on Debian VPS installs), /etc/netplan/ means netplan (Ubuntu), and a file in /etc/systemd/network/ means systemd-networkd. Use only the one your server uses. We rebooted the test machine after each of these, and all three brought the addresses back as deprecated extras.
ifupdown (Debian with /etc/network/interfaces)
Leave your existing IPv6 lines alone and add up lines under the inet6 block:
# as root, in /etc/network/interfaces, under your existing "iface eth0 inet6 static" block
up ip -6 addr add 2001:db8:abcd::10/48 dev eth0 preferred_lft 0
up ip -6 addr add 2001:db8:abcd::11/48 dev eth0 preferred_lft 0
netplan (Ubuntu)
Put the extra addresses in their own file, so an update to the main file never removes them. The lifetime: 0 option maps to systemd-networkd's PreferredLifetime=0, which marks the address deprecated, and the netplan docs say it works only with the networkd back end (the default on Ubuntu servers):
# as root
cat > /etc/netplan/60-extra-ipv6.yaml <<EOF
network:
version: 2
ethernets:
eth0:
addresses:
- "2001:db8:abcd::10/48":
lifetime: 0
- "2001:db8:abcd::11/48":
lifetime: 0
EOF
chmod 600 /etc/netplan/60-extra-ipv6.yaml
netplan generate && netplan apply
systemd-networkd
A drop-in file adds to the main .network file instead of replacing it. Use the folder that matches your file's name, here eth0.network:
# as root
mkdir -p /etc/systemd/network/eth0.network.d
cat > /etc/systemd/network/eth0.network.d/extra-ipv6.conf <<EOF
[Address]
Address=2001:db8:abcd::10/48
PreferredLifetime=0
[Address]
Address=2001:db8:abcd::11/48
PreferredLifetime=0
EOF
networkctl reload
After networkctl reload the address appeared as deprecated within seconds, and it came back the same way after a reboot. A typo in network files can cut you off from SSH. Make the change with the provider's web console open in another tab, so you can fix it from there.
Recipe 3: one address per website or service
With IPv4 all your sites share one address and the web server sorts them by name. With a /48 each site can have its own address: logs get easier to read, and a service that is not a website (a game server, an API, a mail server) gets a clean address of its own. Here two nginx sites each listen on one address (install nginx first with sudo apt install nginx):
# as root
cat > /etc/nginx/sites-available/site-a <<EOF
server {
listen [2001:db8:abcd::10]:80;
root /var/www/site-a;
}
EOF
cat > /etc/nginx/sites-available/site-b <<EOF
server {
listen [2001:db8:abcd::11]:80;
root /var/www/site-b;
}
EOF
mkdir -p /var/www/site-a /var/www/site-b
echo "site A" > /var/www/site-a/index.html
echo "site B" > /var/www/site-b/index.html
ln -s /etc/nginx/sites-available/site-a /etc/nginx/sites-enabled/
ln -s /etc/nginx/sites-available/site-b /etc/nginx/sites-enabled/
rm /etc/nginx/sites-enabled/default
nginx -t && systemctl restart nginx
ss -ltn | grep ':80 '
Use restart here, not reload. Our first try used reload, and nginx kept its old [::]:80 socket from the default site; the error log said bind() to [2001:db8:abcd::10]:80 failed (98: Address already in use). After a restart, ss listed one socket per address, and from the other machine curl "http://[2001:db8:abcd::10]/" returned "site A" while ::11 returned "site B". The main address no longer answered on port 80 at all. We rebooted three times, and nginx came up every time.
Then point each site's AAAA record (the IPv6 version of an A record) at its address. For HTTPS, the certificate steps are the same as with one address; our reverse proxy and SSL guide covers Caddy, nginx and Traefik.
Recipe 4: send outgoing requests from a chosen address
Some jobs should leave from their own address: a crawler, a monitoring check, an upload job, an API client that a partner whitelists. curl can choose its source address with --interface:
# as your normal user
curl -s -o /dev/null -w "%{http_code} from %{local_ip}\n" --interface 2001:db8:abcd::11 "http://[2001:db8:ffff::80]/"
ip -6 route get 2001:4860:4860::8888
We got 200 from 2001:db8:abcd::11, and the web server on the other side logged that same address. The second command shows which source Linux would pick for a destination on its own, which is handy after changes like the ones in Recipe 1. Other tools have the same option under another name, such as wget --bind-address or ping -I.
To move all outgoing IPv6 to another address, give the default route a preferred source. Use the gateway your server already has (the first command shows it):
# as your sudo user
ip -6 route show default
sudo ip -6 route replace default via YOUR_GATEWAY dev eth0 src 2001:db8:abcd::10
In our test, curl -6 to Google then left from ::10. Like ip addr, this lasts until the next reboot. For crawlers, our web scraping guide covers rate limits and polite crawling.
Recipe 5: public IPv6 for Docker containers
There are two ways to switch on IPv6 in Docker. They behave very differently.
The simple way: IPv6 with NAT
This gives containers IPv6 for outgoing traffic, hidden behind the server's address, the same way Docker handles IPv4. Docker's IPv6 page names a ULA range from fd00::/8 as a fitting choice here:
# as root
cat > /etc/docker/daemon.json <<EOF
{
"ipv6": true,
"fixed-cidr-v6": "fd00:d0c:1::/64"
}
EOF
systemctl restart docker
docker run --rm alpine:3.22 sh -c "ip -6 addr show eth0 scope global; ping -6 -c2 2001:4860:4860::8888"
The container got fd00:d0c:1::2/64 and reached Google over IPv6 with 0% packet loss. Docker added a masquerade rule for that range, which is NAT: the outside world sees your server's address, not the container's.
The /48 way: routed IPv6, no NAT
With thousands of spare /64s, you can hand one to Docker and give each container a real address. Docker calls this the routed gateway mode. Its port publishing docs say that in this mode "only published container ports are accessible", so the firewall stays closed by default:
# as your normal user (a member of the docker group)
docker network create --ipv6 --subnet 2001:db8:abcd:d0c::/64 \
-o com.docker.network.bridge.gateway_mode_ipv6=routed pub6
docker run -d --name web6 --restart unless-stopped --network pub6 \
--ip6 2001:db8:abcd:d0c::80 -p 80:80 nginx:1.29-alpine
docker run -d --name hidden6 --restart unless-stopped --network pub6 \
--ip6 2001:db8:abcd:d0c::81 nginx:1.29-alpine
Our first test from the other machine failed to connect at all, and its neighbour table showed the container's address as FAILED. On a server where the whole /48 sits on the main interface, the network in front of the server asks "who has this address?" on the wire, and nobody answered for the container. That question is NDP (neighbour discovery), the IPv6 version of ARP. The fix is to let the server answer for each container address, which is called proxy NDP:
# as your sudo user
sudo sysctl -w net.ipv6.conf.eth0.proxy_ndp=1
sudo ip -6 neigh add proxy 2001:db8:abcd:d0c::80 dev eth0
sudo ip -6 neigh add proxy 2001:db8:abcd:d0c::81 dev eth0
After that, from the other machine:
web6on port 80 answered200, and the nginx log inside the container showed the visitor's real IPv6 address. With NAT, every visitor would look like the server.hidden6, started without-p, timed out on port 80 but answered ping. Only published ports get through.docker psshowed[::]:0->80/tcpforweb6. In routed mode the container answers on its own port on its IPv6 address; the host port in-p 8080:80only applies to IPv4.- The ip6tables NAT table had a masquerade rule for the ULA range from the simple setup and none for
pub6.
The proxy entries disappear on reboot. On systemd-networkd you can make them permanent with a drop-in. In our lab the proxy entries came back after networkctl reload and again after a full reboot, and web6 answered on port 80 as before:
# as root
cat > /etc/systemd/network/eth0.network.d/docker-ndp.conf <<EOF
[Network]
IPv6ProxyNDP=yes
IPv6ProxyNDPAddress=2001:db8:abcd:d0c::80
IPv6ProxyNDPAddress=2001:db8:abcd:d0c::81
EOF
networkctl reload
The same kind of network in Docker Compose. It gets its own /64 (d0d, next to the d0c used above), and the container gets a fixed ipv6_address, because proxy NDP works one address at a time:
# as your normal user, in compose.yaml
services:
web:
image: nginx:1.29-alpine
restart: unless-stopped
ports:
- "8080:80"
networks:
pub6:
ipv6_address: 2001:db8:abcd:d0d::90
networks:
pub6:
enable_ipv6: true
driver_opts:
com.docker.network.bridge.gateway_mode_ipv6: routed
ipam:
config:
- subnet: 2001:db8:abcd:d0d::/64
# as your normal user, in the folder with compose.yaml
docker compose up -d
docker compose ps
sudo ip -6 neigh add proxy 2001:db8:abcd:d0d::90 dev eth0
docker compose ps listed the container as Up with [::]:0->80/tcp, and from the other machine port 80 on ::90 answered 200. Add the address to the docker-ndp.conf drop-in too, so it survives a reboot. Compose v5.6.0 refused our first version with "Pool overlaps with other one on this address space", because we had picked a range inside the network made by hand above. Each Docker network needs its own range, and with a /48 you can give every Compose project its own /64. The cost is tiny: in our lab each nginx container used about 3.3 MiB of RAM, and the whole test machine, with Docker and three web servers, used 134 MB. Installing Docker from Docker's own repository took 11.5 seconds. Our Docker on a VPS guide covers the install.
Recipe 6: reverse DNS for each address
Reverse DNS (a PTR record) maps an address back to a name. Mail servers check it, and it makes traceroutes and logs readable. Each address in your /48 can have its own name. Look one up with dig (package dnsutils on Debian and Ubuntu):
# as your normal user
dig -x 2001:4860:4860::8888 +short
dig +short AAAA dns.google
The first command printed dns.google. and the second gave back 2001:4860:4860::8888 among its answers. That round trip, address to name and name back to the same address, is what receiving mail servers want to see. Google's email sender guidelines require that "the sending IP address must match the IP address of the hostname specified in the Pointer (PTR) record", and they list a specific error for IPv6 senders without one. Any address without a name returns NXDOMAIN.
So if you run mail, give it its own address, for example ::25, with a PTR record, a matching AAAA record and the outgoing source set as in Recipe 4. On RS Computers, set the PTR in the client area, and if the option is not there for an address, send us the address and hostname on Telegram and we set it. Outbound port 25 is closed on new servers and opened on request through Telegram. Our mailcow guide covers the rest of a mail setup.
Recipe 7: a firewall that covers IPv6
IPv6 addresses are public. There is no NAT in front of them to hide a forgotten service, so the firewall has to cover both protocols.
ufw
ufw on Debian 13 ships with IPV6=yes in /etc/default/ufw, so each rule is added twice:
# as your sudo user
grep ^IPV6 /etc/default/ufw
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow proto tcp to 2001:db8:abcd::10 port 8080
sudo ufw --force enable
sudo ufw status numbered
Each plain allow printed two lines, one for IPv4 and one ending in "(v6)", and the status list showed 80/tcp (v6) next to 80/tcp. The rule with to printed only "Rule added (v6)", because it opens a port on that one IPv6 address and nothing else. From the other machine, ping and port 80 worked over IPv6 and a port with no rule timed out.
nftables
One inet table filters IPv4 and IPv6 together. This is /etc/nftables.conf from our test. Keep the ICMPv6 line:
# as root, in /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
# IPv6 breaks without these: neighbour discovery, path MTU, ping
meta l4proto ipv6-icmp accept
meta l4proto icmp accept
tcp dport { 22, 80, 443 } accept
# a port open on one address only
ip6 daddr 2001:db8:abcd::10 tcp dport 8081 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter;
}
}
# as root
nft -c -f /etc/nftables.conf && nft -f /etc/nftables.conf
systemctl enable --now nftables
Then we deleted the ICMPv6 line to see what happens. IPv6 to the server stopped completely: curl from the other machine failed, and its neighbour table showed the server as FAILED. IPv4 kept working, which is why this mistake is easy to miss. Unlike ICMP in IPv4, ICMPv6 carries neighbour discovery, so dropping it cuts the server off the local network. RFC 4890 lists which ICMPv6 types a firewall must let through. Accepting all ICMPv6, as above, is the simple safe choice.
We tested both firewalls on a machine without Docker. Do not load this nftables file as it is on a Docker host: flush ruleset deletes the rules Docker created, and a forward chain with policy drop blocks the traffic to and from the containers. Docker's page on packet filtering and firewalls explains how its rules fit next to your own.
Recipe 8: check that IPv6 works
# as root
ping -6 -c 3 google.com
curl -6 -s -o /dev/null -w "%{http_code} %{remote_ip}\n" https://www.google.com/
curl -6 -s https://api64.ipify.org; echo
dig +short AAAA example.com
On our test server the ping came back with 0% loss in under a millisecond, curl answered 200 from a Google IPv6 address, api64.ipify.org printed the server's public IPv6 address, and dig listed two IPv6 addresses for example.com. The ipify line is the quickest way to see which address the outside world gets from you after Recipes 1 and 4. To test incoming traffic, connect from another IPv6 machine, for example curl -6 "http://[2001:db8:abcd::10]/" from a second server. On your own computer, test-ipv6.com shows in the browser whether your home connection has IPv6 at all. Many still don't (Ireland stood at 39% in Google's data), and that is why every site should keep working over IPv4 too.
What a /48 does not do
- It does not replace IPv4. Half the users and many APIs are still IPv4 only, so keep the A record next to the AAAA.
- It is not a way around blocklists or rate limits. Many services treat a whole /64, and sometimes a whole /48, as one client, and abuse from one address can mark the whole prefix. Our acceptable use policy applies to every address in it.
- It does not open ports for you. A container or service on a public IPv6 address is reachable from anywhere unless the firewall says otherwise.
- It is not portable. The /48 belongs to the server. To take addresses with you between providers you need your own prefix and ASN, which our ASN and BGP guide explains.
Frequently asked questions
How many IPv6 addresses are in a /48?
A /48 has 2 to the power of 80 addresses, which is 1,208,925,819,614,629,174,706,176. More usefully, it holds 65,536 subnets of /64, the standard size for one network segment.
Is a /48 too much for one server?
It is more than one server needs for addresses alone, but RFC 6177 asks providers to give end sites "in most cases significantly more" than a single /64. The extra /64s are what make Docker, VPN clients and per-site addresses easy without NAT.
Why does my server send traffic from the IPv6 address I added last?
When several addresses fit a destination equally well, Linux picked the one added last in our tests. Add extra addresses with preferred_lft 0, or PreferredLifetime=0 in systemd-networkd and lifetime: 0 in netplan, and your main address stays the outgoing one.
Do Docker containers get public IPv6 automatically?
No. With "ipv6": true Docker uses NAT by default. For public addresses, create a network with a /64 from your prefix and com.docker.network.bridge.gateway_mode_ipv6=routed. When the prefix sits on the main interface, you also need proxy NDP for each container address.
Does ufw block IPv6?
Yes, when enabled. ufw on Debian 13 has IPV6=yes by default and adds a v6 copy of each rule. On older systems check /etc/default/ufw: the comment in that file says that with IPV6=no only IPv6 on loopback is accepted, so your services stop answering over IPv6. Change it to yes, then disable and enable ufw.
Can I set reverse DNS for every IPv6 address?
Yes, each address can have its own PTR record. On RS Computers you set it in the client area, or ask on Telegram with the address and the hostname. Make sure the hostname has a matching AAAA record.
Put your /48 to work
Every VPS and VDS in Amsterdam and Dublin comes with its own IPv4 address and its own /48 of IPv6, on KVM with NVMe storage and unmetered traffic, from the Nano plan up to VDS Large with 10 Gb/s. Servers in Prishtina have their own IPv4 and IPv6 as well. Availability by city is on the plans page. If you want help with an address plan, reverse DNS for a block of addresses, or port 25 for a mail address, write to us on Telegram.