Skip to content

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