Jump to content

Services/Jellyfin

From Stronghold Wiki

As of: 2026-07-05 — migrated from `services/JELLYFIN.md`

Jellyfin — Media Server

Operations, backup, and recovery guide for stronghold's Jellyfin install.

Criticality: MEDIUM — Multi-user service. Media files themselves are large and not in scope for cloud backup (rerippable from physical media as a last resort). Server state (DB, metadata, plugins, config, subtitles) IS in scope — losing it means rebuilding the library from scratch, redoing all manual metadata fixes, and losing watch history. This is the only stronghold service used by multiple people.


Overview

Jellyfin is the Free Software media server. Stronghold runs it as a native systemd service (not Docker) because of the package's tight integration with the host (ffmpeg, GPU, file permissions).

What's special about this install:

  • Native systemd, not containerised. Updates via apt, not Watchtower.
  • Metadata/cache/transcode paths have been relocated away from /var/lib/jellyfin to /library/jellyfin-server-app/. The old paths still exist on disk but are stale (untouched since Oct 2025).
  • ~74G of /var/lib/jellyfin/data is regenerable cache (trickplay, subtitle extraction, internal backups). Only ~16G is genuinely irreplaceable.
  • Uses SQLite — Jellyfin does not support PostgreSQL/MySQL in any non-experimental form as of 10.11.

Current Version: 10.11.8+ubu2404 (Debian package). Latest is 10.11.9 (released 2026-05-21) — pending upgrade; see [Maintenance](#maintenance).

Users: Multi-user — Marcus + family/friends. Auth is local Jellyfin accounts (not Authentik-federated).


Service Information

Property Value
Package jellyfin (Debian, Ubuntu 24.04 repo)
Systemd unit /usr/lib/systemd/system/jellyfin.service
Process user jellyfin:jellyfin
Working directory /var/lib/jellyfin
Environment file /etc/default/jellyfin
Binary /usr/bin/jellyfin
HTTP port (internal) 8096
HTTPS port (internal) 8920 (unused — TLS terminated upstream)
Public URL (whatever your reverse proxy/Tailscale exposes — internal-only by default)

Companion services

Service Purpose Where
logo-cache (Docker, nginx:alpine) Jellyfin / IPTV support tool. Static HTTP server for the IPTV feed used by Jellyfin Live TV and the Apple TV IPTV client app. Serves: channel logos (30-day immutable cache) + filtered.m3u playlist + filtered.xml EPG (no-cache, must-revalidate — updater rewrites every ~3h via atomic rename). Stood up 2026-05-24. Compose: /library/logo-cache-app/ · Listens: 0.0.0.0:8079 → container :80 · Mounts: /raid1/Consolidated/IPTV/logos → /usr/share/nginx/html:ro (logo files) and /raid1/Consolidated/IPTV → /srv/iptv:ro (parent dir, exposed via nginx alias for /filtered.m3u + /filtered.xml only — directory bind, not single-file, so atomic-rename updates are picked up live) · Watchtower: enabled

Data layout (the important table)

Path Purpose Size Backup?
/etc/jellyfin/ System config (system.xml, network.xml, encoding.xml, users) <1M YES — mounted as /data/jellyfin-etc
/var/lib/jellyfin/data/jellyfin.db Core library DB (items, users, playstate) 1.3G YES via SQLite .backup to /dumps/ — live file excluded from Restic
/var/lib/jellyfin/data/playback_reporting.db Playback history (from plugin) small YES via SQLite .backup to /dumps/
/var/lib/jellyfin/data/subtitles/ Extracted/cached subs (.srt, .ass, .sup) ~40G YES — re-extraction is slow + lossy
/var/lib/jellyfin/data/collections/, playlists/, ScheduledTasks/, livetv/, attachments/ User-curated state <100M YES
/var/lib/jellyfin/plugins/ Installed plugins + their configs 14M YES
/var/lib/jellyfin/root/ Library definitions 7M YES
/var/lib/jellyfin/metadata/ STALE (legacy, pre-relocation, last write Oct 2025) 14G NO — excluded
/var/lib/jellyfin/data/trickplay/ Generated thumbnails for scrub previews 16G NO — scheduled task regenerates
/var/lib/jellyfin/data/backups/ Jellyfin's own internal backup plugin output 15G NO — we have our own
/var/lib/jellyfin/data/just-in-case/ Leftover from some operation 2G NO — investigate + delete
/var/lib/jellyfin/data/library.db.old, library.db.bak, library.db-wal Pre-10.9 schema consolidation leftovers ~700M NO — safe to delete
/var/lib/jellyfin/data/SQLiteBackups/ Jellyfin's auto-DB backups (separate from /data/backups) varies NO — we have our own
/library/jellyfin-server-app/metadata/ ACTIVE metadata (images, NFOs, TMDB cache) 28G YES — mounted as /data/jellyfin-server
/library/jellyfin-server-app/cache/ Image transcodes, web cache 739M NO — excluded
/library/jellyfin-server-app/transcodes/ On-the-fly transcode working dir 588K NO — excluded

Confirm relocation in /etc/jellyfin/system.xml: <syntaxhighlight lang="xml"> <CachePath>/library/jellyfin-server-app/cache</CachePath> <MetadataPath>/library/jellyfin-server-app/metadata</MetadataPath> </syntaxhighlight>

Media files (NOT backed up)

  • /raid1/Consolidated/Video/Movies/, /raid1/Consolidated/Video/TV/, etc. — many TB
  • Strategy: Not backed up to cloud. Reproducible from physical media (rerip) as last resort. External RAID is the eventual local-only backup plan. Treat the library as effort-recoverable, not byte-recoverable.

Backup

The cardinal rule

Never raw-copy a live SQLite WAL DB. A cp jellyfin.db (or unmodified file backup) while the server is running can yield an inconsistent file that fails to open, or worse, looks fine but is silently corrupt. SQLite's .backup command is the only safe online snapshot mechanism.

How it works on stronghold

Two things conspire to make Jellyfin backup safe:

  1. host-dump-databases.sh runs as ExecStartPre for backup.service (03:00 CST daily). For each SQLite DB it runs:
  ```sh
  sqlite3 /var/lib/jellyfin/data/jellyfin.db ".backup '$DUMP_DIR/jellyfin-$DATE.db'"
  gzip -f "$DUMP_DIR/jellyfin-$DATE.db"
  ```
  The .backup command takes a short shared lock, copies pages atomically, and produces a file that's identical to a VACUUM INTO-style snapshot. Jellyfin keeps running throughout.
  1. backup-to-hetzner.sh then runs Restic. The live jellyfin.db, playback_reporting.db, and all their -wal/-shm sidecars are excluded so the only DB copy in the snapshot is the consistent one from /dumps/.

Restic also excludes the regenerable dirs (trickplay, internal backups, just-in-case, stale metadata path, cache, transcodes).

What's actually in a snapshot

Per nightly Restic run for Jellyfin:

Source mount Inside snapshot Approx after exclusions
/etc/jellyfin/data/jellyfin-etc All config XML <1M
/var/lib/jellyfin/data/jellyfin plugins/, root/, data/{subtitles,collections,playlists,ScheduledTasks,livetv,attachments} ~40G (subtitles dominate)
/library/jellyfin-server-app/data/jellyfin-server metadata/ 28G
/library/backup-stack/dumps//dumps jellyfin-YYYYMMDD.db.gz, jellyfin-playback-YYYYMMDD.db.gz ~300M compressed

First snapshot ≈ 70G stored. Daily incrementals tiny thanks to Restic dedup/compression.

Verifying the backup

<syntaxhighlight lang="bash">

  1. Confirm today's SQLite dump was created

ls -lh /library/backup-stack/dumps/jellyfin-*.db.gz

  1. Spot-check dump integrity (extract + open)

gunzip -k /library/backup-stack/dumps/jellyfin-$(date +%Y%m%d).db.gz sqlite3 /tmp/jellyfin-test.db "PRAGMA integrity_check;" # expect "ok"

  1. Confirm Restic snapshot contains the right paths

docker exec restic-backup restic snapshots --latest 1 docker exec restic-backup restic ls latest | grep -E "^/data/jellyfin-(etc|server)?" | head -20 </syntaxhighlight>


Recovery

Scenario A: corrupt or lost jellyfin.db

The DB is the most fragile and most critical piece. Recovery in order of cost:

  1. Use the live data/SQLiteBackups/ if it's recent and intact — Jellyfin keeps its own rolling SQLite copies here.
  2. Restore from /library/backup-stack/dumps/jellyfin-YYYYMMDD.db.gz (today's dump, lives locally on stronghold).
  3. Restore from Restic (Hetzner) if local dumps are gone too.

Procedure (option 2 or 3): <syntaxhighlight lang="bash"> sudo systemctl stop jellyfin

  1. Move aside the broken DB

sudo mv /var/lib/jellyfin/data/jellyfin.db{,.broken-$(date +%s)} sudo rm -f /var/lib/jellyfin/data/jellyfin.db-{wal,shm}

  1. From local dump:

sudo gunzip -c /library/backup-stack/dumps/jellyfin-YYYYMMDD.db.gz \

 | sudo tee /var/lib/jellyfin/data/jellyfin.db >/dev/null

sudo chown jellyfin:jellyfin /var/lib/jellyfin/data/jellyfin.db

  1. OR from Restic:

docker exec restic-backup restic restore latest \

 --target /tmp/jellyfin-restore \
 --include /dumps/jellyfin-YYYYMMDD.db.gz

sudo gunzip -c /tmp/jellyfin-restore/dumps/jellyfin-*.db.gz \

 | sudo tee /var/lib/jellyfin/data/jellyfin.db >/dev/null

sudo chown jellyfin:jellyfin /var/lib/jellyfin/data/jellyfin.db

sudo systemctl start jellyfin </syntaxhighlight>

Scenario B: full server state loss

Rebuild order:

  1. Install jellyfin package, do not start the service yet.
  2. Restic restore /data/jellyfin-etc//etc/jellyfin/.
  3. Restic restore /data/jellyfin//var/lib/jellyfin/ (minus the excluded paths — they'll regenerate).
  4. Restic restore /data/jellyfin-server//library/jellyfin-server-app/.
  5. Restore jellyfin.db and playback_reporting.db from /dumps/ per Scenario A.
  6. chown -R jellyfin:jellyfin /var/lib/jellyfin /library/jellyfin-server-app.
  7. systemctl start jellyfin. First start will regenerate trickplay, transcode dirs, etc.
  8. Re-scan libraries from web UI (idempotent — won't re-import existing items).

Scenario C: media library on /raid1 is gone

Server state survives independently. Restore server state per Scenario B; then:

  • Media has to come back from physical media (rerip) or external RAID if one exists.
  • The library DB still references the original paths, so once the files reappear at the same paths, items light back up. No re-import needed.

Health checks

Add as a weekly cron / systemd timer (see [TODO](#known-issues--tracked-work)):

<syntaxhighlight lang="bash"> sqlite3 /var/lib/jellyfin/data/jellyfin.db "PRAGMA integrity_check;" sqlite3 /var/lib/jellyfin/data/playback_reporting.db "PRAGMA integrity_check;" </syntaxhighlight>

Any output other than ok → page Matrix.

Also useful, opportunistically: <syntaxhighlight lang="bash">

  1. What's holding the DB right now (should be ONLY the jellyfin process)

sudo lsof /var/lib/jellyfin/data/jellyfin.db

  1. Confirm WAL is on (it should always say "wal")

sudo sqlite3 /var/lib/jellyfin/data/jellyfin.db "PRAGMA journal_mode;"

  1. Lock-error counts in the last day

sudo grep -c "database table is locked" /var/log/jellyfin/log_$(date +%Y%m%d).log </syntaxhighlight>


Maintenance

Apt update path

Jellyfin updates via the Ubuntu apt repo, not Watchtower (it's not a container).

<syntaxhighlight lang="bash">

  1. Check current vs latest

apt list --installed 2>/dev/null | grep jellyfin

  1. Latest tag: https://api.github.com/repos/jellyfin/jellyfin/releases/latest
  1. Upgrade pattern (always snapshot DB first):

sudo systemctl stop jellyfin sudo sqlite3 /var/lib/jellyfin/data/jellyfin.db ".backup '/library/backup-stack/dumps/jellyfin-pre-upgrade-$(date +%Y%m%d).db'" sudo apt update && sudo apt install --only-upgrade jellyfin jellyfin-server jellyfin-web sudo systemctl start jellyfin </syntaxhighlight>

Scheduled tasks worth knowing about

Visible in the web UI at Dashboard → Scheduled Tasks. Two that matter:

  • Extract Subtitles — runs daily, can cause UserData lock contention if it overlaps active playback (see [Gotchas](#gotchas)). Schedule defensively (e.g. 04:30, not 01:00).
  • Refresh Library — keeps jellyfin.db warm. Don't run multiple library refreshes in parallel.

Gotchas

SQLite "database table is locked" errors are internal, not external

If you see SQLite Error 6: 'database table is locked: UserData' in /var/log/jellyfin/log_*.log:

  • Error code 6 is SQLITE_LOCKED — same-process shared-cache contention. NOT another OS process holding the file. So it is not a backup/cp problem.
  • Code 5 (SQLITE_BUSY) would indicate external process contention. We don't see that.
  • Typical cause: PlaybackReporting plugin is in a tight write loop on UserData for an active session, and Extract Subtitles scheduled task fires at the same time and tries to write the same table.
  • Mitigations:
    1. Move Extract Subtitles schedule off active-viewing hours.
    2. Test disabling PlaybackReporting plugin (you lose history but isolate the variable).
    3. Upgrade — 10.11.9 (PR #15368 + #16845) explicitly touches UserManager EF Core + user session concurrency logging.

These errors abort the failing transaction cleanly. They do not corrupt the DB.

Metadata path relocation

system.xml points MetadataPath and CachePath to /library/jellyfin-server-app/ — not the package default of /var/lib/jellyfin/. The default-location dirs still exist with stale content from before relocation. Don't be fooled into thinking /var/lib/jellyfin/metadata is the live one; check system.xml to confirm. Both dirs are listed in the data-layout table above with their actual roles.

The .sup subtitle files

PGS (bitmap) subs from Blu-rays. Most are 5–30MB, but some legitimately exceed 100MB or 1GB — e.g. animated sing-along subtitles (Wicked's .sup is 1.1GB and intentional). Don't auto-delete by size. Look up the title via BaseItems.Id matching the dir hash (dashed-uppercase GUID) before deleting any oversized .sup. Genuine broken extractions can go; Jellyfin re-extracts on demand.

DB lookups

The on-disk subtitle dir name like da/dab167ae-31c1-aa0a-7b64-dd821c44b879/4.sup maps to a BaseItems.Id. The DB stores it dashed and uppercase: <syntaxhighlight lang="bash"> sudo sqlite3 /var/lib/jellyfin/data/jellyfin.db \

 "SELECT Name, Type, Path FROM BaseItems WHERE Id='DAB167AE-31C1-AA0A-7B64-DD821C44B879';"

</syntaxhighlight>

Jellyfin's own backup plugin

There's a built-in Backup plugin that writes to data/backups/. We don't rely on it — our own .backup dumps are simpler to reason about. The data/backups/ dir is excluded from Restic for that reason. If you ever want to use the built-in plugin instead, flip the exclusion and update this doc.

Stale library.db.* files

Jellyfin 10.9 consolidated multiple SQLite DBs into a single jellyfin.db. The leftover library.db.old, library.db.bak1, library.db-wal (~700M total) under /var/lib/jellyfin/data/ are safe to delete after confirming Jellyfin starts cleanly without them — Jellyfin will not reach for them again.


Known issues / tracked work

See TODO.md § Jellyfin for the live list. Current outstanding items:

  • [ ] Investigate + delete oversized .sup files that are genuinely broken (size alone isn't a signal; manual eyeball required).
  • [ ] Delete pre-10.9 schema cruft (library.db.old, library.db.bak1, etc.).
  • [ ] Weekly PRAGMA integrity_check → Matrix alert on anything other than ok.
  • [ ] Upgrade to 10.11.9 (UserManager/EF Core fixes; minor, do alongside next routine maintenance window).
  • [ ] Haruhi S01E01 has a nightly subtitle-extraction failure (Unable to get streams for File:...) — investigate file integrity.

References

  • Backup stack overview: services/BACKUP_SYSTEM.md
  • Restic include/exclude patterns: /library/backup-stack/scripts/backup-to-hetzner.sh
  • SQLite dump invocations: /library/backup-stack/scripts/host-dump-databases.sh
  • Disaster recovery procedure: DISASTER_RECOVERY.md (this service's recovery section above is the authoritative copy)
  • Upstream changelog: https://github.com/jellyfin/jellyfin/releases