# Quick Start: Multi-Instance Jellyfin ## What Is This? Run **multiple Jellyfin instances** that share the same PostgreSQL database, allowing you to: - πŸš€ **Scale horizontally** - Add more instances for more users - ⚑ **Load balance** - Distribute streaming/transcoding across servers - πŸ› οΈ **Specialize** - Dedicate instances to specific tasks (scanning, serving, transcoding) - πŸ”„ **High availability** - If one instance fails, others continue --- ## Prerequisites βœ… PostgreSQL 12+ database βœ… All instances on **same OS** with **identical library paths** βœ… Shared network storage accessible from all instances βœ… Jellyfin with multi-instance support (this branch) --- ## Quick Setup (2 Instances) ### Step 1: Set Up Shared Database ```bash # Create PostgreSQL database createdb -U postgres jellyfin # Create user psql -U postgres -c "CREATE USER jellyfin WITH PASSWORD 'your_secure_password';" psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE jellyfin TO jellyfin;" ``` ### Step 2: Configure Instance 1 (Primary) Create `instance1/config/startup.json`: ```json { "InstanceId": "11111111-1111-1111-1111-111111111111", "InstanceName": "Jellyfin-Primary", "DatabaseProvider": "Postgres", "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=jellyfin;Username=jellyfin;Password=your_secure_password" }, "DataPath": "C:/Jellyfin/instance1/data", "WebDir": "wwwroot", "CachePath": "C:/Jellyfin/instance1/cache", "LogPath": "C:/Jellyfin/instance1/logs", "EnableMultiInstance": true, "InstanceConfiguration": { "HttpPort": 8096, "HttpsPort": 8920, "CanBecomePrimary": true, "Capabilities": { "CanScan": true, "CanTranscode": true, "CanServeApi": true, "CanStream": true } } } ``` ### Step 3: Configure Instance 2 (Secondary) Create `instance2/config/startup.json`: ```json { "InstanceId": "22222222-2222-2222-2222-222222222222", "InstanceName": "Jellyfin-Secondary", "DatabaseProvider": "Postgres", "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=jellyfin;Username=jellyfin;Password=your_secure_password" }, "DataPath": "C:/Jellyfin/instance2/data", "WebDir": "wwwroot", "CachePath": "C:/Jellyfin/instance2/cache", "LogPath": "C:/Jellyfin/instance2/logs", "EnableMultiInstance": true, "InstanceConfiguration": { "HttpPort": 8097, "HttpsPort": 8921, "CanBecomePrimary": false, "Capabilities": { "CanScan": false, "CanTranscode": true, "CanServeApi": true, "CanStream": true } } } ``` ### Step 4: Run Database Migration ```bash # Run ONCE from any instance directory dotnet ef database update -p src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres -s Jellyfin.Server ``` Or manually: ```bash psql -U jellyfin -d jellyfin -f docs/multi_instance_migration.sql ``` ### Step 5: Start Instances ```powershell # Terminal 1 - Instance 1 cd instance1 dotnet lib/Release/net11.0/jellyfin.dll # Terminal 2 - Instance 2 cd instance2 dotnet lib/Release/net11.0/jellyfin.dll ``` ### Step 6: Verify Check the database: ```sql SELECT * FROM library."Instances"; ``` You should see both instances registered! --- ## Load Balancer Configuration ### Nginx Example ```nginx upstream jellyfin_backend { least_conn; server localhost:8096 max_fails=3 fail_timeout=30s; server localhost:8097 max_fails=3 fail_timeout=30s; } server { listen 80; server_name jellyfin.example.com; location / { proxy_pass http://jellyfin_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WebSocket support proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } ``` ### HAProxy Example ```haproxy frontend jellyfin_front bind *:80 default_backend jellyfin_back backend jellyfin_back balance leastconn option httpchk GET /health server instance1 localhost:8096 check server instance2 localhost:8097 check ``` --- ## Architecture Patterns ### Pattern 1: Load Balanced API ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚Load Balancerβ”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”‚Instance1β”‚ β”‚Instance2β”‚ β”‚(Primary)β”‚ β”‚(Second) β”‚ β”‚Port 8096β”‚ β”‚Port 8097β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”‚PostgreSQL β”‚ β”‚ Database β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` **Use Case:** Scale web API and streaming **Capabilities:** - Instance1: `CanScan=true, CanTranscode=true, CanServeApi=true, CanStream=true` - Instance2: `CanScan=false, CanTranscode=true, CanServeApi=true, CanStream=true` ### Pattern 2: Dedicated Scanner ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Scanner β”‚ ← Runs library scans (Primary) β”‚(Background)β”‚ No public API β”‚ Instance β”‚ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”‚PostgreSQL β”‚ β”‚ Database β”‚ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β–Όβ”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”‚Serve1 β”‚ β”‚ Serve2 β”‚ ← Serve only β”‚API/ β”‚ β”‚ API/ β”‚ No scanning β”‚Stream β”‚ β”‚ Stream β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` **Use Case:** Separate scanning from serving **Capabilities:** - Scanner: `CanScan=true, CanTranscode=false, CanServeApi=false, CanStream=false` - Serve1/2: `CanScan=false, CanTranscode=true, CanServeApi=true, CanStream=true` ### Pattern 3: Geo-Distributed (Same Paths) ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ PostgreSQL β”‚ ← Accessible from all locations β”‚ (Central) β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β–Όβ”€β”€β”€β” β”Œβ”€β”€β”€β–Όβ”€β”€β”€β” β”Œβ”€β”€β”€β–Όβ”€β”€β”€β” β”‚Office1β”‚ β”‚Office2β”‚ β”‚Office3β”‚ β”‚8096 β”‚ β”‚8096 β”‚ β”‚8096 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ ``` **Use Case:** Multiple offices with local instances **Requirement:** All must have identical library paths (e.g., `Z:\Media`) --- ## Configuration Options ### Instance-Specific (startup.json) ```json { "InstanceId": "auto-generated-or-specified", "InstanceName": "Human-readable name", "EnableMultiInstance": true, "InstanceConfiguration": { "HttpPort": 8096, "HttpsPort": 8920, "CanBecomePrimary": true, "Capabilities": { "CanScan": true, // Library scanning "CanTranscode": true, // Transcoding "CanServeApi": true, // HTTP API "CanStream": true // Media streaming }, "HeartbeatInterval": 30, // Seconds between heartbeats "LockTimeout": 300 // Max seconds to hold a lock } } ``` ### Shared (Database) - Library paths - User accounts - Metadata settings - Playback settings --- ## Monitoring & Maintenance ### Check Instance Health ```sql SELECT "InstanceId", "Hostname", "HttpPort", "Status", "IsPrimary", "LastHeartbeat", EXTRACT(EPOCH FROM (NOW() - "LastHeartbeat")) AS seconds_since_heartbeat FROM library."Instances" WHERE "Status" = 'Active' ORDER BY "LastHeartbeat" DESC; ``` ### Cleanup Stale Instances ```sql SELECT library.cleanup_stale_instances(); ``` ### View Primary Instance ```sql SELECT library.get_primary_instance(); ``` ### Force Primary Election ```sql SELECT library.elect_primary_instance(); ``` --- ## Troubleshooting ### Issue: Multiple instances become primary **Cause:** Race condition or manual database changes **Solution:** ```sql -- Reset all to secondary UPDATE library."Instances" SET "IsPrimary" = FALSE; -- Elect new primary SELECT library.elect_primary_instance(); ``` ### Issue: Instance not registering **Symptoms:** Instance starts but doesn't appear in database **Check:** 1. Database connection string correct? 2. Migration applied? (`SELECT * FROM library."Instances"` should work) 3. `EnableMultiInstance=true` in startup.json? 4. Logs show registration errors? ### Issue: Stale instances not cleaning up **Symptoms:** Failed instances still show as Active **Solution:** ```sql -- Manual cleanup UPDATE library."Instances" SET "Status" = 'Failed' WHERE "LastHeartbeat" < NOW() - INTERVAL '5 minutes'; ``` --- ## Performance Tips ### 1. Connection Pooling ```json { "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=jellyfin;Username=jellyfin;Password=***;Pooling=true;MinPoolSize=5;MaxPoolSize=50" } } ``` ### 2. Dedicated Transcoding Instances - Set `CanServeApi=false, CanStream=false` on transcode-only instances - Allocate more CPU/RAM to these instances - Use separate cache directories per instance ### 3. Primary Instance - Give primary instance more CPU for scanning - Reduce `CanTranscode` capability on primary - Primary should be on fastest/most reliable server ### 4. Database Optimization ```sql -- Add more connections ALTER SYSTEM SET max_connections = 200; -- Increase work memory ALTER SYSTEM SET work_mem = '256MB'; -- Reload config SELECT pg_reload_conf(); ``` --- ## Security Best Practices ### 1. Network Security - Use PostgreSQL SSL connections - Firewall database access to known instance IPs - Use strong database passwords ### 2. Instance Isolation - Run each instance under separate OS user - Use separate cache/log directories - Limit transcoding temp paths ### 3. Access Control ```sql -- Create separate database user per instance CREATE USER instance1_user WITH PASSWORD 'secure_password_1'; CREATE USER instance2_user WITH PASSWORD 'secure_password_2'; -- Grant permissions GRANT ALL ON SCHEMA library TO instance1_user; GRANT ALL ON SCHEMA library TO instance2_user; ``` --- ## Migration from Single Instance ### Step 1: Backup ```bash # Backup database pg_dump jellyfin > jellyfin_backup_$(date +%Y%m%d).sql # Backup config cp -r config config_backup ``` ### Step 2: Update Config Add to `startup.json`: ```json { "EnableMultiInstance": true, "InstanceConfiguration": { "CanBecomePrimary": true, "Capabilities": { "CanScan": true, "CanTranscode": true, "CanServeApi": true, "CanStream": true } } } ``` ### Step 3: Run Migration ```bash dotnet ef database update ``` ### Step 4: Restart Your single instance will now register itself in the Instances table! --- ## Current Limitations (Phase 1) ⚠️ **Phase 1 Only - Foundation Complete, Services In Progress** ### βœ… What Works - Database schema for instance registration - Entity model complete - Build succeeds ### ❌ Not Yet Implemented - Automatic instance registration on startup - Heartbeat mechanism - Primary election logic - Distributed locking - Cache coordination - Session isolation **Status:** Schema ready, awaiting service implementation (Phases 2-6) --- ## Get Help - **Documentation:** See `docs/MULTI_INSTANCE_SUPPORT_PLAN.md` for architecture - **Implementation Status:** See `docs/MULTI_INSTANCE_SUPPORT_SUMMARY.md` - **Issues:** Report on GitHub with `multi-instance` label - **Testing:** Use branch `multi-instance-testing` --- **Ready to scale your Jellyfin deployment? Start with 2 instances and expand from there!** πŸš€