CI / typecheck + build (turbo) (push) Canceled after 0s
- content/: +5 real docs (caddy, bullmq, mcp-streamable-http, postgres-fts, cloudflare-525 writeup) across docs/writeups/notes. Reindexed: 9 docs, 17 chunks, 5 revisions (was 4 docs). - apps/mcp: add write-tools index_document/reindex_all/restore_revision (require x-webhook-secret) + queue_status (public). createMcpServer(authSecret?) threads the HTTP header; stdio keeps writes open (trusted local). - apps/api: GET /metrics (Prometheus text: uptime + queue job gauges). - Caddy: expose /metrics on wiki. domain -> :4020. Verified live: /metrics 200; MCP tools/list -> 10; index_document unauth -> error, auth -> enqueues + worker drains; typecheck green.
86 lines
2.3 KiB
Markdown
86 lines
2.3 KiB
Markdown
---
|
||
id: caddy-reverse-proxy
|
||
title: Caddy Reverse Proxy
|
||
type: documentation
|
||
tags:
|
||
- caddy
|
||
- reverse-proxy
|
||
- tls
|
||
- infra
|
||
status: published
|
||
author: asep
|
||
created_at: 2026-08-20
|
||
updated_at: 2026-08-20
|
||
---
|
||
|
||
# Caddy Reverse Proxy
|
||
|
||
Caddy is the single reverse proxy on the host. Every public service sits behind it
|
||
and terminates TLS with automatic Let's Encrypt certificates. Understanding its
|
||
config model prevents the two recurring failure modes: **525 (origin TLS)** and
|
||
**404 (path routing)**.
|
||
|
||
## Config model
|
||
|
||
The live config is `/etc/caddy/Caddyfile`. It is a manual, tuned file — CI does not
|
||
deploy it, so the live file is the source of truth and must be kept in sync with any
|
||
repo reference.
|
||
|
||
A reusable snippet handles the common case:
|
||
|
||
```
|
||
(proxy) {
|
||
encode zstd gzip
|
||
header {
|
||
-Server
|
||
X-Content-Type-Options "nosniff"
|
||
}
|
||
reverse_proxy 127.0.0.1:{args[0]} {
|
||
transport http {
|
||
keepalive 120s
|
||
dial_timeout 3s
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
A site block wires a domain to a backend port:
|
||
|
||
```
|
||
wiki.asepharyana.my.id {
|
||
import proxy 4016
|
||
}
|
||
```
|
||
|
||
## Path routing: `handle` vs `handle_path`
|
||
|
||
`handle /trpc/*` forwards the request **with** the `/trpc` prefix preserved.
|
||
`handle_path /trpc/*` **strips** it before proxying. Stripping is wrong when the
|
||
upstream already mounts the route at `/trpc` — the upstream then receives `/` and 404s.
|
||
|
||
Rule: when the upstream already serves the path (e.g. Hono `app.all("/trpc/*")`),
|
||
use `handle`, not `handle_path`.
|
||
|
||
## 525 — origin TLS handshake failed
|
||
|
||
Cloudflare proxies every `*.asepharyana.my.id` record. If a subdomain has **no**
|
||
Caddy site block, Caddy has no certificate for that SNI and the TLS handshake dies →
|
||
Cloudflare returns 525. Fix: add the block, `caddy validate`, `systemctl reload caddy`.
|
||
The first request after adding a block triggers ACME certificate issuance; until it
|
||
completes the origin may briefly 525. That is expected and self-heals in ~10s.
|
||
|
||
## Slow upstreams
|
||
|
||
LLM gateways (9router) have time-to-first-token of 30–40s. The default
|
||
`response_header_timeout 30s` yields false 504s. Lengthen it for those blocks:
|
||
|
||
```
|
||
reverse_proxy 127.0.0.1:4014 {
|
||
transport http {
|
||
response_header_timeout 120s
|
||
read_timeout 300s
|
||
write_timeout 300s
|
||
}
|
||
}
|
||
```
|