# 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.
