name: paperlab x-backend: &backend build: ./backend # Optional: every setting has a default in app/config.py (Docker Compose 2.24 or later). env_file: - path: .env required: false volumes: - ./backend:/src - pdfs:/data/pdfs - models:/models depends_on: db: condition: service_healthy redis: condition: service_healthy # What the main and the ingest worker share; only the main worker compiles, so only it gets TECTONIC_USER. x-worker-env: &worker-env DISCOVERY_PROVIDER: ${DISCOVERY_PROVIDER:-live} # Search sources behind a model connection embed here, so the E2E stack's fake providers must reach the worker # too: a real source would record real names while the API reads `fake:` ones. LLM_PROVIDER: ${LLM_PROVIDER:-ollama} # The built-in search model's folder. E2E's no-search-model run points it at an empty one (README). MODELS_DIR: ${MODELS_DIR:-/models} # Threads the search model may use; unset it takes min(4, cores). Uncomment to change that. # EMBED_THREADS: 4 PAPERLAB_STACK: ${COMPOSE_PROJECT_NAME} DEV_RELOAD: ${DEV_RELOAD:-} # see the api services: db: image: pgvector/pgvector:pg16 environment: POSTGRES_USER: paperlab POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-paperlab} POSTGRES_DB: paperlab ports: ["127.0.0.1:${DB_PORT:-5433}:5432"] volumes: [pgdata:/var/lib/postgresql/data] healthcheck: test: ["CMD-SHELL", "pg_isready -U paperlab"] interval: 2s retries: 30 redis: image: redis:7-alpine healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 2s retries: 30 api: <<: *backend # Shell > .env > default. E2E runs on its own stack: scripts/e2e-stack.sh (README). environment: LLM_PROVIDER: ${LLM_PROVIDER:-ollama} DISCOVERY_PROVIDER: ${DISCOVERY_PROVIDER:-live} # The compose project (`paperlab`, or `paperlab-e2e` from scripts/e2e-stack.sh). Fake providers refuse to start # unless it ends in -e2e (D273). PAPERLAB_STACK: ${COMPOSE_PROJECT_NAME} # The folder `docker compose up` ran in, for the Connect Claude page (Compose uses its own working directory # when the shell exports no PWD). PAPERLAB_DIR: ${PWD:-} # The built-in search model's folder. E2E's no-search-model run points it at an empty one (README). MODELS_DIR: ${MODELS_DIR:-/models} # Any non-empty DEV_RELOAD (e.g. DEV_RELOAD=1) restarts the api and workers on every saved change under backend/. # Off by default: when your own library runs from this folder, a save would cancel its running jobs. scripts/e2e-stack.sh turns it on. DEV_RELOAD: ${DEV_RELOAD:-} # Killed for memory (a test stack beside it did that twice on 2026-10-15) or crashed, it comes back; stopped by hand, # it stays stopped. on-failure, not always: nothing starts with Docker itself. The workers set it too. restart: on-failure command: sh -c 'alembic upgrade head && exec uvicorn app.main:app --host 0.0.0.0 --port 8000 $${DEV_RELOAD:+--reload}' ports: ["127.0.0.1:${API_PORT:-8000}:8000"] worker: &worker <<: *backend restart: on-failure # see the api # The References fetch runs here, so the E2E stack's fake discovery must reach the worker too. environment: <<: *worker-env TECTONIC_USER: tex # manuscripts compile as this unprivileged account (the ingest worker never compiles) # With DEV_RELOAD, watchfiles, not `arq --watch`: that restarts the worker in-process and never re-imports changed # code. command: >- sh -c 'if [ -n "$$DEV_RELOAD" ]; then exec watchfiles --filter python "arq app.workers.settings.WorkerSettings" app; else exec arq app.workers.settings.WorkerSettings; fi' ingest-worker: <<: *worker environment: *worker-env # A paper's ingest and re-embed, one paper at a time: ten at once ran the worker out of memory. command: >- sh -c 'if [ -n "$$DEV_RELOAD" ]; then exec watchfiles --filter python "arq app.workers.settings.IngestWorkerSettings" app; else exec arq app.workers.settings.IngestWorkerSettings; fi' frontend: build: ./frontend environment: API_URL: http://api:8000 ports: ["127.0.0.1:${FRONTEND_PORT:-5180}:5173"] volumes: - ./frontend:/app - /app/node_modules depends_on: [api] volumes: pgdata: pdfs: models: