โ† Docker Compose 15 / 18 Next: Testing โ†’

Caddy & HTTPS

Module 5 ยท Infrastructure ยท โฑ ~20 min
Why this matters: Caddy is the front door of JongYang. Every HTTPS request goes through it. It handles TLS certificates automatically (so you never need to manually install SSL certs), and routes traffic to the right container based on the hostname.

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:

HTTPS and TLS: Why It Matters

HTTPS encrypts the data between the browser and the server. Without it:

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

text โ€” /root/my_project/caddy_proxy/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:

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:

bash โ€” the reload script vs full restart
# 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
Let's Encrypt rate limits: You can only request 5 certificates per domain per week. If you restart Caddy many times during testing, you might hit the limit. For development, use Caddy's staging Let's Encrypt CA instead of production.

๐Ÿง  Self-Check Quiz

1. What is TLS termination and why does Caddy do it instead of our app containers?

TLS termination means decrypting incoming HTTPS traffic. Caddy receives encrypted HTTPS, decrypts it, then forwards plain HTTP to the app containers (nextjs/fastapi) within the private Docker network. The app containers only see unencrypted HTTP traffic โ€” they don't need TLS configuration at all. Having one place handle TLS simplifies certificate management and means our app code doesn't need to deal with encryption.

2. What is the HTTP-01 ACME challenge and why does it prove we control the domain?

Let's Encrypt issues a random token and asks us to serve it at a specific URL on our domain. Only the owner of the domain can control what's served at that URL (since DNS points to our server). If we successfully serve the token, Let's Encrypt knows we control the domain and issues the certificate. An attacker can't get a certificate for our domain because they can't control what's served on our server.

3. How does reverse_proxy nextjs:3000 in the Caddyfile know where the Next.js container is?

Docker's built-in DNS. Both the Caddy container and the Next.js container are on the same Docker network (caddy-net). Docker automatically creates a DNS entry for each container on that network using its container name. When Caddy resolves "nextjs", Docker's DNS returns the internal IP address of the Next.js container.

4. Why is Caddy in a separate Docker Compose project rather than in the JongYang docker-compose.yml?

Caddy is shared infrastructure โ€” it also serves other sites on the same server. If Caddy were in the JongYang project, stopping/restarting JongYang (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?

Reload: sends a signal to the running Caddy process to re-read its config. Zero downtime. If Caddy thinks "config unchanged", it skips cert issuance. Restart: stops and starts the container. Brief downtime (~1 second). Forces Caddy to re-evaluate all cert requirements from scratch. Restart is necessary when Caddy is stuck in a "nothing to do" state โ€” for example, when certs weren't issued because DNS wasn't set up the first time the config was loaded.