Files
pgsql-jellyfin/docs/MULTI_INSTANCE_QUICKSTART.md
T
wjones 77e30685bb Complete multi-instance support: Phases 3–6 & deployment
- Implements Phases 3–6: session isolation, cache coordination, primary election, and file system monitor coordination for Jellyfin with PostgreSQL.
- Adds new database entities (Instance, DistributedLock, FileSystemChange) and EF model configurations.
- Includes SQL migration scripts and EF migration for all required tables, columns, and helper functions.
- Updates Device entity and JellyfinDbContext for multi-instance tracking.
- Integrates new DI services for instance registry, distributed locks, cache coordinator, and primary election.
- Adds publishing profiles (Win/Linux/FrameworkDependent) and automation script for deployment.
- Extensive documentation for architecture, setup, and publishing.
- All changes are backward compatible and build successfully.
2026-03-05 16:10:26 -05:00

12 KiB

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

# 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:

{
  "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:

{
  "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

# Run ONCE from any instance directory
dotnet ef database update -p src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres -s Jellyfin.Server

Or manually:

psql -U jellyfin -d jellyfin -f docs/multi_instance_migration.sql

Step 5: Start Instances

# 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:

SELECT * FROM library."Instances";

You should see both instances registered!


Load Balancer Configuration

Nginx Example

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

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)

{
  "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

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

SELECT library.cleanup_stale_instances();

View Primary Instance

SELECT library.get_primary_instance();

Force Primary Election

SELECT library.elect_primary_instance();

Troubleshooting

Issue: Multiple instances become primary

Cause: Race condition or manual database changes
Solution:

-- 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:

-- Manual cleanup
UPDATE library."Instances"
SET "Status" = 'Failed'
WHERE "LastHeartbeat" < NOW() - INTERVAL '5 minutes';

Performance Tips

1. Connection Pooling

{
  "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

-- 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

-- 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

# 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:

{
  "EnableMultiInstance": true,
  "InstanceConfiguration": {
    "CanBecomePrimary": true,
    "Capabilities": {
      "CanScan": true,
      "CanTranscode": true,
      "CanServeApi": true,
      "CanStream": true
    }
  }
}

Step 3: Run Migration

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! 🚀