Self-hosted media library file transformation automation
Flowarr automates media file transformations for private media servers running Jellyfin, Plex or Emby. It watches filesystem directories, runs configurable jobs (transcoding, subtitle extraction and conversion), and holds all processing while someone is streaming, or outside a time window you choose, so jobs never compete with playback for the GPU.
Built for the Steam Deck media server problem: transcode h.264 → HEVC for ~60% space savings, extract ASS/SSA subtitles to SRT sidecars for direct-play compatibility.
- GPU-Accelerated Transcoding: HEVC encoding via NVENC or VAAPI (AMD/Intel) with automatic detection and libx265 fallback; HDR sources are tonemapped to SDR; all audio, subtitle and attachment tracks are kept
- Subtitle Extraction: Extract text-based subtitles (SRT, ASS, SSA, WebVTT, mov_text) to
<name>.<lang>.srtsidecars, optionally stripping them from the video - Subtitle Conversion: Convert ASS/SSA/WebVTT subtitle files to SRT without ever overwriting an existing SRT
- Playback-Aware Pausing: Jellyfin, Plex and Emby webhooks suspend running jobs (SIGSTOP on the whole ffmpeg process tree) and resume them when playback ends
- Processing Window: Only process during chosen hours (e.g. 22:00–06:00); jobs are suspended outside the window
- Live Execution Control: Progress, logs and failure reasons for every file; pause, resume, stop and retry individual or bulk executions
- Smart Rescans: Files are fingerprinted (size + mtime), so unchanged files are never reprocessed and changed files are picked up again
- Worker Pools: Per-job-type concurrency (1–10 processes) scaled live via supervisord, with crash recovery for lost executions
- Notifications: Webhook notifications on completed/failed executions (Discord, Slack and generic JSON)
- REST API: Token-authenticated
/api/v1for status, libraries, executions and pause/resume, handy for Homepage dashboards and scripts - Web UI: Dashboard, libraries, executions and workers via Inertia + React
- Authentication: Registration, login, passkeys (WebAuthn), email verification, 2FA/TOTP
| Layer | Technology |
|---|---|
| Backend | PHP 8.5, Laravel 13, Fortify |
| Frontend | React 19, Inertia v3, TypeScript, Tailwind v4, shadcn/ui |
| Database | PostgreSQL 18 |
| Queue | Database driver |
| Cache | Redis |
| Containers | Docker (single Debian-based image: nginx + PHP-FPM + supervisord queue workers) |
Pull the prebuilt image from GHCR, configure environment, and start with Docker Compose.
Prerequisites
- Docker Engine 24+ with Compose plugin
- An
APP_KEY(generate withopenssl rand -base64 32) - A PostgreSQL 18-compatible password
- (Optional) A running Traefik reverse proxy for TLS / homelab routing
PostgreSQL 18 note: The volume mounts at
/var/lib/postgresql(not/var/lib/postgresql/data). PG 18+ requires a mount at the parent directory so it can create a version-specific subdirectory, which is needed for cleanpg_upgradelater. See docker-library/postgres#1259.
# 1. Pull the compose file and env template
curl -O https://raw.githubusercontent.com/fais649/flowarr/main/docker-compose.prod.yml
curl -O https://raw.githubusercontent.com/fais649/flowarr/main/.env.prod.example
# 2. Configure
cp .env.prod.example .env.prod
# Edit .env.prod — at minimum set:
# APP_KEY=$(openssl rand -base64 32)
# DB_PASSWORD=<your-password>
# DOMAIN=<your-domain> # only needed with Traefik
# APP_URL=https://<your-domain> # only needed with Traefik
# 3. Start
docker compose --env-file .env.prod -f docker-compose.prod.yml up -d.env.prod serves double duty: it feeds Compose variable substitution via --env-file, and it's bind-mounted into the container (./.env.prod:/var/www/html/.env.prod) as the app's own config.
Visit http://localhost:8080/register (direct access) or https://<your-domain>/register (with Traefik) to create the first user.
Build from source instead of pulling:
:latestonly exists after a release tag is published — pushes tomainpublish:edge. Until then, build locally:git clone https://github.com/Fais649/flowarr && cd flowarr && docker build -t ghcr.io/fais649/flowarr:latest . docker compose --env-file .env.prod -f docker-compose.prod.yml up -d
The stack runs three services: PostgreSQL 18, Redis 7, and the Flowarr app container.
services:
flowarr-postgres:
image: postgres:18-alpine
volumes:
- postgres-data:/var/lib/postgresql # PG 18+ needs mount at /var/lib/postgresql, not /data
healthcheck:
test: ["CMD", "pg_isready", "-q", "-d", "flowarr", "-U", "flowarr"]
start_period: 60s # grace for first-start initdb
retries: 5
flowarr-redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
flowarr:
image: ghcr.io/fais649/flowarr:latest
ports:
- "8080:8080" # remove for Traefik-only access
environment:
APP_KEY: # required — generate with `openssl rand -base64 32`
APP_URL: # https://<your-domain>
DB_PASSWORD: # required — matches POSTGRES_PASSWORD above
RUN_MIGRATIONS: "true"
volumes:
- /path/to/media:/media # writable — Flowarr writes transcoded files & sidecars here
- flowarr-storage:/var/www/html/storageThe production compose file is at docker-compose.prod.yml in the repo root, and the Laravel Sail development compose file is at compose.yaml. Environment variables are documented in .env.prod.example.
The Docker image (ghcr.io/fais649/flowarr:latest) is a single Debian-based container where supervisord manages nginx, PHP-FPM, and the queue worker processes.
- Port: 8080 (internal), FastCGI proxy to
127.0.0.1:9000 - PHP: 8.5, extensions: pgsql, pdo_pgsql, bcmath, zip, intl, pcntl, redis
- System tools: jellyfin-ffmpeg 7 (NVENC, VAAPI, zscale tonemapping), mkvtoolnix, bash, curl, postgresql-client
- Build: 3-stage (composer → assets → runtime), ~465 MB compressed
On first start, PostgreSQL initializes its data directory (can take 30-60s). The healthcheck has start_period: 60s to allow for this — Docker won't mark it unhealthy during init.
Once Postgres is healthy, the Flowarr entrypoint runs in order:
- Requires
.env.prodto be mounted at/var/www/html/.env.prod— if it's absent, the container exits with an error message (no.envis auto-created) - Copies
.env.prodto.env - Clears the Laravel configuration, route, event, and view caches
- Runs database migrations if
RUN_MIGRATIONS=true(default) - Rebuilds the configuration, route, event, and view caches
- Starts supervisord, which runs PHP-FPM, nginx, the orchestration/notification queue worker, the library scanner worker, the task scheduler (
schedule:work, which drives interval library scans and crash recovery), and a one-shotqueue:orchestratestartup job
The transcode, subtitle-extraction, and subtitle-conversion worker pools (up to 10 processes each) are defined with autostart=false and are scaled by the orchestrator to each worker's configured concurrency. Default workers for all three job types are created on first migration. No manual artisan commands required.
APP_KEY is required — it is never auto-generated. If it's missing or empty, the app cannot encrypt sessions, cookies, or other encrypted data. Generate one and set it in your .env.prod:
openssl rand -base64 32
# → SaN+R05iCUuA64GtMh579p/MdA5giQLJw1q8wYQ3oB8=
# Set APP_KEY=base64:SaN+R05iCUuA64GtMh579p/MdA5giQLJw1q8wYQ3oB8= in .env.prodThe compose file includes Traefik v2/v3 discovery labels. To use them, run the stack with the docker-compose.traefik.yml override, which attaches the flowarr container to the external traefik network:
- Make sure Traefik is running with an
traefikDocker network:docker network create traefik(if absent) - Set
DOMAINin.env.prod— this becomes theHost()rule - Set
APP_URLtohttps://<your-domain> - Optionally remove the
ports:mapping (Traefik routes internally) - Start the stack:
docker compose --env-file .env.prod -f docker-compose.prod.yml -f docker-compose.traefik.yml up -d
Labels are pre-configured for TLS on the websecure entrypoint. The base compose file keeps the flowarr container on the flowarr internal network; the override additionally attaches it to the external traefik network.
/var/www/html/
├── public/ ← Laravel public dir (index.php, build/ assets)
├── storage/ ← persistent (mount as volume)
├── vendor/ ← Composer deps
├── app/ config/ etc ← Laravel app
Mount your media library at /media (writable — Flowarr writes transcoded files and subtitle sidecars here):
volumes:
- /path/to/your/media:/mediagit clone https://github.com/fais649/flowarr
cd flowarr
cp .env.example .env
./vendor/bin/sail up -d
./vendor/bin/sail artisan key:generate
./vendor/bin/sail composer setup
./vendor/bin/sail artisan migrate
./vendor/bin/sail artisan db:seedThe app will be available at http://localhost.
./vendor/bin/sail composer run dev # Starts application at localhost:80
./vendor/bin/sail artisan test # Run testsFlowarr holds processing while a media server reports active playback. The endpoint URLs are also listed under Config → Processing.
| Server | Setup |
|---|---|
| Jellyfin | Install the Webhook plugin, add a Generic destination pointed at http://your-flowarr-host/webhooks/jellyfin, enable Playback Start, Playback Stop (and optionally Playback Progress) and tick Send All Properties. |
| Plex | Settings → Webhooks → add http://your-flowarr-host/webhooks/plex (requires Plex Pass). |
| Emby | Settings → Webhooks → add http://your-flowarr-host/webhooks/emby with the playback events. |
Set MEDIA_SERVER_WEBHOOK_TOKEN to require a shared secret, sent as the X-Flowarr-Token header or as a ?token= query parameter (Plex cannot send custom headers). JELLYFIN_WEBHOOK_TOKEN is still honoured.
When a stream starts, running ffmpeg processes are suspended (SIGSTOP) and queued jobs wait; they resume (SIGCONT) when the last stream ends. Sessions without a stop event expire after the configurable stream timeout, and the Workers page can clear them manually.
Under Config → Processing you can:
- restrict processing to a daily window (in
APP_TIMEZONE, may wrap around midnight) - send notifications for completed and/or failed executions to a webhook URL (Discord, Slack or any JSON endpoint)
- generate a REST API token
Authenticate with Authorization: Bearer <token> (or X-Api-Key: <token>):
curl -H "Authorization: Bearer $FLOWARR_TOKEN" http://your-flowarr-host/api/v1/status
curl -X POST -H "Authorization: Bearer $FLOWARR_TOKEN" http://your-flowarr-host/api/v1/processing/pause
curl -H "Authorization: Bearer $FLOWARR_TOKEN" "http://your-flowarr-host/api/v1/executions?status=failed"See ARCHITECTURE.md for all endpoints.
TRANSCODE_HW_MODE=auto (default) uses NVENC when an NVIDIA GPU is visible, VAAPI when /dev/dri/renderD128 exists, and libx265 otherwise. The Workers page shows which encoder is detected.
- AMD / Intel (VAAPI): keep the
devices: - /dev/dri:/dev/drimapping indocker-compose.prod.yml. - NVIDIA: install the NVIDIA Container Toolkit on the host and use the commented
deploy.resourcesblock instead. - CPU only: remove the device mapping or set
ENABLE_GPU_TRANSCODING=false.
See ARCHITECTURE.md for the full entity schema, data flow, and implementation plan.
See CONTRIBUTING.md for setup instructions, test commands, and PR workflow.
MIT — see LICENSE.