Deployment overview¶
A Canopy deployment is one container image running as two process types, backed by Postgres and Redis:
| Process | Command | Role |
|---|---|---|
api |
uvicorn canopy.main:app |
The HTTP/WebSocket API |
worker |
arq canopy.worker.WorkerSettings |
Background jobs (snapshot create/restore, batch conversion) |
Both run from the same image (ghcr.io/slaclab/canopy) and share the same
configuration. Postgres holds the persistent modules' data (one schema per
module); Redis carries the job queue.
Two deployment paths are documented here:
- Docker Compose — the development stack, and the app-only production compose file.
- Deploy at your facility — image tags, required configuration, migrations, and health checks for a production instance.
Configuration¶
Canopy is configured entirely through environment variables (optionally via a
.env file in development). There are no config files to mount. The
essentials:
| Variable | Purpose |
|---|---|
CANOPY_API_KEYS |
API-key registry — see below. Unset means every gated endpoint returns 401 |
CANOPY_EPICS__BACKEND |
simulation (default) or epics. Production must set epics |
EPICS_CA_ADDR_LIST, EPICS_PVA_ADDR_LIST, ... |
Standard EPICS network discovery variables (only with the epics backend) |
CANOPY_DATABASE_URL |
Postgres, asyncpg driver: postgresql+asyncpg://user:pass@host:port/db |
CANOPY_REDIS_URL |
Redis: redis://[:password@]host:port/0 |
CANOPY_DEBUG |
Verbose errors in responses; keep false in production |
CANOPY_LOG_LEVEL |
Root log level for the API and worker (DEBUG, INFO, WARNING, ...). Default INFO; an unknown name falls back to INFO |
CANOPY_SQUIRREL__RESTORE_HOOKS |
Comma-separated restore hooks, applied in order; empty means none run. Once a hook's write fails for a PV, later hooks skip it, so list write_to_con before magnet_trim to trim a magnet only after its BCON is updated. Must reach the worker, which performs restores. Verify with GET /squirrel/v1/health/restore-hooks: worker is what the worker reported loading at startup, api is the API's view, and matchesApi: false flags a mismatch |
CANOPY_<MODULE>__<FIELD> |
Per-module tuning (e.g. CANOPY_VOLE__GLOBAL_BYTES_MAX) |
The annotated template
.env.example
at the repo root documents every setting with its default.
Simulation is the default backend
A deployment that forgets CANOPY_EPICS__BACKEND=epics starts cleanly,
passes health checks, and serves synthetic sine waves for every PV. The
WebSocket heartbeat frame reports the active backend — check it when
values look too smooth to be real.
API keys¶
CANOPY_API_KEYS="<token>:<scope>[+<scope>][@<module>[+<module>]][,<token>:<scope>...]"
Issue separate tokens per client class with the minimum scope, and generate long random values for production. The variable is parsed once at process startup, so adding, rotating, or revoking a key is a config change plus a restart.
See API keys for the provisioning, rotation, and revocation runbook.
Database migrations¶
The schema is managed exclusively by Alembic — the app never creates tables at runtime:
CANOPY_DATABASE_URL=postgresql+asyncpg://... uv run alembic upgrade head
The production image carries the migration tree, so run
alembic upgrade head from the new image before restarting services on it
(see Upgrade). A fresh database serves nothing until
its first migration.
Health checks¶
| Endpoint | Use |
|---|---|
GET /health |
Process liveness — used by the container healthchecks and post-deploy checks |
GET /<module>/v1/health/watchdog |
Per-module liveness heartbeat (version, uptime) |
GET /<module>/v1/health/summary |
Per-module status rollup (requires read access) — e.g. Hare's reports backend, client and monitor counts |
GET /metrics |
Prometheus exposition (unauthenticated, like /health): request counts and latency per module route, plus module gauges — see Metrics |