services: podx-web: build: context: ./app args: BASE_IMAGE: ${GPU_BASE_IMAGE:-python:3.11-slim} container_name: podx-web env_file: [.env] environment: MEILI_URL: http://meili:7700 REDIS_URL: redis://redis:6379/0 LIBRARY_ROOT: /library TRANSCRIPT_ROOT: /library TMP_ROOT: /tmpdl WHISPER_MODEL: ${WHISPER_MODEL:-large-v3} WHISPER_PRECISION: ${WHISPER_PRECISION:-int8} WHISPER_DEVICE: ${WHISPER_DEVICE:-cpu} WHISPER_CPU_THREADS: ${WHISPER_CPU_THREADS:-4} TRANSCRIBE_BACKEND: ${TRANSCRIBE_BACKEND:-local} OPENAI_API_KEY: ${OPENAI_API_KEY:-} OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1} OPENAI_TRANSCRIBE_MODEL: ${OPENAI_TRANSCRIBE_MODEL:-whisper-1} OPENAI_TRANSCRIBE_TIMEOUT: ${OPENAI_TRANSCRIBE_TIMEOUT:-600} OPENWEBUI_AUTO_FIX_METADATA: ${OPENWEBUI_AUTO_FIX_METADATA:-1} OPENWEBUI_METADATA_TEMPLATE_JSON: ${OPENWEBUI_METADATA_TEMPLATE_JSON:-} MEDIA_NORMALIZE: ${MEDIA_NORMALIZE:-1} MEDIA_NORMALIZE_KEEP_ORIGINAL: ${MEDIA_NORMALIZE_KEEP_ORIGINAL:-0} VIDEO_NORMALIZE_CODEC: ${VIDEO_NORMALIZE_CODEC:-h264_nvenc} VIDEO_NORMALIZE_EXTENSION: ${VIDEO_NORMALIZE_EXTENSION:-.mp4} VIDEO_NORMALIZE_CRF: ${VIDEO_NORMALIZE_CRF:-28} VIDEO_NORMALIZE_PRESET: ${VIDEO_NORMALIZE_PRESET:-medium} VIDEO_NORMALIZE_TUNE: ${VIDEO_NORMALIZE_TUNE:-} VIDEO_NORMALIZE_AUDIO_CODEC: ${VIDEO_NORMALIZE_AUDIO_CODEC:-aac} VIDEO_NORMALIZE_AUDIO_BITRATE: ${VIDEO_NORMALIZE_AUDIO_BITRATE:-160k} AUDIO_NORMALIZE_CODEC: ${AUDIO_NORMALIZE_CODEC:-libmp3lame} AUDIO_NORMALIZE_EXTENSION: ${AUDIO_NORMALIZE_EXTENSION:-.mp3} AUDIO_NORMALIZE_BITRATE: ${AUDIO_NORMALIZE_BITRATE:-192k} AUDIO_NORMALIZE_CHANNELS: ${AUDIO_NORMALIZE_CHANNELS:-2} OPENWEBUI_URL: ${OPENWEBUI_CONTAINER_URL:-http://open-webui:8080} OPENWEBUI_API_KEY: ${OPENWEBUI_API_KEY} OPENWEBUI_KB_NAME: ${OPENWEBUI_KB_NAME:-Homelab Library} OPENWEBUI_KB_ID: ${OPENWEBUI_KB_ID:-} volumes: - ${TMP_HOST_DIR:-./tmp}:/tmpdl - ${MODELS_HOST_DIR:-./models}:/root/.cache/huggingface - ./app:/app - /mnt/skynet-media/data/media/podx-video:/library/video - /mnt/skynet-media/data/media/podx-audio:/library/audio ports: ["8088:8080"] depends_on: [podx-worker, meili, redis, open-webui] restart: unless-stopped extra_hosts: - host.docker.internal:host-gateway healthcheck: test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/health || exit 1"] interval: 30s timeout: 5s retries: 3 # Main worker: handles downloads, indexing, RSS, OWUI, etc. (no heavy Whisper) podx-worker: build: context: ./app args: BASE_IMAGE: ${GPU_BASE_IMAGE:-python:3.11-slim} container_name: podx-worker command: ["rq", "worker", "-u", "redis://redis:6379/0", "default"] env_file: [.env] environment: MEILI_URL: http://meili:7700 REDIS_URL: redis://redis:6379/0 LIBRARY_ROOT: /library TRANSCRIPT_ROOT: /library TMP_ROOT: /tmpdl WHISPER_MODEL: ${WHISPER_MODEL:-large-v3} WHISPER_PRECISION: ${WHISPER_PRECISION:-int8} WHISPER_DEVICE: ${WHISPER_DEVICE:-cpu} WHISPER_CPU_THREADS: ${WHISPER_CPU_THREADS:-4} WHISPER_BEAM_SIZE: ${WHISPER_BEAM_SIZE:-1} WHISPER_LOG_SEGMENTS: ${WHISPER_LOG_SEGMENTS:-1} WHISPER_RESUME: ${WHISPER_RESUME:-1} WHISPER_PARTIAL_SAVE_EVERY_SEGS: ${WHISPER_PARTIAL_SAVE_EVERY_SEGS:-20} TRANSCRIBE_BACKEND: ${TRANSCRIBE_BACKEND:-local} OPENAI_API_KEY: ${OPENAI_API_KEY:-} OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1} OPENAI_TRANSCRIBE_MODEL: ${OPENAI_TRANSCRIBE_MODEL:-whisper-1} OPENAI_TRANSCRIBE_TIMEOUT: ${OPENAI_TRANSCRIBE_TIMEOUT:-600} OPENWEBUI_AUTO_FIX_METADATA: ${OPENWEBUI_AUTO_FIX_METADATA:-1} OPENWEBUI_METADATA_TEMPLATE_JSON: ${OPENWEBUI_METADATA_TEMPLATE_JSON:-} MEDIA_NORMALIZE: ${MEDIA_NORMALIZE:-1} MEDIA_NORMALIZE_KEEP_ORIGINAL: ${MEDIA_NORMALIZE_KEEP_ORIGINAL:-0} VIDEO_NORMALIZE_CODEC: ${VIDEO_NORMALIZE_CODEC:-hevc} VIDEO_NORMALIZE_EXTENSION: ${VIDEO_NORMALIZE_EXTENSION:-.mp4} VIDEO_NORMALIZE_CRF: ${VIDEO_NORMALIZE_CRF:-28} VIDEO_NORMALIZE_PRESET: ${VIDEO_NORMALIZE_PRESET:-medium} VIDEO_NORMALIZE_TUNE: ${VIDEO_NORMALIZE_TUNE:-} VIDEO_NORMALIZE_AUDIO_CODEC: ${VIDEO_NORMALIZE_AUDIO_CODEC:-aac} VIDEO_NORMALIZE_AUDIO_BITRATE: ${VIDEO_NORMALIZE_AUDIO_BITRATE:-160k} AUDIO_NORMALIZE_CODEC: ${AUDIO_NORMALIZE_CODEC:-libmp3lame} AUDIO_NORMALIZE_EXTENSION: ${AUDIO_NORMALIZE_EXTENSION:-.mp3} AUDIO_NORMALIZE_BITRATE: ${AUDIO_NORMALIZE_BITRATE:-192k} AUDIO_NORMALIZE_CHANNELS: ${AUDIO_NORMALIZE_CHANNELS:-2} WORKER_MODE: all NVIDIA_VISIBLE_DEVICES: ${NVIDIA_VISIBLE_DEVICES:-all} NVIDIA_DRIVER_CAPABILITIES: ${NVIDIA_DRIVER_CAPABILITIES:-video,compute,utility} OPENWEBUI_URL: ${OPENWEBUI_CONTAINER_URL:-http://open-webui:8080} OPENWEBUI_API_KEY: ${OPENWEBUI_API_KEY} OPENWEBUI_KB_NAME: ${OPENWEBUI_KB_NAME:-Homelab Library} OPENWEBUI_KB_ID: ${OPENWEBUI_KB_ID:-} PYTHONPATH: /app JOB_TIMEOUT: ${JOB_TIMEOUT:-14400} JOB_TTL: ${JOB_TTL:-86400} RESULT_TTL: ${RESULT_TTL:-86400} FAILURE_TTL: ${FAILURE_TTL:-86400} volumes: - ${TMP_HOST_DIR:-./tmp}:/tmpdl - ${MODELS_HOST_DIR:-./models}:/root/.cache/huggingface - ./app:/app - /mnt/skynet-media/data/media/podx-video:/library/video - /mnt/skynet-media/data/media/podx-audio:/library/audio runtime: ${DOCKER_GPU_RUNTIME:-runc} depends_on: [meili, redis, open-webui] restart: unless-stopped healthcheck: test: ["CMD-SHELL", "exit 0"] extra_hosts: - host.docker.internal:host-gateway # Transcribe-only worker: listens to the "transcribe" queue and runs Whisper jobs podx-worker-transcribe: build: context: ./app args: BASE_IMAGE: ${GPU_BASE_IMAGE:-python:3.11-slim} container_name: podx-worker-transcribe command: ["rq", "worker", "-u", "redis://redis:6379/0", "transcribe"] env_file: [.env] environment: MEILI_URL: http://meili:7700 REDIS_URL: redis://redis:6379/0 LIBRARY_ROOT: /library TRANSCRIPT_ROOT: /library TMP_ROOT: /tmpdl WHISPER_MODEL: ${WHISPER_MODEL:-large-v3} WHISPER_PRECISION: ${WHISPER_PRECISION:-int8} WHISPER_DEVICE: ${WHISPER_DEVICE:-cpu} WHISPER_CPU_THREADS: ${WHISPER_CPU_THREADS:-4} WHISPER_LOG_SEGMENTS: ${WHISPER_LOG_SEGMENTS:-1} WHISPER_RESUME: ${WHISPER_RESUME:-1} WHISPER_PARTIAL_SAVE_EVERY_SEGS: ${WHISPER_PARTIAL_SAVE_EVERY_SEGS:-20} TRANSCRIBE_BACKEND: ${TRANSCRIBE_BACKEND:-local} OPENAI_API_KEY: ${OPENAI_API_KEY:-} OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1} OPENAI_TRANSCRIBE_MODEL: ${OPENAI_TRANSCRIBE_MODEL:-whisper-1} OPENAI_TRANSCRIBE_TIMEOUT: ${OPENAI_TRANSCRIBE_TIMEOUT:-600} OPENWEBUI_AUTO_FIX_METADATA: ${OPENWEBUI_AUTO_FIX_METADATA:-1} OPENWEBUI_METADATA_TEMPLATE_JSON: ${OPENWEBUI_METADATA_TEMPLATE_JSON:-} MEDIA_NORMALIZE: ${MEDIA_NORMALIZE:-1} MEDIA_NORMALIZE_KEEP_ORIGINAL: ${MEDIA_NORMALIZE_KEEP_ORIGINAL:-0} VIDEO_NORMALIZE_CODEC: ${VIDEO_NORMALIZE_CODEC:-hevc} VIDEO_NORMALIZE_EXTENSION: ${VIDEO_NORMALIZE_EXTENSION:-.mp4} VIDEO_NORMALIZE_CRF: ${VIDEO_NORMALIZE_CRF:-28} VIDEO_NORMALIZE_PRESET: ${VIDEO_NORMALIZE_PRESET:-medium} VIDEO_NORMALIZE_TUNE: ${VIDEO_NORMALIZE_TUNE:-} VIDEO_NORMALIZE_AUDIO_CODEC: ${VIDEO_NORMALIZE_AUDIO_CODEC:-aac} VIDEO_NORMALIZE_AUDIO_BITRATE: ${VIDEO_NORMALIZE_AUDIO_BITRATE:-160k} AUDIO_NORMALIZE_CODEC: ${AUDIO_NORMALIZE_CODEC:-libmp3lame} AUDIO_NORMALIZE_EXTENSION: ${AUDIO_NORMALIZE_EXTENSION:-.mp3} AUDIO_NORMALIZE_BITRATE: ${AUDIO_NORMALIZE_BITRATE:-192k} AUDIO_NORMALIZE_CHANNELS: ${AUDIO_NORMALIZE_CHANNELS:-2} WORKER_MODE: transcribe NVIDIA_VISIBLE_DEVICES: ${NVIDIA_VISIBLE_DEVICES:-all} NVIDIA_DRIVER_CAPABILITIES: ${NVIDIA_DRIVER_CAPABILITIES:-video,compute,utility} OPENWEBUI_URL: ${OPENWEBUI_CONTAINER_URL:-http://open-webui:8080} OPENWEBUI_API_KEY: ${OPENWEBUI_API_KEY} OPENWEBUI_KB_NAME: ${OPENWEBUI_KB_NAME:-Homelab Library} OPENWEBUI_KB_ID: ${OPENWEBUI_KB_ID:-} PYTHONPATH: /app JOB_TIMEOUT: ${JOB_TIMEOUT:-14400} JOB_TTL: ${JOB_TTL:-86400} RESULT_TTL: ${RESULT_TTL:-86400} FAILURE_TTL: ${FAILURE_TTL:-86400} volumes: - ${TMP_HOST_DIR:-./tmp}:/tmpdl - ${MODELS_HOST_DIR:-./models}:/root/.cache/huggingface - ./app:/app - /mnt/skynet-media/data/media/podx-video:/library/video - /mnt/skynet-media/data/media/podx-audio:/library/audio runtime: ${DOCKER_GPU_RUNTIME:-runc} depends_on: [meili, redis, open-webui] restart: unless-stopped healthcheck: test: ["CMD-SHELL", "exit 0"] extra_hosts: - host.docker.internal:host-gateway meili: image: getmeili/meilisearch:v1.8 container_name: meili env_file: [.env] environment: MEILI_NO_ANALYTICS: "true" ports: ["7700:7700"] volumes: - ${MEILI_DATA_HOST_DIR:-./data/meili}:/meili_data restart: unless-stopped redis: image: redis:7-alpine container_name: redis volumes: - ${REDIS_DATA_HOST_DIR:-./data/redis}:/data restart: unless-stopped ollama: image: ollama/ollama:latest container_name: ollama ports: - "11434:11434" volumes: - ${OLLAMA_DATA_HOST_DIR:-./data/ollama}:/root/.ollama tty: true restart: unless-stopped open-webui: image: ghcr.io/open-webui/open-webui:main container_name: open-webui environment: - OLLAMA_BASE_URL=http://ollama:11434 - WEBUI_SECRET_KEY=${OPENWEBUI_SECRET_KEY:-} volumes: - ${OPENWEBUI_DATA_HOST_DIR:-./data/open-webui}:/app/backend/data depends_on: - ollama ports: - "3003:8080" extra_hosts: - host.docker.internal:host-gateway restart: unless-stopped healthcheck: test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/api/health || exit 1"] interval: 30s timeout: 5s retries: 3 metube: image: alexta69/metube:latest container_name: metube ports: - "8081:8081" environment: - PUID=1000 - PGID=1000 - TZ=Europe/Prague - DOWNLOAD_DIR=/downloads/video - OUTPUT_TEMPLATE=%(uploader)s/%(upload_date)s - %(title)s.%(ext)s # Optional: pass a cookies file to bypass consent/age walls # - COOKIE_FILE=/config/cookies.txt # Optional: yt-dlp options (JSON). Example enables Android client fallback # - YTDL_OPTIONS={"extractor_args":{"youtube":{"player_client":"android"}}} - YTDL_OPTIONS={"extractor_args":{"youtube":{"player_client":"android"}},"extract_flat":"in_playlist","concurrent_fragment_downloads":1,"writesubtitles":true,"writeautomaticsub":true,"subtitleslangs":["en.*"],"convertsubs":"srt","writeinfojson":true,"writethumbnail":true,"converttumbnails":"jpg"} volumes: - /mnt/skynet-media/data/media/podx-video:/downloads # Optional cookies file on host → /config/cookies.txt inside container # - /mnt/secure/cookies.txt:/config/cookies.txt:ro restart: unless-stopped # Scanner: watches /library and enqueues jobs (heavy jobs go to "transcribe" queue) podx-scanner: build: context: ./app args: BASE_IMAGE: ${GPU_BASE_IMAGE:-python:3.11-slim} container_name: podx-scanner command: ["python", "scanner.py"] env_file: [.env] environment: MEILI_URL: http://meili:7700 REDIS_URL: redis://redis:6379/0 LIBRARY_ROOT: /library TRANSCRIPT_ROOT: /library TRANSCRIBE_QUEUE: transcribe SCAN_INTERVAL: 30 JOB_TIMEOUT: ${JOB_TIMEOUT:-14400} JOB_TTL: ${JOB_TTL:-86400} RESULT_TTL: ${RESULT_TTL:-86400} FAILURE_TTL: ${FAILURE_TTL:-86400} volumes: - ./app:/app - /mnt/skynet-media/data/media/podx-video:/library/video - /mnt/skynet-media/data/media/podx-audio:/library/audio depends_on: [redis] healthcheck: test: ["CMD-SHELL", "exit 0"] restart: unless-stopped podx-rss: build: context: ./app args: BASE_IMAGE: ${GPU_BASE_IMAGE:-python:3.11-slim} container_name: podx-rss command: ["python", "rss_ingest.py"] env_file: [.env] environment: MEILI_URL: http://meili:7700 REDIS_URL: redis://redis:6379/0 LIBRARY_ROOT: /library TRANSCRIPT_ROOT: /library PODCASTS_ROOT: /library PODCASTS_PER_SHOW: ${PODCASTS_PER_SHOW:-true} FEEDS_FILE: /library/feeds.txt RSS_STATE_FILE: /library/.rss_state.json RSS_SCAN_MINUTES: ${RSS_SCAN_MINUTES:-120} RSS_CONNECT_TIMEOUT: ${RSS_CONNECT_TIMEOUT:-15} RSS_READ_TIMEOUT: ${RSS_READ_TIMEOUT:-60} AUDIO_MAX_MB: ${AUDIO_MAX_MB:-4096} USER_AGENT: ${USER_AGENT:-podx-rss/1.0 (+local-archive)} RSS_ONCE: ${RSS_ONCE:-0} volumes: - ./app:/app - /mnt/skynet-media/data/media/podx-audio:/library depends_on: [redis] healthcheck: test: ["CMD-SHELL", "python - <<'PY'\nimport os,sys; p=os.getenv('FEEDS_FILE',''); sys.exit(0 if (p and os.path.exists(p)) else 1)\nPY"] interval: 60s timeout: 5s retries: 3 restart: unless-stopped