HTTP/3 on Kamal, and a PR to Basecamp
A few days ago I read a good writeup on how much HTTP/3 can shave off page loads — QUIC drops a round trip on connection setup and survives network changes without renegotiating. I wanted it on my own sites.
All of them deploy with Kamal, which fronts each
app with kamal-proxy. The good news: kamal-proxy already speaks HTTP/3 — it
has an --http3 flag. The bad news: Kamal gave me no way to turn it on.
Two things were missing, and the second is the subtle one:
- Kamal never passes
--http3to the proxy. - The proxy container only publishes TCP 80 and 443. HTTP/3 runs over QUIC, which is UDP — so even with the flag set, the UDP 443 listener would sit there unreachable.
Someone had already filed a feature request,
so I wasn’t alone in wanting this. I forked Kamal and added a proxy.run.http3
option that does both halves of the job: it passes --http3 to
kamal-proxy run, and it publishes the HTTPS port over UDP as well
(443:443/udp). There’s a guard, too — you can’t enable HTTP/3 when ports
aren’t published, since there’d be nothing to listen on.
Turning it on is one line:
# config/deploy.yml
proxy:
run:
http3: true
Until the PR lands, the Gemfile points at the fork:
gem "kamal", github: "tonic20/kamal", branch: "proxy-http3", require: false
I shipped it on watchbrandindex.com and confirmed the negotiation — browser and proxy now agree on h3.
Now to see if DHH merges it upstream. 🤞
P.S. — While running other sites through that checker, it flagged this one
too. This blog isn’t on Kamal; it’s a static site on S3 + CloudFront, which
already serves HTTP/3. But browsers were discovering it the slow way: connect
over h2, read the Alt-Svc header, then upgrade to h3 on a later connection.
The fix here lives in DNS, not the server — an
HTTPS resource record (RFC 9460) at
the apex:
kopylov.net. HTTPS 1 . alpn="h3,h2"
Now browsers learn h3 is available during the DNS lookup and try QUIC on the very first connection. One more round trip saved, no server involved.