Add a browsable mdBook developer guide under docs/dev-guide/ covering the full PiCloud v1.1.9 surface for developers building on the platform: - Guide: introduction, 10-minute quickstart, core concepts, writing scripts - Tutorials: 5 end-to-end example apps (URL shortener, webhook receiver, TODO API with auth, scheduled report, file-upload service) + feature matrix - SDK reference: kv/docs/files, pubsub/queue, http, email, users, secrets, invoke/retry/dead_letters, stdlib, ctx & events - HTTP API reference: every /api/v1 endpoint, auth, and capability - CLI reference: the `pic` client + the server admin recovery command - Config: every PICLOUD_* env var, capabilities & roles - Deployment: Docker Compose, bare binary, Caddy/TLS, production checklist - Operations: security, best practices, troubleshooting Every example was built and run against a live instance; SDK signatures, endpoint paths, and env vars were verified against source. Build the site with `mdbook serve docs/dev-guide`. A pointer is added to README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3.3 KiB
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
(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: nosniffandReferrer-Policy: no-referreron every response.- A strict CSP,
X-Frame-Options: DENY,Cache-Control: no-store, and a locked-downPermissions-Policyon 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:
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-stoppedon 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 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.