Caddy & HTTPS
What Is a Reverse Proxy?
A reverse proxy sits in front of your servers and routes incoming requests. It acts as the "receptionist" โ the public face that decides where to send each request.
Internet
โ
โ HTTPS port 443 (all traffic comes here)
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Caddy (reverse proxy) โ
โ โ
โ if host == "jongyang.xyz": โ nextjs:3000 โ
โ if host == "admin.jongyang.xyz": โ nextjs:3000 โ
โ if host == "api.jongyang.xyz": โ fastapi:8000 โ
โ if host == "bkk-sports-club...xyz": โ nextjs:3000 โ
โ (etc. for 3 more shop subdomains) โ
โ โ
โ Also handles: TLS certificates, HTTPโHTTPS redirect โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโผโโโโโโโ โโโโโโโโโผโโโโโโโ
โ Next.js :3000โ โ FastAPI :8000 โ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
Why We Need It
Without a reverse proxy:
- Users would need to type port numbers:
https://jongyang.xyz:3000โ ugly and confusing - You'd need TLS certificates manually configured in every service
- No way to host multiple services on the same IP address (ports 80/443 can only be used once)
HTTPS and TLS: Why It Matters
HTTPS encrypts the data between the browser and the server. Without it:
- Passwords and tokens are visible to anyone on the network (coffee shop WiFi, ISP)
- Browsers show "Not Secure" warnings and block certain features
- LINE Login and Omise webhooks require HTTPS endpoints
TLS certificates are what prove to the browser that it's really talking to your server (not a man-in-the-middle). Caddy gets these automatically.
How Caddy Gets Certificates Automatically (ACME)
ACME (Automatic Certificate Management Environment) is the protocol used between Caddy and Let's Encrypt (the free certificate authority). Here's how the HTTP-01 challenge works:
1. Caddy sees "jongyang.xyz" in its config
2. Caddy asks Let's Encrypt: "please give me a certificate for jongyang.xyz"
3. Let's Encrypt: "prove you control that domain. Serve this token at:
http://jongyang.xyz/.well-known/acme-challenge/ABC123..."
4. Caddy serves the token at that URL
5. Let's Encrypt makes an HTTP request to jongyang.xyz to verify
6. Token matches โ Let's Encrypt issues the certificate!
7. Caddy stores it, handles renewal automatically
All 7 certs (jongyang.xyz, api., admin., 4 shop subdomains) were
issued in parallel โ took about 6 seconds total.
The Caddyfile
# Marketplace homepage
jongyang.xyz {
reverse_proxy nextjs:3000
}
# "nextjs" resolves to the Next.js container IP via Docker DNS on caddy-net
# Caddy handles: TLS termination, HTTPโHTTPS redirect, certificate renewal
# API backend โ also receives Omise + LINE webhooks
api.jongyang.xyz {
reverse_proxy fastapi:8000
}
# Admin panel (same Next.js app, different subdomain โ different middleware behavior)
admin.jongyang.xyz {
reverse_proxy nextjs:3000
}
# Demo shop subdomains (all go to Next.js โ middleware handles routing internally)
bkk-sports-club.jongyang.xyz {
reverse_proxy nextjs:3000
}
paw-and-groom.jongyang.xyz {
reverse_proxy nextjs:3000
}
the-barber-bkk.jongyang.xyz {
reverse_proxy nextjs:3000
}
serenity-spa.jongyang.xyz {
reverse_proxy nextjs:3000
}
Why Caddy Is Separate From the App Compose
Caddy lives at /root/my_project/caddy_proxy/ in its own Docker Compose project, not in the JongYang project. This is intentional:
- Caddy is shared infrastructure โ it also serves an unrelated personal site (
than-is-on.duckdns.org) - You don't want to accidentally stop Caddy when restarting JongYang
- Caddy accumulates TLS certificates โ restarting it forces re-issuance (rate limits apply)
- Different teams could manage the proxy and the application independently
The Cert-Not-Issued Bug We Hit
After deploying, all 7 domains gave TLS errors. Caddy logs showed "config unchanged" on reload โ it had loaded the jongyang.xyz blocks earlier but didn't try to get certs (possibly because DNS wasn't pointing to the server then). Restarting Caddy (not just reloading) forced fresh certificate attempts:
# Reload: Caddy reads new config without stopping (no downtime)
# But "config unchanged" means it skips cert issuance if it thinks nothing changed
bash /root/my_project/caddy_proxy/reload-caddy.sh
# Runs: docker exec caddy caddy reload --config /etc/caddy/Caddyfile
# Restart: full stop + start โ forces fresh cert issuance (brief downtime)
cd /root/my_project/caddy_proxy
docker compose restart caddy
# This fixed the cert issue โ 7 certs issued in 6 seconds
๐ง Self-Check Quiz
1. What is TLS termination and why does Caddy do it instead of our app containers?
2. What is the HTTP-01 ACME challenge and why does it prove we control the domain?
3. How does reverse_proxy nextjs:3000 in the Caddyfile know where the Next.js container is?
4. Why is Caddy in a separate Docker Compose project rather than in the JongYang docker-compose.yml?
docker compose down) would also stop Caddy, taking down all other sites. Separate projects also mean different teams can manage them independently, and TLS certificates stored in Caddy's volume aren't at risk of being deleted by a docker compose down -v for JongYang.5. What is the difference between caddy reload and restarting the Caddy container? When is restart necessary?