Localhost TLD in Safari not found

1. The problem I’m having:

Safari can’t connect to a.localhost domain, while other browsers can.

2. Error messages and/or full log output:

Safari can’t open the page “https://rootable.localhost” because Safari can’t find the server “rootable.localhost”.

3. Caddy version:

v2.11.4 h1:XKxkMTgNSizEvKG6QHue6cAsFOteU2qA61w2tKkCWi0=

4. How I installed and ran Caddy:

macOS homebrew (brew install caddy)

a. System environment:

macOS 15.7.4

Using caddy just by itself via the terminal. No systemd, docker, etc.

b. Command:

caddy run

c. Service/unit/compose file:

n/a

d. My complete Caddy config:

rootable.localhost {
  reverse_proxy localhost:3000
}

5. Links to relevant resources:

I’ve also looked at Localhost on OSX works in Chrome, but not Safari and the only solution offered was to add 127.0.0.1 *.localhostto my /etc/hosts and I’ve done that.

Is that really your full Caddyfile?

It is. You can even see it in the repo, here: https://codeberg.org/rootable/tomato/src/branch/main/Caddyfile

I cannot replicate your problem:

rootable.localhost {
  reverse_proxy localhost:3000
}

:3000 {
	respond "ALIVE!"
}
$ grep localhost  /etc/hosts
# localhost is used to configure the loopback interface
127.0.0.1	localhost
::1             localhost
$ ping -c 3 rootable.localhost
PING localhost (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.132 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.118 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.244 ms

--- localhost ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.118/0.165/0.244/0.056 ms
$ curl -v https://rootable.localhost
* Host rootable.localhost:443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* SSL Trust Anchors:
*   Native: Apple SecTrust
*   OpenSSL default paths (fallback)
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519MLKEM768 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
*   subject: 
*   start date: Jul 15 00:21:15 2026 GMT
*   expire date: Jul 15 12:21:15 2026 GMT
*   issuer: CN=Caddy Local Authority - ECC Intermediate
*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 2: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   subjectAltName: "rootable.localhost" matches cert's "rootable.localhost"
* OpenSSL verify result: 0
* SSL certificate verified via OpenSSL.
* Established connection to rootable.localhost (::1 port 443) from ::1 port 64689 
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://rootable.localhost/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: rootable.localhost]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.21.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: rootable.localhost
> User-Agent: curl/8.21.0
> Accept: */*
> 
* Request completely sent off
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 200 
< alt-svc: h3=":443"; ma=2592000
< content-type: text/plain; charset=utf-8
< date: Wed, 15 Jul 2026 00:25:31 GMT
< server: Caddy
< via: 1.1 Caddy
< content-length: 6
< 
* Connection #0 to host rootable.localhost:443 left intact
ALIVE!
$ caddy version
v2.11.4 h1:XKxkMTgNSizEvKG6QHue6cAsFOteU2qA61w2tKkCWi0=
$ sw_vers
ProductName:		macOS
ProductVersion:		26.5.2
BuildVersion:		25F84

Thanks so much for taking the time to run all those tests. I really appreciate that and it gives me some more ways to debug. I ran them also and got the following:

grep /etc/hosts

Same as you, but I also have 127.0.0.1 *.localhost in addition

ping

ping: cannot resolve rootable.localhost: Unknown host

curl -v

Some differences. Not sure they are interesting because it did find the host, but here is my output

* Host rootable.localhost:443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying [::1]:443...
* Connected to rootable.localhost (::1) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
} [323 bytes data]
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
{ [122 bytes data]
* (304) (IN), TLS handshake, Unknown (8):
{ [19 bytes data]
* (304) (IN), TLS handshake, Certificate (11):
{ [934 bytes data]
* (304) (IN), TLS handshake, CERT verify (15):
{ [79 bytes data]
* (304) (IN), TLS handshake, Finished (20):
{ [36 bytes data]
* (304) (OUT), TLS handshake, Finished (20):
} [36 bytes data]
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
*  subject: [NONE]
*  start date: Jul 15 13:17:14 2026 GMT
*  expire date: Jul 16 01:17:14 2026 GMT
*  subjectAltName: host "rootable.localhost" matched cert's "rootable.localhost"
*  issuer: CN=Caddy Local Authority - ECC Intermediate
*  SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://rootable.localhost/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: rootable.localhost]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: rootable.localhost
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/2 200 
< alt-svc: h3=":443"; ma=2592000
< content-type: text/plain; charset=utf-8
< date: Wed, 15 Jul 2026 15:52:18 GMT
< server: Caddy
< via: 1.1 Caddy
< content-length: 6
< 
{ [6 bytes data]

100     6  100     6    0     0    170      0 --:--:-- --:--:-- --:--:--   171
* Connection #0 to host rootable.localhost left intact
ALIVE!

Just for the test, can you also add

127.0.0.1   rootable.localhost

I’m wondering if the OS and Safari version difference between you and me makes such an impact.

I mean this is not a Caddy problem. If you google the internet, you’ll see a bunch of people having the same problem with Safari and something listening on the localhost. I’m just curious at this point.

Adding that to /etc/hosts/ makes it work. So strange. Any guess as to what’s going on? Is Safari bypassing whatever DNS stuff Caddy is setting up but still using /etc/hosts? That’s the only explanation I can think of but it’s hardly helpful because I don’t know how Caddy establishes itself as a place for DNS to resolve.

This isn’t a Caddy issue. Caddy doesn’t participate in name resolution at all. The hostname must already resolve before the browser can connect to Caddy.

RFC 6761 reserves .localhost and says that names beneath it, such as rootable.localhost, should resolve locally to the loopback interface without querying DNS. My macOS 26 machine appears to implement that behaviour, while your macOS 15 machine doesn’t, or at least not consistently.

Adding rootable.localhost to /etc/hosts simply gives your resolver the explicit mapping it was missing. Safari isn’t bypassing anything Caddy set up because Caddy never sets up DNS in the first place.