Putting the blog behind HTTPS with Caddy

June 24, 2026

Notes from getting the learning journal served over HTTPS, and making it survive reboots. Two parts: Caddy as a TLS-terminating reverse proxy, and systemd to keep things alive.

Why a reverse proxy at all

The blog is a tiny Go binary that serves static files. It could, in principle, handle TLS itself — but that means teaching it about certificates, renewals, port 80/443 privileges, and HTTP->HTTPS redirects. That's a lot of moving parts to bolt onto a file server.

Instead, put Caddy in front. The division of labour:

This is the standard shape: a hardened, purpose-built proxy at the edge, a simple app behind it.

The setup

Caddy installs as a systemd service from the official repo. The entire config is four lines:

syd.sparlay.me {
    reverse_proxy 127.0.0.1:8080
}

That's it. Caddy sees the domain, automatically requests a Let's Encrypt certificate, renews it forever without a cron job, and proxies everything to the Go server. The automatic HTTPS is the headline feature - no certbot, no manual renewal, no config beyond naming the domain.

Ports 80 and 443 need to be open (80 for the certificate challenge, 443 for HTTPS), and DNS for the domain must already point at the box.

port 443 was taken

Caddy refused to start:

listen udp :443: bind: address already in use

The cause was a genuine surprise. An old service was configured to use the port and it had never actually been removed; the process was alive the whole time, quietly holding the exact port the blog now needed.

Caddy's HTTP/3 support uses UDP 443 for QUIC, which is what collided.

The diagnosis was a chain:

Lesson

"Address already in use" means find the current owner before assuming the conflict is your new service's fault. The squatter was a process I thought was long gone.

A second, smaller lesson: when in doubt, disabling HTTP/3 (protocols h1 h2 in a global Caddy block) sidesteps the whole UDP 443 question. HTTP/2 over TCP is fine for a small blog.

Making it an ever-living service

A server you start by hand dies when you close the terminal — and then the proxy in front of it returns 502 Bad Gateway, because it has nothing to talk to. The fix is a systemd unit so the file server runs in the background and restarts on boot or crash:

[Unit]
Description=Blog static file server
After=network.target

[Service]
Type=simple
User=qsar
ExecStart=/home/qsar/go/blog/serve/serve -dir /home/qsar/go/blog/public -addr 127.0.0.1:8080
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target

Then:

sudo systemctl daemon-reload
sudo systemctl enable --now blog

enable makes it start on boot; --now starts it immediately. With both Caddy and this service enabled, the whole stack comes back on its own after a reboot — no terminal, no manual steps.

A gotcha worth recording: if a manually-started copy of the server is still holding the port, the systemd unit can't bind and gets stuck in activating, retrying forever. Kill the stray process first, then restart the service.

The shape of it now

internet ──HTTPS──> Caddy (:443, TLS) ──HTTP──> serve (127.0.0.1:8080) ──> files

Two enabled services, one four-line config, an auto-renewing certificate, and a file server that can't be reached except through the proxy. For a 512MB box, that's a clean place to be.