
Your VPS came with an IPv4 address and an IPv6 address, and you are not sure what the second one does. Maybe the site loads at home but times out for a friend on mobile data, certbot fails while your browser shows the page, or you wonder if you could drop IPv4 to save money. Having both addresses doesn't mean your app uses both. This guide covers who can reach which address, what your server does with each, and when to publish an AAAA record. The commands work on Ubuntu 24.04 and Debian 12.
Quick answer: Keep IPv4. On 29 October 2026 more than half of Google's users still reached it without IPv6, and an IPv6-only server can't reach IPv4-only hosts such as github.com without NAT64. Add IPv6 to a name only when it works end to end: the service listens on it (* or [::] in ss -ltn), the firewall lets it in, and curl -6 from another machine succeeds. Then publish the AAAA record. A wrong AAAA record is worse than none, because clients and Let's Encrypt try IPv6 first. An app behind nginx can stay on 127.0.0.1; only nginx has to listen on both.
Who Can Reach Which Address
A server with both is dual stack, and RFC 4213, which defines it, notes that whether a particular application is available on each stack "is explicitly not defined". The addresses exist, but every program decides for itself where it listens.
Clients decide the rest. Google's IPv6 statistics put the share of its users who reached it over IPv6 at 47.2% on 29 October 2026, with big gaps between countries: 76% of its users in Germany, 52% in the Netherlands and 19% in Poland. Everyone else arrives over IPv4, so a public service needs an IPv4 address. The other direction is easier: an IPv6-only network can reach IPv4 servers through NAT64, which RFC 6146 describes as allowing "IPv6-only clients to contact IPv4 servers".
When a client has both and your name has A and AAAA records, IPv6 goes first: RFC 6724 says applications "will use IPv6 in preference to IPv4 when the two are equally suitable". Browsers race the two with Happy Eyeballs (RFC 8305), giving IPv6 a head start of 300 ms in Firefox and Chrome according to curl's documentation. Clients that try one address after another wait for IPv6 to fail first, so a bad AAAA record shows up as slow or failing tools while the browser looks fine.
Outbound counts too. When we checked for this post, dig AAAA github.com returned no address, so an IPv6-only server can't clone from GitHub without NAT64.
Step 1: See Which Addresses Your VPS Has
ip -brief address show
ip -6 address show scope global
ip -6 route show default
-brief prints each interface on one line with its addresses. Read them like this:
203.0.113.10/24: an IPv4 address, aninetline in the long format.2001:db8:1234::10/64,scope globalin the long format: an IPv6 address that can be public. The exception is addresses starting withfd: unique local addresses, which RFC 4193 says "are not expected to be routable on the global Internet".fe80::withscope link: link-local, "valid only on this device" in the words of the ip-address man page. RFC 4291 requires one on every IPv6 interface, so it proves nothing about internet access.
The route command should print a line starting with default via; ip-route treats default as ::/0 for IPv6. No line means no way out over IPv6, even with a global address. Then test each protocol outbound against Google Public DNS, whose documented addresses include 8.8.8.8 and 2001:4860:4860::8888:
ping -4 -c 3 8.8.8.8
ping -6 -c 3 2001:4860:4860::8888
If you expected IPv6 but see only fe80::, ask your provider for the address, prefix length and gateway. Every Space-Node VPS includes both IPv4 and IPv6, so there the only question is what your software does with them.
Step 2: Make Each Public Service Listen on Both
sudo ss -ltunp
-l shows listening sockets, -t and -u TCP and UDP, -n numbers and -p the process. The Local Address column shows which protocol each socket takes:
0.0.0.0:443: IPv4 only.[::]:443: IPv6 only; fine next to a0.0.0.0:443line for the same program.*:443: one socket for both. The kernel settingnet.ipv6.bindv6onlydefaults to 0, so a program that binds::without the IPV6_V6ONLY option also gets IPv4, and ss prints such a socket as*.127.0.0.1or[::1]: reachable only from the server itself.
Defaults differ per program:
| Software | Default | For IPv4 and IPv6 |
|---|---|---|
| nginx | listen 443 is IPv4 only, listen [::]:443 is IPv6 only | Both lines in every server block |
| Apache | Listen 80 takes both; Linux builds use IPv4-mapped addresses by default | Nothing to change |
| Node.js | server.listen(port) without a host binds ::, which also takes IPv4 | Don't pass '0.0.0.0' as the host |
| Java | An IPv6 socket that accepts both | Leave out -Djava.net.preferIPv4Stack=true |
| OpenSSH | AddressFamily any on all local addresses | Nothing to change |
| Docker | -p 8080:80 publishes on all host addresses, IPv4 and IPv6 | Nothing to change, but read step 3 |
The nginx row needs the most care. Its address parser turns a bare port into an IPv4 wildcard, and ipv6only "is turned on by default" (listen docs). nginx picks a server block by address and port first and by server_name second (request processing), so a block without the [::] line doesn't exist for IPv6 visitors: they get the default server of [::]:443, another site with another certificate, or a refused connection if no block listens there. Give each block both lines:
listen 443 ssl;
listen [::]:443 ssl;
Apply it with sudo nginx -t && sudo systemctl reload nginx. Our Cloudflare 521 guide shows a complete server block.
An app that only listens on IPv4 can stay that way behind a reverse proxy: nginx listens on both and passes requests to http://127.0.0.1:3000. Write the literal address there; our nginx 502 guide explains how localhost can end up meaning ::1.
Step 3: Open the Firewall for Both
Several of those programs listen on both by default, so a firewall that only filters IPv4 leaves them open over IPv6. With UFW, check:
grep IPV6 /etc/default/ufw
sudo ufw status
IPV6=yes is the default in ufw 0.36.2, the version in Ubuntu 24.04 and Debian 12. With no, the same file says, "only IPv6 on loopback" is accepted, so your public IPv6 address goes dark while UFW runs. After a change, run sudo ufw disable and then sudo ufw enable.
A rule without an address, such as sudo ufw allow 443/tcp, covers both protocols, and ufw status lists the IPv6 copy with (v6). A rule with a source address covers only that address's protocol, so allow your IPv6 admin address separately:
sudo ufw allow proto tcp from 2001:db8:5::7 to any port 22
Leave ICMPv6 alone. UFW's before6.rules already accepts the types RFC 4890 says must not be dropped, such as neighbor discovery and Packet Too Big. IPv6 routers don't fragment packets, so Packet Too Big is "vital to the correct functioning of Path MTU Discovery", and without it, in the RFC's words, "parts of the Internet will become inaccessible". Don't carry over the IPv4 habit of dropping all ICMP.
Without UFW, an nftables table of family inet covers both protocols, while ip and ip6 tables cover one each. With iptables, IPv6 rules go into ip6tables. Docker warns that traffic to published ports "gets diverted before it goes through the ufw firewall settings"; our Docker and UFW guide covers that, IPv6 included.
Step 4: Test From Outside, One Protocol at a Time
Run these on your own computer, not on the VPS. The IPv6 lines need IPv6 on your side too; test-ipv6.com tells you, and if it fails, test from a second server.
curl -4 -sS -o /dev/null -w '%{http_code} %{remote_ip}\n' https://example.com/
curl -6 -sS -o /dev/null -w '%{http_code} %{remote_ip}\n' https://example.com/
-4 and -6 make curl use only IPv4 or only IPv6 addresses, and remote_ip prints the address that answered; both lines should print the same status code. Before the AAAA record exists, aim curl at the new address with --resolve, which takes IPv6 addresses in brackets (since curl 7.57.0, so Ubuntu 24.04 and Debian 12 both have it). Keep the quotes, or the shell may treat the brackets as a pattern:
curl -sS -o /dev/null -w '%{http_code} %{remote_ip}\n' \
--resolve 'example.com:443:[2001:db8:1234::10]' https://example.com/
For other services, test each port over IPv6:
ssh -6 user@example.com
nc -6 -zv -w 5 2001:db8:1234::10 25565
-6 limits ssh and nc to IPv6, and -z makes nc only check that something listens. For UDP and nmap, see the Docker guide's test section. Check the DNS records with dig +short A example.com and dig +short AAAA example.com; if dig is missing, it is in the bind9-dnsutils package.
Step 5: Publish the AAAA Record Last
Add the AAAA record once every port that the name serves passes step 4 over IPv6, and delete AAAA records you don't use. A wrong record hurts more than a missing one, and certificates show it first: Let's Encrypt "will always prefer the IPv6 addresses for the initial connection" and retries over IPv4 only after a connection timeout, not after other errors. If something answers on the IPv6 address, like an nginx default server, validation fails without a retry, and redirects never get one. Its docs add: "There is not a way to request Let's Encrypt prefer IPv4, you must fix the misconfiguration."
Behind Cloudflare's proxy, your server doesn't need IPv6 for web traffic. Cloudflare's IPv6 compatibility is on by default and generates AAAA records for proxied names, so IPv6 visitors reach Cloudflare, which "will prefer the IPv4 address when connecting to your origin server" if a record has both. If your software only understands IPv4-formatted visitor addresses, the same page points to Pseudo IPv4.
Which One Fits
| Setup | Records | Who reaches it | Fits |
|---|---|---|---|
| IPv4 only | A | Everyone; IPv6-only networks through NAT64 | Software that can't listen on IPv6, or until your IPv6 test passes |
| Dual stack | A and AAAA | Everyone, IPv6 clients directly | Public sites, APIs and game servers that listen on both |
| IPv6 only | AAAA | Clients with IPv6, 47.2% of Google's users | Internal services between machines that all have IPv6 |
Aim for dual stack on anything public, and keep a name IPv4 only until its software passes the IPv6 test.
FAQ
Can I save money with an IPv6-only VPS? Some providers bill IPv4 separately, as our VPS price checklist shows, but an IPv6-only server can't be reached by clients without IPv6 and can't fetch from IPv4-only hosts such as github.com. For anything public, keep IPv4.
Should I turn IPv6 off on my VPS?
Usually not. Without an AAAA record, clients use IPv4 for that name, and with IPV6=yes, UFW's default incoming policy covers the IPv6 address too. If IPv6 is half working, see the next question.
Why do apt and curl hang on my VPS?
Probably a global IPv6 address without a working route, the case RFC 6724 calls broken IPv6: programs try IPv6 first and run into timeouts. Check step 1 and ask your provider to fix the route. Meanwhile, sudo apt-get -o Acquire::ForceIPv4=true update makes apt use IPv4 only (apt.conf), and this line, which the comments in /etc/gai.conf (glibc's copy) suggest for sites that prefer IPv4, makes programs started afterwards do so:
echo 'precedence ::ffff:0:0/96 100' | sudo tee -a /etc/gai.conf
Remove it again once IPv6 works.
Why does certbot fail when the site loads in my browser?
Your browser fell back to IPv4 and Let's Encrypt didn't. Run dig +short AAAA example.com; if an address comes back, test it with the --resolve command from step 4. Then fix the listen lines or the firewall, or delete the record, and run certbot again.
Why does Gmail bounce my server's mail with an IPv6 error?
The bounce reads "550-5.7.1: Message does not meet IPv6 sending guidelines regarding PTR records and authentication." Google's sender guidelines require a PTR record for the sending address, pointing to a hostname whose A or AAAA record resolves back to that address. Ask your provider for reverse DNS on the IPv6 address as well, or set inet_protocols = ipv4 in Postfix's main.cf; Postfix says you "MUST stop and start" it after that change.
Why do my logs show addresses like ::ffff:203.0.113.5?
That is an IPv4 client on a dual-stack socket. The ipv6 man page explains that an IPv4 connection to an IPv6 socket gets its source address "mapped to v6". The part after ::ffff: is the client's IPv4 address.
Sources
All opened on 31 October 2026:
- RFCs: 4213 dual stack, 6724 address selection, 8305 Happy Eyeballs, 6146 NAT64, 4890 ICMPv6 filtering, 4193 unique local addresses, 4291 IPv6 addressing
- Google: IPv6 statistics with its adoption data and per-country data, Public DNS addresses, email sender guidelines
- Linux: ip(8), ip-address(8), ip-route(8), ss(8), ss.c, ipv6(7), IP sysctl, ping(8), gai.conf(5), glibc gai.conf and getaddrinfo.c
- nginx: listen, How nginx processes a request, ngx_inet.c
- Other software: Apache binding, Node.js net, Java networking properties, sshd_config(5), ssh(1), Docker bridge driver, Docker packet filtering, Postfix postconf(5), apt.conf(5)
- Firewalls: ufw(8), ufw.defaults 0.36.2, before6.rules, backend_iptables.py, nft(8), iptables(8)
- Packages: ufw for Ubuntu 24.04 and Debian, bind9-dnsutils for Ubuntu 24.04 and Debian, curl for Ubuntu 24.04 and Debian 12
- Testing: curl man page, CURLOPT_RESOLVE, nc(1), dig(1), bash(1), test-ipv6.com
- Let's Encrypt IPv6 support, Cloudflare IPv6 compatibility