128 lines
4.3 KiB
Markdown
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.
|