Reverse Proxy
The Reverse Proxy Manager lets you map public domains to internal upstream containers or services with one-click TLS, without ever editing a Caddyfile by hand. It sits on top of the embedded Caddy 2 daemon every Cloud OS node ships and talks to its admin API over a Unix socket at /run/caddy/admin.sock.
Where Domains & SSL auto-provisions routes for apps you install from the App Store, the Reverse Proxy page is for user-authored routes: self-hosted containers, external services, development targets — anything you want to put behind a domain + HTTPS cert.
How It Works
Every route is a row in the proxy_routes SQLite table — the source of truth. When you click Apply changes, Cloud OS:
- Reads every persisted route.
- Projects them into a Caddy admin-API config rooted at
apps.http.servers.orbit(onehost-matched entry per domain, with areverse_proxyhandler pointing at your upstream). - POSTs the config to
/loadon the Caddy admin socket. - Nudges
systemctl reload caddythrough the sharedsysctlwhitelist as belt-and-braces.
If Caddy rejects the new config, Cloud OS automatically rolls back to the last known good snapshot so a broken row can’t take down healthy traffic.
Creating a Route
- Navigate to Reverse Proxy in the sidebar (under Infra).
- Click New route.
- Fill in:
- Domain — the public hostname, e.g.
app.example.com. - Upstream —
host:portof the container or service Caddy should proxy to, e.g.127.0.0.1:8080. - Auto-HTTPS — leave on to provision a Let’s Encrypt certificate automatically (HTTP-01 challenge); turn off for HTTP-only on trusted networks.
- Domain — the public hostname, e.g.
- (Optional) expand Middleware to add basic-auth, IP allowlist, or rate-limit.
- Click Create route — the row lands in the table immediately.
- Click Apply changes to push the config to Caddy.
Routes are not auto-applied: you can batch several edits and push them to Caddy in a single round-trip to minimise reloads.
Middleware
All three middleware types are optional, per-route, and can be combined.
Basic Auth
Require HTTP Basic authentication for the route.
- Format:
user:bcrypt-hash, e.g.alice:$2a$14$s5uzCLvtmehHtFtdKWd21e... - Generating a hash: run
caddy hash-passwordon the node or any machine with Caddy installed. Enter the password interactively; paste the$2a$14$...output into the form. - Behaviour: unauthenticated requests get a
401 WWW-Authenticate: Basicprompt from the browser.
IP Allowlist
Only allow clients whose remote IP is on the supplied list.
- Format: comma-separated CIDRs, e.g.
10.0.0.0/8, 192.168.1.0/24, 2001:db8::/32. - Behaviour: non-matching clients get a plain
403 Forbiddenbefore the request reaches the upstream, basic-auth handler, or rate limiter. This is the cheapest filter to run, so it executes first.
Rate Limit
Cap request rate per client IP.
- Format:
Nr/s,Nr/m, orNr/h, e.g.100r/m= 100 requests per minute. - Behaviour: requests beyond the cap get
429 Too Many Requests. The counter is per-client-IP and per-route, so a noisy client can’t DoS your other domains. - Requires the
mholt/caddy-ratelimitmodule to be compiled into the running Caddy. Cloud OS ships with it pre-loaded; on custom Caddy builds you may need to include the module manually.
Middleware Execution Order
When multiple middlewares are configured, Caddy runs them in this order before the reverse_proxy handler:
- IP allowlist — cheapest filter, short-circuits unlisted clients with 403.
- Basic auth — cryptographic work, only runs on allowed IPs.
- Rate limit — stateful counter, applies to authenticated traffic.
- reverse_proxy — forwards to the configured upstream.
Applying Changes
The Apply changes button reconciles the store with the live Caddy config. Each click:
- Lists every route from the store.
- Builds a fresh admin-API config.
- Atomically swaps the running config via
/load. - Rolls back automatically if Caddy rejects the new config.
- Fires
caddy.reloadthrough thesysctlaudit trail so operators can see the reload in the Audit Log.
You’ll see “Last applied X ago” below the Apply button once a reconcile succeeds.
Deleting a Route
Click the trash-can icon on the route row. The row disappears from the table immediately, but Caddy still serves the old config until you click Apply changes. This is deliberate: it lets you recover from an accidental delete by re-creating the row before the next apply.
API
All UI actions are mirrored as REST endpoints under /api/proxy/:
| Method | Path | Purpose |
|---|---|---|
| GET | /api/proxy/routes | List all routes |
| POST | /api/proxy/routes | Create a route |
| GET | /api/proxy/routes/{id} | Fetch a single route |
| PUT | /api/proxy/routes/{id} | Update a route |
| DELETE | /api/proxy/routes/{id} | Delete a route |
| POST | /api/proxy/apply | Reconcile store with Caddy |
All endpoints require a session JWT — an outbound MCP tool that lands on the node can’t create or apply routes without a real operator login.
Troubleshooting
“Caddy admin: load non-2xx 400” — the generated config was rejected. Common causes:
- Upstream address unreachable on initial TLS handshake during cert provisioning.
- Rate-limit module not compiled into the running Caddy.
Cloud OS automatically rolls back to the previous snapshot, so existing traffic is unaffected. Read the error body for the specific rejection reason.
TLS cert didn’t provision — Let’s Encrypt requires port 80 reachable from the internet for the HTTP-01 challenge. Verify your firewall + DNS are wired up and check the Audit Log for caddy.reload entries that might reveal issuer errors.
Route not reachable — the row is persisted but routes are only live after the next Apply changes click. Check that the button shows “Last applied” with a recent timestamp.
Related
- Domains & SSL — auto-routes for apps you install from the App Store.
- VPN — tunnel traffic before it hits the proxy layer.
- Security — WAF and rate-limit policies that complement per-route middleware.