2026.09 / ACME over IPv6 059
Coolify Let’s Encrypt failing? Check a stale AAAA record
Your Coolify application works over IPv4, its A record points to the correct VPS, and ports 80 and 443 are open—yet Let’s Encrypt still will not issue a certificate. Before restarting Traefik or repeatedly redeploying, inspect the hostname’s AAAA record.
A forgotten IPv6 record can create two different public routes for one name. Normal tests may use the working IPv4 route while the certificate authority reaches an old, unreachable, or incorrectly configured IPv6 destination. The result looks like an intermittent Coolify or ACME fault even though DNS is behaving exactly as published.
If a hostname publishes both A and AAAA records, treat IPv4 and IPv6 as separate production paths. “The domain points to this server” is not proven until both paths agree.
Why AAAA can break an otherwise healthy certificate request
Let’s Encrypt’s official IPv6 guidance says that when a name has both A and AAAA records, validation initially prefers IPv6. It may retry IPv4 after a connection timeout, but it does not safely recover from every bad IPv6 response. For example, an IPv6 web server that answers with the wrong content or challenge response can make validation fail rather than fall back.
For the usual HTTP-01 flow, Let’s Encrypt requests a token below /.well-known/acme-challenge/. Its challenge documentation says HTTP-01 begins on port 80 and only follows redirects to HTTP or HTTPS on ports 80 or 443. Coolify’s proxy must therefore receive the request for the exact hostname on the address the validator chooses.
Recognise the stale-AAAA pattern
This failure is plausible when several of these observations line up:
curl -4reaches the expected Coolify application, butcurl -6times out or returns another site.- The
Arecord was changed during a VPS migration, but theAAAArecord was not. - The new server has no routed IPv6 address, although public DNS still advertises one.
- Traefik listens on IPv4 but the host firewall, provider firewall, or Docker path blocks IPv6.
- Certificate logs mention authorization failure, timeout, connection refusal, unexpected status, or the wrong challenge content.
- Some visitors see a different certificate or application depending on their network.
Do not infer the cause from a browser alone. Many client networks prefer one address family, and a proxied Cloudflare record can hide the origin route. Compare DNS and force each protocol explicitly.
1. Inventory every public DNS answer
Query more than one recursive resolver, then query the authoritative nameserver if answers disagree:
dig +short A app.example.com
dig +short AAAA app.example.com
dig +short CNAME app.example.com
dig @1.1.1.1 +short A app.example.com
dig @1.1.1.1 +short AAAA app.example.com
dig @8.8.8.8 +short A app.example.com
dig @8.8.8.8 +short AAAA app.example.com
dig +trace app.example.com
Cloudflare documents that A records map names to IPv4 addresses and AAAA records map names to IPv6 addresses. Having both is correct only when both destinations intentionally serve this hostname. A documentation-only example might use 192.0.2.10 and 2001:db8::10; replace neither placeholder with a real address in a public incident report.
Also inspect CNAME flattening, wildcard records and delegated subdomains. Editing one visible record does not prove that every resolver sees the same final address set. Note each TTL before making a change so you know how long stale recursive answers may remain.
2. Force HTTP over IPv4 and IPv6
Test the same hostname, path and port through each address family:
curl -4 --connect-timeout 10 -I http://app.example.com/
curl -6 --connect-timeout 10 -I http://app.example.com/
curl -4 --connect-timeout 10 -I https://app.example.com/
curl -6 --connect-timeout 10 -I https://app.example.com/
Record whether each request connects, which status it returns, where it redirects, and which server or application answers. A redirect from HTTP to HTTPS is not enough by itself; both protocols must route the exact hostname to the intended proxy, and the HTTPS destination must eventually present a suitable certificate.
If your current network has no IPv6 connectivity, run the IPv6 test from a known dual-stack system or use an external DNS/HTTP probe. Do not mistake a local “network unreachable” error for proof that the server’s IPv6 route is broken.
3. Bypass DNS while preserving the hostname
When you know the intended addresses privately, curl --resolve separates DNS selection from HTTP virtual-host routing. Keep the hostname in the URL so Host and TLS SNI remain correct:
# Documentation addresses only
curl --resolve app.example.com:80:192.0.2.10 \
-I http://app.example.com/
curl --resolve 'app.example.com:80:[2001:db8::10]' \
-I http://app.example.com/
curl --resolve 'app.example.com:443:[2001:db8::10]' \
-I https://app.example.com/
If the expected IPv6 address is reachable but returns a different site, inspect Coolify’s domain assignment and Traefik router rather than deleting the DNS record immediately. If IPv6 does not reach this host at all, the honest choices are to complete the IPv6 path or stop advertising it.
4. Check the Coolify host’s IPv6 path end to end
Run read-only checks on the server. The exact interface, firewall and proxy names vary, so first observe rather than copy a destructive fix:
ip -6 address show scope global
ip -6 route show
ss -lnt '( sport = :80 or sport = :443 )'
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker logs --since 20m COOLIFY_PROXY_CONTAINER
Confirm that the published AAAA address belongs to this server, the provider routes it, the host has a usable default IPv6 route, inbound TCP 80 and 443 are allowed in provider and host firewalls, and Coolify’s proxy receives requests on that path. A global IPv6 address on an interface does not by itself prove inbound reachability.
Search proxy logs around one controlled certificate attempt. Preserve the authorization hostname, timestamp and error class, but redact account identifiers, real origin addresses, tokens and private configuration before sharing.
5. Choose one of two correct repairs
Option A: make IPv6 real
Keep the AAAA record only if you intend to operate IPv6. Point it to the current server, complete provider routing and firewall rules, ensure the Coolify proxy handles ports 80/443, and prove that the hostname reaches the same application over both address families. This preserves dual-stack availability.
Option B: remove the AAAA record
If the service is intentionally IPv4-only, delete the stale AAAA record from the authoritative DNS provider. This is better than publishing an IPv6 route that cannot serve the name. Wait at least the old record’s TTL, then confirm multiple resolvers no longer return it.
Do not replace the origin AAAA record with an arbitrary address, disable TLS verification, or globally block IPv6 as a shortcut. Fix the public claim made by DNS or the route that is supposed to satisfy it.
Cloudflare proxying changes what public DNS proves
With an orange-clouded record, public A and AAAA answers can be Cloudflare edge addresses rather than your origin. A successful public curl -6 then proves the client-to-Cloudflare path, not Cloudflare-to-origin IPv6 and not a direct ACME path.
Coolify’s official Let’s Encrypt troubleshooting guide recommends checking correct DNS, ports 80/443 and Cloudflare proxy settings. For a controlled HTTP-01 diagnosis, temporarily switching the application record to DNS only can expose the actual origin route—provided that direct exposure fits your architecture and firewall plan. If you use Cloudflare Tunnel, an Origin CA certificate, or DNS-01 deliberately, follow that design instead of mixing validation methods.
During DNS-only testing, inspect A and AAAA again. A stale record can be invisible while proxied and become active as soon as the cloud is turned off.
6. Retry once and verify the certificate itself
After DNS and reachability are correct and old TTLs have elapsed, trigger one controlled Coolify redeploy or proxy certificate retry. Repeated retries against a known-bad route add noise and can encounter certificate-authority rate limits.
Then verify the certificate over every address family you still publish:
openssl s_client -4 \
-connect app.example.com:443 \
-servername app.example.com \
-verify_hostname app.example.com </dev/null
openssl s_client -6 \
-connect app.example.com:443 \
-servername app.example.com \
-verify_hostname app.example.com </dev/null
Check for a successful verification result, a valid date range, and a Subject Alternative Name matching the hostname. Finally fetch a unique heading or release marker from the live page over IPv4 and, if retained, IPv6. A generic HTTP 200 can come from the wrong virtual host or an SPA fallback.
A compact stale-AAAA checklist
- Query A, AAAA and CNAME through multiple resolvers.
- Confirm every advertised address intentionally belongs to the current service.
- Force HTTP and HTTPS separately with
curl -4andcurl -6. - Use
--resolveto preserve Host/SNI while testing each intended origin. - Confirm IPv6 assignment, routing, firewalls, listeners and Coolify proxy logs.
- Either complete the IPv6 route or remove the AAAA record.
- Wait for the old TTL and verify recursive DNS convergence.
- Retry issuance once, then inspect the certificate by hostname.
- Verify unique application content over every address family still published.
Authoritative references: Coolify’s official Let’s Encrypt troubleshooting guide and domains guide; Let’s Encrypt’s explanations of IPv6 validation behaviour and HTTP-01 challenges; and Cloudflare’s DNS record-type reference.