Files

128 lines
4.3 KiB
Markdown

# Caddy Reverse Proxy for External-Facing Services
## Pattern
When a Docker service needs a public HTTPS URL:
```caddy
sign.itpropartner.com {
reverse_proxy 127.0.0.1:3000
}
```
## Installation
```bash
apt-get install -y caddy
```
## Key Rules
- **Port 443 must be free** — stop Tailscale Serve if it's using 443
- **Service binds to 127.0.0.1** — never expose Docker port to the internet
- Caddy handles automatic Let's Encrypt certificates — no certbot needed
- Caddy must run as root or have `CAP_NET_BIND_SERVICE`
## Verification
```bash
curl -s -o /dev/null -w "HTTPS %{http_code}" https://yourdomain.com
```
## Testing
```bash
caddy validate --config /etc/caddy/Caddyfile
systemctl restart caddy
systemctl status caddy
```
## Multi-Domain Configuration (validated Jul 7, 2026)
When serving multiple subdomains from a single server:
```caddy
# ── Signing ───────────────────────────────────────────────────────────
sign.itpropartner.com {
reverse_proxy 127.0.0.1:3000
}
# ── Core API & JSON data ─────────────────────────────────────────────
core.itpropartner.com {
header Access-Control-Allow-Origin "*"
@health path /health
handle @health {
respond "OK" 200
}
@vehicles path /vehicles.json
handle @vehicles {
root * /var/www/static
file_server
}
}
# ── App Portal ────────────────────────────────────────────────────────
app.itpropartner.com {
reverse_proxy 127.0.0.1:8081
}
```
### Writing the Caddyfile
The `write_file` tool refuses to write to `/etc/caddy/Caddyfile` (sensitive system path). Use terminal with a heredoc or Python:
```bash
python3 -c "
content = '''your caddyfile content here'''
with open('/etc/caddy/Caddyfile', 'w') as f:
f.write(content)
"
```
Then:
```bash
caddy fmt --overwrite /etc/caddy/Caddyfile
systemctl restart caddy # full restart needed, reload may skip new hosts
```
**Important:** `systemctl reload caddy` only applies changes to existing hosts. New hosts (core.itpropartner.com, app.itpropartner.com) added to the Caddyfile require a **full restart** (`systemctl stop caddy && systemctl start caddy`) to be picked up. Otherwise only the previously-loaded hosts work.
### Verification
Check which hosts Caddy is actually serving:
```bash
curl -s http://localhost:2019/config/ | python3 -c "
import sys,json
d=json.load(sys.stdin)
for s in d.get('apps',{}).get('http',{}).get('servers',{}).get('srv0',{}).get('routes',[]):
for h in s.get('match',[]):
if 'host' in h:
print('Host:', h['host'])
"
```
### New host TLS provisioning delay
When a new domain is added to the Caddyfile, Let's Encrypt certificate provisioning runs on the first HTTPS request. The initial request may fail with `tlsv1 alert internal error` while the cert is being issued. After 10-30 seconds, retry succeeds. This is normal behavior.
### Port 443 conflict: Caddy vs Tailscale Serve
Tailscale Serve claims port 443 for its internal HTTPS proxy. If Caddy needs port 443 to serve public domains (sign.itpropartner.com, core.itpropartner.com, app.itpropartner.com), run `tailscale serve off` to free port 443. This disables all Tailscale Serve routes (vaultwarden.tailc2f3b0.ts.net, app1.tailc2f3b0.ts.net).
After freeing port 443, Caddy's auto-https system requests certificates for all configured hosts and serves them on 443. HTTP to HTTPS redirects are automatic.
## Deployed Services
| Service | Domain | Port | Caddyfile Entry |
|---------|--------|------|-----------------|
| DocuSeal | sign.itpropartner.com | 127.0.0.1:3000 | `reverse_proxy 127.0.0.1:3000` |
| Vehicle JSON | core.itpropartner.com | /var/www/static | `file_server` (static) |
| Health check | core.itpropartner.com | inline | `respond "OK"` |
| Portal mockups | app.itpropartner.com | 127.0.0.1:8081 | `reverse_proxy 127.0.0.1:8081` |
## Cloudflare Tunnel Alternative
If port 443 is occupied and can't be freed:
```bash
cloudflared tunnel login # opens browser URL
cloudflared tunnel create <name>
cloudflared tunnel route dns <name> <domain>
```
Requires the domain to be on Cloudflare's DNS.