Self-hosted media library file transformation automation
Flowarr automates media file transformations for private media servers running Jellyfin/Plex. It watches filesystem directories, runs configurable jobs (transcoding, subtitle extraction), and pauses all processing when Jellyfin streams are active to avoid GPU contention.
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: HDR → SDR tonemapped HEVC encoding via NVENC (with libx265 fallback)
- Subtitle Extraction: Extract text-based subtitles (SRT, ASS, SSA, WebVTT) to sidecar files, strip internal subtitle tracks
- Subtitle Conversion: Convert non-SRT subtitle files (VTT, ASS) to SRT
- Jellyfin Webhook Integration: Auto-pause all processing when Jellyfin streams are active, resume when they stop
- Library Management: Multiple media directories with per-library job configuration and scan intervals
- Execution Tracking: Full lifecycle tracking from queued → processing → completed/failed
- Queue-backed Jobs: database-backed job queues with per-job-type routing
- Web UI: Dashboard, library management, execution monitoring 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 image: nginx + PHP-FPM, Alpine-based) |
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 Alpine-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: ffmpeg 8.1, mkvtoolnix 99, 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 queue worker (
queue:work --queue=orchestration), the task scheduler (schedule:work, which drives interval library scans), and a one-shotqueue:orchestratestartup job
The transcode, subtitle-extraction, and subtitle-conversion worker pools (10 processes each) are defined with autostart=false and are started on demand by the orchestrator. 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 tests- Install the Webhook plugin in Jellyfin
- Add a webhook pointed at
http://your-flowarr-host/webhooks/jellyfin - Select events:
Playback startandPlayback stop - (Optional) Set
JELLYFIN_WEBHOOK_TOKENin.envto secure the endpoint
When a stream starts, all running ffmpeg/mkvmerge processes are paused (SIGSTOP). They resume (SIGCONT) when the stream ends.
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.