# Self-contained local development stack — Postgres + API + scheduler, all on this machine. # # Fully decoupled: its own local database (a Docker volume) and its own scheduler, so there # is no shared DB and no migration coupling with any other instance. Edit code, rebuild, and # everything (including the background sync) runs right here. # # docker compose -f docker-compose.localdev.yml up -d --build # # Browse at http://localhost:8080 — Google login works because the OAuth redirect is # http://localhost:8080/auth/callback. The API runs `alembic upgrade head` on startup, so it # applies local migrations to THIS local DB only. Seed the catalog once with a pg_dump from # another instance if you want real data (see RUNBOOK). services: db: image: postgres:16-alpine environment: POSTGRES_USER: ${POSTGRES_USER:-siftlode} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-siftlode} POSTGRES_DB: ${POSTGRES_DB:-siftlode} volumes: - pgdata:/var/lib/postgresql/data security_opt: - apparmor:unconfined healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-siftlode} -d ${POSTGRES_DB:-siftlode}"] interval: 5s timeout: 5s retries: 12 restart: unless-stopped api: build: context: . dockerfile: Dockerfile args: APP_VERSION: ${APP_VERSION:-dev} GIT_SHA: ${GIT_SHA:-unknown} BUILD_DATE: ${BUILD_DATE:-} env_file: .env environment: # Own local database (overrides whatever DATABASE_URL is in .env). DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD:-siftlode}@db:5432/${POSTGRES_DB:-siftlode} # This instance owns its scheduler now (its own DB, so no double-write concern). SCHEDULER_ENABLED: "true" # Download center: the API serves finished files (it does NOT run the worker loop). DOWNLOAD_ROOT: /downloads WORKER_ENABLED: "false" depends_on: db: condition: service_healthy # Harmless on Docker Desktop; needed on some nested/hardened Docker hosts. security_opt: - apparmor:unconfined ports: - "${APP_PORT:-8080}:8000" volumes: # Downloaded media lives on the host so you can inspect the generated Plex tree. # Gitignored. The worker writes here; the API reads it to serve local downloads. - ./downloads:/downloads # Plex media, read-only (see the plex_media volume note below). Maps to plex_path_map. - plex_media:/plex-media:ro restart: unless-stopped # Dedicated download worker: same image, runs the yt-dlp job loop instead of the API. # It shares the DB (job queue) and the downloads mount with the API. worker: build: context: . dockerfile: Dockerfile args: APP_VERSION: ${APP_VERSION:-dev} GIT_SHA: ${GIT_SHA:-unknown} BUILD_DATE: ${BUILD_DATE:-} command: ["python", "-m", "app.worker"] env_file: .env environment: DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD:-siftlode}@db:5432/${POSTGRES_DB:-siftlode} # The worker must not also run the scheduler; the API owns that. SCHEDULER_ENABLED: "false" DOWNLOAD_ROOT: /downloads WORKER_ENABLED: "true" depends_on: db: condition: service_healthy bgutil-pot: condition: service_started security_opt: - apparmor:unconfined volumes: - ./downloads:/downloads - plex_media:/plex-media:ro restart: unless-stopped # PO-token provider: mints YouTube Proof-of-Origin tokens on demand so the worker bypasses # bot-detection and gets high-quality formats. The worker reaches it at http://bgutil-pot:4416 # (DOWNLOAD_POT_BASE_URL). Pin the tag to match the plugin version in requirements.txt. bgutil-pot: image: brainicism/bgutil-ytdlp-pot-provider:1.3.1 init: true security_opt: - apparmor:unconfined restart: unless-stopped volumes: pgdata: # OPTIONAL (Plex integration dev testing): the Plex media, mounted READ-ONLY from the SMB share # so the container can play the physical file locally (this dev box isn't the Plex host). Uses a # dedicated read-only samba user via PLEX_SMB_USER/PLEX_SMB_PASS in .env. Only referenced when the # Plex module is enabled; harmless otherwise (Docker mounts it lazily on first container use). On # prod (LXC on the Plex host) this is a plain host bind-mount instead — no SMB. plex_media: driver: local driver_opts: type: cifs # Host/share + credentials come from .env (gitignored) so no site-specific values live in # the tracked compose: PLEX_SMB_HOST, PLEX_SMB_SHARE, PLEX_SMB_USER, PLEX_SMB_PASS. device: "//${PLEX_SMB_HOST:-localhost}/${PLEX_SMB_SHARE:-data}" o: "username=${PLEX_SMB_USER:-},password=${PLEX_SMB_PASS:-},ro,vers=3.0,uid=1000,gid=1000,file_mode=0444,dir_mode=0555"