Anton Kopylov

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:

  1. Kamal never passes --http3 to the proxy.
  2. 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.

← Blog