proxy App#
Edge proxy stack: caddy-tls (TLS termination + Let's Encrypt), caddy-proxy
(internal routing), anubis (PoW gate), and frps (reverse-tunnel server). All
routing and identity data is per-Host — the operator supplies it; the
committed repo carries only an hosts/example/ skeleton.
Services#
| Service | Image | Role |
|---|---|---|
| proxy-caddy-tls | library/caddy |
TLS termination, ACME, route to PoW / internal |
| proxy-caddy-proxy | library/caddy |
Internal HTTP routing on :3001 |
| proxy-anubis | ghcr.io/techarohq/anubis |
PoW challenge gate |
| proxy-frps | snowdreamtech/frps |
frp server (reverse tunnel + dashboard) |
Quadlets reference config/ paths and stay host-agnostic; real data lands on
the server via the hosts/<host>/ → config/ overlay applied by deploy-to.sh.
Setup (new host)#
The proxy has no .env — all user-supplied values live in per-Host data
files under hosts/<host>/. To stand up a new host:
-
Copy the skeleton to a host dir named after your VPS (the SSH alias that matches a server in
servers/):cp -r hosts/example hosts/<my-host> -
Fill every
CHANGE_MEin the three data files:-
hosts/<my-host>/caddy-tls/Caddyfile— your apex domain. Replace everyCHANGE_MEwith it. The skeleton defines four routes; the subdomains (direct.,mail.,frps.) are wired to the right upstreams, so only the domain changes:CHANGE_ME → example.com (apex, via Anubis PoW) direct.CHANGE_ME → direct.example.com (no PoW) mail.CHANGE_ME → mail.example.com (no PoW: JMAP/OAuth/SSE) frps.CHANGE_ME → frps.example.com (dashboard, via PoW)The apex
CHANGE_MEmust DNS-resolve to the VPS so Caddy can obtain Let's Encrypt certs automatically. -
hosts/<my-host>/caddy-proxy/Caddyfile— the same apex domain, used by internal routing. Replace everyCHANGE_MEand the"Hello from CHANGE_ME"test banner:frps.CHANGE_ME → frps.example.com (→ proxy-frps:7500) mail.CHANGE_ME → mail.example.com (→ mail-stalwart / mail-bulwark)Leave the
mail.*JMAP/bulwark reverse-proxy blocks as-is unless your mail App's upstreams differ. -
hosts/<my-host>/frps/frps.toml— the frps dashboard password:webServer.password = "CHANGE_ME" → webServer.password = "<your-password>"The remaining frps settings (
bindPort,vhostHTTPPort, dashboard port / addr / user) are static; change them only if your topology requires it.
-
-
Deploy:
./deploy-to.sh <my-host>deploy-to.shrsyncsconfig/(scaffolding) thenhosts/<my-host>/(your reals) to~/apps/proxy/config/on the server, then runsdeploy.sh.
What never gets committed#
Real host dirs are gitignored (hosts/* with !hosts/example/ negated). Only
the hosts/example/ skeleton and the config/anubis/.gitkeep scaffolding are
tracked. See apps/README.md → "Host-specific config" and ADR 0002 for the
model.
Deploy-time safety#
deploy.sh greps every synced real file under config/ for a surviving
CHANGE_ME and fails the deploy with a clear message listing the offending file.
Run a local grep -r CHANGE_ME hosts/<my-host> before deploying to self-check.