← Back home

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:

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

  1. Query A, AAAA and CNAME through multiple resolvers.
  2. Confirm every advertised address intentionally belongs to the current service.
  3. Force HTTP and HTTPS separately with curl -4 and curl -6.
  4. Use --resolve to preserve Host/SNI while testing each intended origin.
  5. Confirm IPv6 assignment, routing, firewalls, listeners and Coolify proxy logs.
  6. Either complete the IPv6 route or remove the AAAA record.
  7. Wait for the old TTL and verify recursive DNS convergence.
  8. Retry issuance once, then inspect the certificate by hostname.
  9. 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.