# Caddy & TLS Caddy is the single entry point in front of PiCloud. The same topology serves dev and production — only the upstream targets and TLS settings change. ## Routing topology Caddy routes by path prefix (from `caddy/Caddyfile`): | Path | Goes to | |---|---| | `/healthz`, `/version` | picloud (liveness + version) | | `/api/v1/admin/*` | picloud (control plane) | | `/api/v1/execute/*` | picloud (execute-by-id) | | `/api/*` (anything else) | **404** — reserved for future API versions | | `/admin/*` | the dashboard SPA | | **everything else** | picloud's **user-route matcher** — your scripts' routes | That final catch-all is what makes arbitrary user paths like `/hello` or `/r/abc123` work: Caddy forwards them to picloud, which matches them against the [route table](../guide/concepts.md#routes-and-domains) (or returns a JSON `404`). This is why your routes can't use the reserved prefixes `/api/`, `/admin/`, `/healthz`, `/version` — Caddy would never forward them to the matcher. ## Security headers & body limit The Caddyfile adds defense-in-depth headers and a hard body cap: - `X-Content-Type-Options: nosniff` and `Referrer-Policy: no-referrer` on **every** response. - A strict CSP, `X-Frame-Options: DENY`, `Cache-Control: no-store`, and a locked-down `Permissions-Policy` on the **dashboard** and **admin API**. - A **12 MB request-body ceiling** at the proxy (just above the orchestrator's 10 MiB user-route read). - **User-route responses get no CSP on purpose** — your scripts own their own response headers. The `?` (set-if-missing) operator means a script's own header wins over the default. ## Production: HTTPS with Let's Encrypt Layer the production overlay, which swaps in `caddy/Caddyfile.prod` and exposes 80/443: ```sh export PICLOUD_DOMAIN="picloud.example.com" export PICLOUD_ADMIN_EMAIL="you@example.com" export POSTGRES_PASSWORD="$(head -c 24 /dev/urandom | base64)" export PICLOUD_SECRET_KEY="$(head -c 32 /dev/urandom | base64)" export PICLOUD_ADMIN_USERNAME="admin" export PICLOUD_ADMIN_PASSWORD="…" export PICLOUD_PUBLIC_BASE_URL="https://picloud.example.com" docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build ``` Caddy obtains and renews a certificate for `PICLOUD_DOMAIN` automatically (HTTP/TLS-ALPN challenge — the host must be reachable on 80/443 from the internet). The prod overlay also: - **removes the published Postgres port** (DB reachable only inside the compose network); - adds **HSTS** to every response (on top of the dev headers); - sets `restart: unless-stopped` on all services. The prod Caddyfile reads `PICLOUD_DOMAIN` and `PICLOUD_ADMIN_EMAIL` from the environment, so set them before `up`. ## Custom domains for your apps PiCloud's app [domains](../reference/rest-api/apps.md#domains) are matched by picloud *after* Caddy forwards the request — they're independent of Caddy's own host config. For multi-tenant setups (`{tenant}.example.com`), point a wildcard DNS record + Caddy cert at the box, then let each app claim its host pattern. The most-specific app claim wins; unclaimed hosts get a `404 no app claims host`. ## Adding a future API version When `/api/v2/...` ships, add a `handle /api/v2/admin/* { … }` block before the catch-all `/api/*` 404, mirroring the v1 block. The v1 routes stay live through the deprecation window.