Deployment
Production compose
docker compose -f docker-compose.yml up -d
This is the same file used in Getting Started, without the
dev-only docker-compose.override.yml hot-reload mounts. The frontend
container serves the built React app via Nginx and proxies /api to the
backend — it publishes no host port by default, since it’s meant to sit
behind a reverse proxy.
Reverse proxy
Point a reverse proxy at the frontend container on port 80, terminating
TLS there. With Traefik, for example, add
labels to the frontend service:
frontend:
labels:
- "traefik.enable=true"
- "traefik.http.routers.shoebill.rule=Host(`your-domain.example`)"
- "traefik.http.routers.shoebill.entrypoints=web"
- "traefik.http.routers.shoebill-secure.rule=Host(`your-domain.example`)"
- "traefik.http.routers.shoebill-secure.entrypoints=web-secure"
- "traefik.http.routers.shoebill-secure.tls=true"
- "traefik.http.routers.shoebill-secure.tls.certresolver=letsencrypt"
networks: [shoebill_internal, shoebill] # shoebill = external network shared with Traefik
For local testing without any reverse proxy, add a direct port mapping instead:
frontend:
ports:
- "8080:80"
Volumes and persistence
Volume |
Contents |
|---|---|
|
All application data |
|
The Beat scheduler’s persisted schedule, so scheduled tasks don’t reset on restart |
Back up the Postgres volume; everything else is reproducible from config + migrations.
Note
Backend/Celery containers run as UID 1000, not root. If celerybeat-data
already exists owned by root (e.g. from an older setup, or a manual
docker volume create), the container will fail to write to it on
startup — remove it and let Docker recreate it with the correct ownership:
docker volume rm shoebill_feed_celerybeat-data.
Scaling notes
celery-worker(fetch + default queues) can run with higher concurrency safely — fetching is I/O-bound.celery-worker-process(LLM processing) is deliberately concurrency 1 when backed by a single local Ollama instance/GPU — see Architecture for why. If you’re running Anthropic (or multiple Ollama instances behind a load balancer), raising this concurrency is reasonable.Fetches for identical sources shared across users are automatically deduplicated (see Architecture), so adding more users doesn’t multiply outbound HTTP requests to the same feeds.
Upgrading
Migrations run automatically as part of the backend image’s startup. Pull the new images, then:
docker compose pull
docker compose up -d
If you’re running SHOEBILL_TAG=stable, this picks up whatever the most
recent tagged release published; on latest, it picks up the newest
build from main.
See Development for how migrations are written, if you’re running a fork with local schema changes (which requires building from source instead of using the published images).