The live site was returning a blank page. The Caddy access log showed:
"msg":"NOP", ... "status":0, "size":0, "bytes_read":0
"server is listening only on the HTTP port, so no automatic HTTPS will be applied"
The live Caddy site block had no responder (no reverse_proxy), so requests fell through to a NOP and returned nothing. This also explained the earlier performance findings: the origin was serving everything uncompressed and with no security headers, because Caddy wasn't running the encode/header/reverse_proxy directives.
Root cause
Actual production topology is Cloudflare → Caddy (HTTP :80) → SWS, but there was no committed, correct Caddyfile for it — the repo only documented the DNS-01 (Caddy-terminates-TLS) variant.
Fix
Add a known-good, committed Caddyfile at the repo root for the Cloudflare-fronted, HTTP-only topology:
reverse_proxy blog:80 — the missing responder that caused the blank page
Security headers — HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy; drops the Server header
http:// scheme so Caddy serves plain HTTP and does not try to provision TLS (Cloudflare handles HTTPS)
trusted_proxies static <Cloudflare ranges> (15 IPv4 + 7 IPv6 from cloudflare.com/ips-v4 and /ips-v6) + client_ip_headers Cf-Connecting-Ip X-Forwarded-For — real visitor IPs are honoured only for genuine Cloudflare requests, preventing header spoofing
Also updates docs/caddy-reverse-proxy.md with a "Deployment topologies" overview and a dedicated Cloudflare-fronted section, labelling the existing Compose/K8s examples as the DNS-01 alternative.
Verification
caddy validate + caddy fmt clean against the caddy-cf image; all 22 Cloudflare ranges parse into the adapted JSON.
End-to-end runtime test (Caddy → SWS on a shared docker network): Caddy starts cleanly, serves HTTP 200 with real content (<title>ahmza's ramblings</title>), Content-Encoding: gzip, and X-Frame-Options: SAMEORIGIN.
Deploy note
This fixes prod only once deployed to the caddy_cf host — the repo commit alone doesn't update the running proxy:
The reverse_proxy blog:80 resolves because Caddy and the blog container share a Docker network with the blog service named blog.
## Problem
The live site was returning a **blank page**. The Caddy access log showed:
```
"msg":"NOP", ... "status":0, "size":0, "bytes_read":0
"server is listening only on the HTTP port, so no automatic HTTPS will be applied"
```
The live Caddy site block had **no responder** (no `reverse_proxy`), so requests fell through to a `NOP` and returned nothing. This also explained the earlier performance findings: the origin was serving **everything uncompressed** and with **no security headers**, because Caddy wasn't running the `encode`/`header`/`reverse_proxy` directives.
## Root cause
Actual production topology is **Cloudflare → Caddy (HTTP :80) → SWS**, but there was no committed, correct Caddyfile for it — the repo only documented the DNS-01 (Caddy-terminates-TLS) variant.
## Fix
Add a known-good, committed `Caddyfile` at the repo root for the Cloudflare-fronted, HTTP-only topology:
- **`reverse_proxy blog:80`** — the missing responder that caused the blank page
- **`encode zstd gzip`** — fixes the no-compression problem (HTML ~35 KB → ~6 KB gzipped, CSS ~102 KB → ~16 KB, etc.)
- **Security headers** — HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy; drops the `Server` header
- **`http://` scheme** so Caddy serves plain HTTP and does not try to provision TLS (Cloudflare handles HTTPS)
- **`trusted_proxies static <Cloudflare ranges>`** (15 IPv4 + 7 IPv6 from cloudflare.com/ips-v4 and /ips-v6) + `client_ip_headers Cf-Connecting-Ip X-Forwarded-For` — real visitor IPs are honoured only for genuine Cloudflare requests, preventing header spoofing
Also updates `docs/caddy-reverse-proxy.md` with a "Deployment topologies" overview and a dedicated Cloudflare-fronted section, labelling the existing Compose/K8s examples as the DNS-01 alternative.
## Verification
- `caddy validate` + `caddy fmt` clean against the `caddy-cf` image; all 22 Cloudflare ranges parse into the adapted JSON.
- End-to-end runtime test (Caddy → SWS on a shared docker network): Caddy starts cleanly, serves **HTTP 200** with real content (`<title>ahmza's ramblings</title>`), **`Content-Encoding: gzip`**, and **`X-Frame-Options: SAMEORIGIN`**.
## Deploy note
This fixes prod only once deployed to the `caddy_cf` host — the repo commit alone doesn't update the running proxy:
```bash
docker exec caddy_cf caddy reload --config /etc/caddy/Caddyfile
```
The `reverse_proxy blog:80` resolves because Caddy and the blog container share a Docker network with the blog service named `blog`.
The live Caddy was returning a blank page (NOP, status 0) because its
site block had no responder \u2014 no reverse_proxy to the blog. This adds a
known-good HTTP-only Caddyfile matching the actual topology
(Cloudflare terminates TLS -> Caddy :80 -> SWS blog:80):
- reverse_proxy blog:80 (the missing responder that caused the blank page)
- encode zstd gzip (fixes: origin was serving everything uncompressed)
- security headers (fixes: HSTS/X-Frame-Options/etc were absent)
- trusted_proxies + client_ip_headers for correct Cloudflare visitor IPs
- http:// scheme so Caddy does not try to provision TLS (Cloudflare does)
Validated + fmt-clean against the caddy-cf image; tested end-to-end
locally (Caddy -> SWS) returning HTTP 200 with gzip and headers.
Replace 'private_ranges' with Cloudflare's official published edge ranges
(15 IPv4 + 7 IPv6 from cloudflare.com/ips-v4 and /ips-v6). This ensures
Cf-Connecting-Ip / X-Forwarded-For are only honoured for requests that
genuinely originate from Cloudflare, preventing client-IP header spoofing
from any other source.
Validated + fmt-clean and tested at runtime against the caddy-cf image:
Caddy starts cleanly with all 22 ranges, serves HTTP 200 with gzip and
security headers.
The live production setup is Cloudflare -> Caddy (HTTP :80) -> SWS, which
differed from the DNS-01 topology the doc previously assumed. Add a
'Deployment topologies' overview and a dedicated Cloudflare-fronted
section pointing at the committed root Caddyfile, covering reverse_proxy,
encode, security headers, and Cloudflare trusted_proxies. Label the
existing Compose/K8s examples as the DNS-01 alternative.
ahmza
merged commit 09780d9598 into main2026-07-25 03:56:08 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
The live site was returning a blank page. The Caddy access log showed:
The live Caddy site block had no responder (no
reverse_proxy), so requests fell through to aNOPand returned nothing. This also explained the earlier performance findings: the origin was serving everything uncompressed and with no security headers, because Caddy wasn't running theencode/header/reverse_proxydirectives.Root cause
Actual production topology is Cloudflare → Caddy (HTTP :80) → SWS, but there was no committed, correct Caddyfile for it — the repo only documented the DNS-01 (Caddy-terminates-TLS) variant.
Fix
Add a known-good, committed
Caddyfileat the repo root for the Cloudflare-fronted, HTTP-only topology:reverse_proxy blog:80— the missing responder that caused the blank pageencode zstd gzip— fixes the no-compression problem (HTML ~35 KB → ~6 KB gzipped, CSS ~102 KB → ~16 KB, etc.)Serverheaderhttp://scheme so Caddy serves plain HTTP and does not try to provision TLS (Cloudflare handles HTTPS)trusted_proxies static <Cloudflare ranges>(15 IPv4 + 7 IPv6 from cloudflare.com/ips-v4 and /ips-v6) +client_ip_headers Cf-Connecting-Ip X-Forwarded-For— real visitor IPs are honoured only for genuine Cloudflare requests, preventing header spoofingAlso updates
docs/caddy-reverse-proxy.mdwith a "Deployment topologies" overview and a dedicated Cloudflare-fronted section, labelling the existing Compose/K8s examples as the DNS-01 alternative.Verification
caddy validate+caddy fmtclean against thecaddy-cfimage; all 22 Cloudflare ranges parse into the adapted JSON.<title>ahmza's ramblings</title>),Content-Encoding: gzip, andX-Frame-Options: SAMEORIGIN.Deploy note
This fixes prod only once deployed to the
caddy_cfhost — the repo commit alone doesn't update the running proxy:The
reverse_proxy blog:80resolves because Caddy and the blog container share a Docker network with the blog service namedblog.