2026-06-16 15:21:43 +02:00
|
|
|
# Self-contained local development stack — Postgres + API + scheduler, all on this machine.
|
2026-06-11 16:35:00 +02:00
|
|
|
#
|
2026-06-16 15:21:43 +02:00
|
|
|
# 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.
|
2026-06-11 16:35:00 +02:00
|
|
|
#
|
2026-06-16 15:21:43 +02:00
|
|
|
# docker compose -f docker-compose.localdev.yml up -d --build
|
2026-06-11 16:35:00 +02:00
|
|
|
#
|
2026-06-16 15:21:43 +02:00
|
|
|
# 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).
|
2026-06-11 16:35:00 +02:00
|
|
|
|
|
|
|
|
services:
|
2026-06-16 15:21:43 +02:00
|
|
|
db:
|
|
|
|
|
image: postgres:16-alpine
|
|
|
|
|
environment:
|
2026-06-21 06:53:12 +02:00
|
|
|
POSTGRES_USER: ${POSTGRES_USER:-siftlode}
|
|
|
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-siftlode}
|
|
|
|
|
POSTGRES_DB: ${POSTGRES_DB:-siftlode}
|
2026-06-16 15:21:43 +02:00
|
|
|
volumes:
|
|
|
|
|
- pgdata:/var/lib/postgresql/data
|
|
|
|
|
security_opt:
|
|
|
|
|
- apparmor:unconfined
|
|
|
|
|
healthcheck:
|
2026-06-21 06:53:12 +02:00
|
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-siftlode} -d ${POSTGRES_DB:-siftlode}"]
|
2026-06-16 15:21:43 +02:00
|
|
|
interval: 5s
|
|
|
|
|
timeout: 5s
|
|
|
|
|
retries: 12
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
|
2026-06-11 16:35:00 +02:00
|
|
|
api:
|
|
|
|
|
build:
|
|
|
|
|
context: .
|
|
|
|
|
dockerfile: Dockerfile
|
2026-06-15 00:06:57 +02:00
|
|
|
args:
|
|
|
|
|
APP_VERSION: ${APP_VERSION:-dev}
|
|
|
|
|
GIT_SHA: ${GIT_SHA:-unknown}
|
|
|
|
|
BUILD_DATE: ${BUILD_DATE:-}
|
2026-06-11 16:35:00 +02:00
|
|
|
env_file: .env
|
|
|
|
|
environment:
|
2026-06-16 15:21:43 +02:00
|
|
|
# Own local database (overrides whatever DATABASE_URL is in .env).
|
2026-06-21 06:53:12 +02:00
|
|
|
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD:-siftlode}@db:5432/${POSTGRES_DB:-siftlode}
|
2026-06-16 15:21:43 +02:00
|
|
|
# This instance owns its scheduler now (its own DB, so no double-write concern).
|
|
|
|
|
SCHEDULER_ENABLED: "true"
|
feat(downloads): M1 — schema, config, worker container, deps
- models: MediaAsset (shared file cache, unique on source+format_sig), DownloadJob
(per-user request), DownloadProfile (presets), DownloadShare (ACL), DownloadQuota
- migrations 0036 (5 tables) + 0037 (6 builtin presets)
- sysconfig 'downloads' group (per-user defaults, retention/GC, layout, worker concurrency)
- config: DOWNLOAD_ROOT / WORKER_ENABLED env + download defaults
- deps: yt-dlp in requirements, ffmpeg in Dockerfile runtime stage
- worker: dedicated container (python -m app.worker), thin idle entry (loop lands in M2)
- localdev compose: worker service + downloads bind-mount; gitignore downloads/
2026-07-02 23:57:40 +02:00
|
|
|
# Download center: the API serves finished files (it does NOT run the worker loop).
|
|
|
|
|
DOWNLOAD_ROOT: /downloads
|
|
|
|
|
WORKER_ENABLED: "false"
|
2026-06-16 15:21:43 +02:00
|
|
|
depends_on:
|
|
|
|
|
db:
|
|
|
|
|
condition: service_healthy
|
2026-07-01 12:46:50 +02:00
|
|
|
# Harmless on Docker Desktop; needed on some nested/hardened Docker hosts.
|
2026-06-11 16:42:44 +02:00
|
|
|
security_opt:
|
|
|
|
|
- apparmor:unconfined
|
2026-06-11 16:35:00 +02:00
|
|
|
ports:
|
|
|
|
|
- "${APP_PORT:-8080}:8000"
|
feat(downloads): M1 — schema, config, worker container, deps
- models: MediaAsset (shared file cache, unique on source+format_sig), DownloadJob
(per-user request), DownloadProfile (presets), DownloadShare (ACL), DownloadQuota
- migrations 0036 (5 tables) + 0037 (6 builtin presets)
- sysconfig 'downloads' group (per-user defaults, retention/GC, layout, worker concurrency)
- config: DOWNLOAD_ROOT / WORKER_ENABLED env + download defaults
- deps: yt-dlp in requirements, ffmpeg in Dockerfile runtime stage
- worker: dedicated container (python -m app.worker), thin idle entry (loop lands in M2)
- localdev compose: worker service + downloads bind-mount; gitignore downloads/
2026-07-02 23:57:40 +02:00
|
|
|
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
|
2026-07-05 02:07:05 +02:00
|
|
|
# Plex media, read-only (see the plex_media volume note below). Maps to plex_path_map.
|
|
|
|
|
- plex_media:/plex-media:ro
|
feat(downloads): M1 — schema, config, worker container, deps
- models: MediaAsset (shared file cache, unique on source+format_sig), DownloadJob
(per-user request), DownloadProfile (presets), DownloadShare (ACL), DownloadQuota
- migrations 0036 (5 tables) + 0037 (6 builtin presets)
- sysconfig 'downloads' group (per-user defaults, retention/GC, layout, worker concurrency)
- config: DOWNLOAD_ROOT / WORKER_ENABLED env + download defaults
- deps: yt-dlp in requirements, ffmpeg in Dockerfile runtime stage
- worker: dedicated container (python -m app.worker), thin idle entry (loop lands in M2)
- localdev compose: worker service + downloads bind-mount; gitignore downloads/
2026-07-02 23:57:40 +02:00
|
|
|
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
|
2026-07-03 04:40:10 +02:00
|
|
|
bgutil-pot:
|
|
|
|
|
condition: service_started
|
feat(downloads): M1 — schema, config, worker container, deps
- models: MediaAsset (shared file cache, unique on source+format_sig), DownloadJob
(per-user request), DownloadProfile (presets), DownloadShare (ACL), DownloadQuota
- migrations 0036 (5 tables) + 0037 (6 builtin presets)
- sysconfig 'downloads' group (per-user defaults, retention/GC, layout, worker concurrency)
- config: DOWNLOAD_ROOT / WORKER_ENABLED env + download defaults
- deps: yt-dlp in requirements, ffmpeg in Dockerfile runtime stage
- worker: dedicated container (python -m app.worker), thin idle entry (loop lands in M2)
- localdev compose: worker service + downloads bind-mount; gitignore downloads/
2026-07-02 23:57:40 +02:00
|
|
|
security_opt:
|
|
|
|
|
- apparmor:unconfined
|
|
|
|
|
volumes:
|
|
|
|
|
- ./downloads:/downloads
|
2026-07-05 02:07:05 +02:00
|
|
|
- plex_media:/plex-media:ro
|
2026-06-11 16:35:00 +02:00
|
|
|
restart: unless-stopped
|
2026-06-16 15:21:43 +02:00
|
|
|
|
2026-07-03 04:40:10 +02:00
|
|
|
# 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
|
|
|
|
|
|
2026-06-16 15:21:43 +02:00
|
|
|
volumes:
|
|
|
|
|
pgdata:
|
2026-07-05 02:07:05 +02:00
|
|
|
# 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"
|