Docker Compose¶
Two compose files live under docker/:
docker-compose.yml— the development stack: API + worker built from source with hot reload, plus bundled Postgres and Redis.docker-compose.prod.yml— the production app: API + worker from a prebuilt image, no data layer. The reference for deploying at your facility.
Development stack¶
cd docker
cp ../.env.example .env # optional — sensible defaults are baked in
docker compose up -d
This brings up four services:
| Service | Image | Notes |
|---|---|---|
api |
built from Dockerfile.dev |
Uvicorn with --reload; src/ and tests/ are bind-mounted, so edits apply live |
worker |
built from Dockerfile.dev |
Arq worker for background jobs |
postgres |
postgres:16-alpine |
Data persists in the canopy-pgdata volume |
redis |
redis:7-alpine |
Job queue |
Defaults (overridable in docker/.env): API on port 8000
(API_HOST_PORT), key dev:read+write, simulation EPICS backend, debug
on. Verify:
curl http://localhost:8000/health
curl -H "X-API-Key: dev" http://localhost:8000/hare/v1/pvs/DEMO:TEMP
Database migrations¶
Run Alembic from the host, pointed at the published Postgres port — the
dev image copies only src/, so the migration tree is not inside the
container:
CANOPY_DATABASE_URL=postgresql+asyncpg://canopy:canopy@localhost:5432/canopy \
uv run alembic upgrade head
Postgres-backed endpoints (Squirrel) return errors until this has been run once.
Using a local IOC¶
To point the containers at an IOC on the host, set the EPICS variables in
docker/.env:
CANOPY_EPICS__BACKEND=epics
EPICS_CA_ADDR_LIST=host.docker.internal
EPICS_CA_AUTO_ADDR_LIST=NO
Housekeeping¶
docker compose logs -f api # follow API logs
docker compose down # stop (keeps the Postgres volume)
docker compose down -v # stop and wipe data
Production compose¶
docker-compose.prod.yml runs only the app: an api service and two
worker replicas, both from the image named by IMAGE
(e.g. ghcr.io/slaclab/canopy:<tag>).
It differs from the dev stack deliberately:
- No Postgres/Redis. Run Postgres 16 and Redis 7 however your site runs
databases, and point
CANOPY_DATABASE_URL/CANOPY_REDIS_URLat them — see Postgres and Redis. network_mode: host. EPICS Channel Access / PV Access need UDP broadcast, which bridged container networking breaks. The API listens onAPI_PORT(default 8000) directly on the host.- All config from a sibling
.envfile. Compose reads it forIMAGEandAPI_PORTand passes it to both services as their environment; nothing is baked into the image or the compose file. Write it by hand or render it from your secret store.
The production image is built from docker/Dockerfile.prod by CI on every
push to main and every v* release tag — see
image tags. The image also carries the Alembic
tree, so migrations run as
docker compose run --rm api alembic upgrade head.
On the host, save the file as compose.yml next to its .env, so the bare
docker compose commands in these docs find it. Compose doesn't pick up
docker-compose.prod.yml by itself, and in the repo's docker/ directory the
same commands select the development stack. See
Deploy at your facility.