Services/Jellyfin
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/jellyfinto/library/jellyfin-server-app/. The old paths still exist on disk but are stale (untouched since Oct 2025). - ~74G of
/var/lib/jellyfin/datais 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:
host-dump-databases.shruns asExecStartPreforbackup.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.backupcommand takes a short shared lock, copies pages atomically, and produces a file that's identical to aVACUUM INTO-style snapshot. Jellyfin keeps running throughout.
backup-to-hetzner.shthen runs Restic. The livejellyfin.db,playback_reporting.db, and all their-wal/-shmsidecars 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">
- Confirm today's SQLite dump was created
ls -lh /library/backup-stack/dumps/jellyfin-*.db.gz
- 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"
- 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:
- Use the live
data/SQLiteBackups/if it's recent and intact — Jellyfin keeps its own rolling SQLite copies here. - Restore from
/library/backup-stack/dumps/jellyfin-YYYYMMDD.db.gz(today's dump, lives locally on stronghold). - Restore from Restic (Hetzner) if local dumps are gone too.
Procedure (option 2 or 3): <syntaxhighlight lang="bash"> sudo systemctl stop jellyfin
- 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}
- 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
- 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:
- Install
jellyfinpackage, do not start the service yet. - Restic restore
/data/jellyfin-etc/→/etc/jellyfin/. - Restic restore
/data/jellyfin/→/var/lib/jellyfin/(minus the excluded paths — they'll regenerate). - Restic restore
/data/jellyfin-server/→/library/jellyfin-server-app/. - Restore
jellyfin.dbandplayback_reporting.dbfrom/dumps/per Scenario A. chown -R jellyfin:jellyfin /var/lib/jellyfin /library/jellyfin-server-app.systemctl start jellyfin. First start will regenerate trickplay, transcode dirs, etc.- 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">
- What's holding the DB right now (should be ONLY the jellyfin process)
sudo lsof /var/lib/jellyfin/data/jellyfin.db
- Confirm WAL is on (it should always say "wal")
sudo sqlite3 /var/lib/jellyfin/data/jellyfin.db "PRAGMA journal_mode;"
- 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">
- Check current vs latest
apt list --installed 2>/dev/null | grep jellyfin
- 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
UserDatalock contention if it overlaps active playback (see [Gotchas](#gotchas)). Schedule defensively (e.g. 04:30, not 01:00). - Refresh Library — keeps
jellyfin.dbwarm. 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
UserDatafor an active session, and Extract Subtitles scheduled task fires at the same time and tries to write the same table. - Mitigations:
- Move
Extract Subtitlesschedule off active-viewing hours. - Test disabling PlaybackReporting plugin (you lose history but isolate the variable).
- Upgrade — 10.11.9 (PR #15368 + #16845) explicitly touches UserManager EF Core + user session concurrency logging.
- Move
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
.supfiles 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 thanok. - [ ] 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