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.

Image tags

backend, celery-worker, celery-worker-process, celery-beat, and frontend all run pre-built images from Docker Hub (sebhoos/shoebill-backend, sebhoos/shoebill-frontend) rather than building locally. Set SHOEBILL_TAG in .env to choose which tag to run:

Tag

Tracks

latest (default)

The main branch — newest, but less tested

stable

The most recent tagged release — recommended for production

# .env
SHOEBILL_TAG=stable

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

postgres data volume

All application data

celerybeat-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).