Safe HTTP/3 Experimentation With Caddy
Tags: quic, http3, apache, caddy
Published: 2022-04-25 01:22:29

---
title: Safe HTTP/3 Experimentation With Caddy
published: true
description: Instructions for setting up a reverse proxy to serve HTTP/3 without disturbing your main webserver
tags: quic,http3,apache,caddy
cover_image: https://quic.ulfheim.net/images/heart-padlock.jpg?v2
---
# The Bleeding Edge Problem

I'm writing [QUIC documentation](https://quic.ulfheim.net) and thought it would be great to have the site available as HTTP/3 (which runs on top of QUIC). This is a relatively new protocol, so I ran into some familiar problems:

* *Tooling*: The webserver that I'm using (apache) doesn't offer HTTP/3 support, and has no current plans to
* *Uptime*: The page in question is relatively popular, and I don't want to risk it going down while I set this up

# The QUIC Possibility

Luckily the details of the QUIC transport protocol give us some flexibility here:

* QUIC uses a different underlying protocol (UDP vs TCP), so I can leave apache on TCP port 443 and put up a reverse proxy on the UDP port
* All HTTP/3-capable browsers will try QUIC and, if they get a connection error, will fall back to TCP (and the unmodified apache setup)

# The Solution

The fix is a simple Caddy reverse proxy. It's running in a docker container to let me "split the bindings": Caddy wants to bind both TCP and UDP ports but I can only give it the UDP port. Containerizing gives me the flexibility of letting it bind both in its container, but only exposing the UDP port to the world.

### Details

The Caddy installation looks like this. You'll want to tweak a few lines, indicated with "`youruser`" or "`yoursite`" placeholders:

setup:
```
### change as appropriate for your OS
sudo snap install docker

mkdir ~/caddy/
mkdir ~/caddy/caddy_data
mkdir ~/caddy/caddy_config
```

`~/caddy/docker-compose.yaml`:
```
version: "3.7"

services:
  caddy:
    container_name: caddy
    hostname: caddy
    image: caddy:2.4.6
    restart: unless-stopped
    ports:
      - "443:443/udp"
    volumes:
      - /home/youruser/caddy/Caddyfile:/etc/caddy/Caddyfile
      - /path/to/yoursite/fullchain.pem:/caddy.crt
      - /path/to/yoursite/privkey.pem:/caddy.key
      - /home/youruser/caddy/caddy_data:/data
      - /home/youruser/caddy/caddy_config:/config
    extra_hosts:
      - "host-gateway:172.17.0.1"

volumes:
  caddy_data:
    external: true
  caddy_config:
```

`~/caddy/Caddyfile`:
```
{
	auto_https off
	servers {
		protocol {
			experimental_http3
		}
	}
}

yoursite.com {
	tls /caddy.crt /caddy.key
	reverse_proxy * https://host-gateway {
		transport http {
			tls_insecure_skip_verify
		}
	}
}
```

startup:
```
cd ~/caddy
sudo docker-compose up -d
### tail the logs with `sudo docker logs -f caddy`
```

### Advertising HTTP/3

The above sets up a reverse proxy that serves HTTP/3 on UDP port 443, but nothing will try it until you advertise it on your "real" HTTP server. Fortunately this minor config was the only change needed on the production server:

In my `VirtualHost` apache config for the site:
```
Header set alt-svc "h3=\":443\"; ma=3600, h3-29=\":443\"; ma=3600
```
This advertises an HTTP/3 service (both "standard" and "draft 29" versions of the protocol) with the "Alt-Svc:" header. You'll need to bounce apache for this to take effect.

### Cert Rotation

One last thing is needed to handle cert rotation. The above solution copies your site certificate and key into the Caddy container, but if you're using LetsEncrypt that cert is only good for three months and is likely being rotated monthly. I run the following in cron to ensure the container is always capturing a relatively fresh certificate:

```
### bounce the reverse proxy every month
39 0 25 * * cd /home/youruser/caddy && /path/to/docker-compose down && /path/to/docker-compose up -d
```

<small>cover image by [Stephen Cleary](https://flickr.com/photos/stephencleary/9962828185) CC 2.0</small>