Move from Plex to Jellyfin Without Losing Your Rollback Path
Do not migrate by deleting Plex and hoping Jellyfin feels familiar. Mirror the library first, give Jellyfin read-only media access, protect both applications' state, and keep Plex available until the household and the rollback test pass.
Rights and lawful use: This weekend plan covers transferring the organization and playback of media you own, created, or are authorized to use from Plex to Jellyfin. Side-by-side scanning, watched-state tools, metadata handling, and transcoding do not authorize acquisition, DRM bypass, paid sharing, or public distribution; review the access and terms of any third-party migration tool before using it.
Quick Answer
Run Jellyfin beside Plex with separate application data and the same authorized media mounted read-only. Inventory and back up Plex on Friday, scan a small library on Saturday, then test every important user, client, codec, subtitle, audio, and remote path. Do not retire Plex until Jellyfin state can be backed up and restored in a disposable instance and a failed acceptance branch has returned viewers to Plex.
Who this is for: This is for a Plex operator evaluating Jellyfin who needs a reversible cutover across existing media paths, users, watched state, remote access, hardware acceleration, and household clients. It assumes you can maintain separate Plex and Jellyfin configuration, cache, metadata, transcode, and database paths while both servers read the same authorized library during the pilot.
What Moves Cleanly, and What Does Not
The media files are the shared input, not the application database. Plex and Jellyfin should point at the same stable folder structure during the pilot, but each server needs its own configuration, database, cache, metadata, logs, and transcode directory. Never place one server's internal state inside the other server's app-data tree.
Jellyfin's migration documentation, rechecked on August 24, 2026, says its internal databases cannot be copied or adjusted easily. It links a third-party application programming interface (API) tool for selected Plex user and watched-status transfers. That is not a supported promise of complete Plex database conversion. Review the tool's source, permissions, release activity, and rollback behavior before giving it tokens or household viewing data.
| State | Migration approach | Cutover gate |
|---|---|---|
| Media files | Keep the existing names and paths; mount read-only into Jellyfin first. | Representative items scan and play without source-file changes. |
| Users and permissions | Create least-privilege Jellyfin users manually or evaluate a narrowly scoped third-party API tool. | Every user sees only intended libraries and has no administrator rights. |
| Watched state | Treat third-party transfer as optional; retain Plex as the authoritative rollback copy. | Sample completed, in-progress, and unwatched items match the written expectation. |
| Posters, collections, and playlists | Rescan, use reviewed local metadata, or rebuild deliberately. | Household-critical items are listed and checked; no claim of full parity is assumed. |
| Remote access | Build a separate Jellyfin address and access path after local playback works. | An off-LAN normal user can connect without administrator privileges or unrelated management-service exposure. |
| Application database | Do not copy Plex database files into Jellyfin. | Each application backs up and restores using its own documented method. |
Friday: Inventory the Working Plex System
Record the facts needed to reproduce the current service before changing it: host and operating system, Plex package or container image, media-server version, storage mounts, user and group IDs, library paths, remote hostname, certificate owner, hardware devices, clients, users, and backup location. Run the commands that match the deployment. The block below reads state; it does not change Plex or the library.
date -u '+%Y-%m-%dT%H:%M:%SZ'
uname -a
id
findmnt -T /srv/media
stat -c 'path=%n uid=%u gid=%g mode=%a type=%F' /srv/media
docker version --format 'docker_client={{.Client.Version}} docker_server={{.Server.Version}}'
docker compose version --short
docker inspect plex --format 'image={{.Config.Image}} id={{.Id}}'
docker inspect plex --format '{{range .Mounts}}{{println .Source "->" .Destination "rw=" .RW}}{{end}}'
docker image inspect "$(docker inspect plex --format '{{.Image}}')" --format '{{json .RepoDigests}}'
Illustrative output format only; not performed by TechGeeks. Values below are synthetic:
2026-08-24T14:30:00Z
Linux media-canary 6.8.0-example x86_64 GNU/Linux
uid=1000(media) gid=1000(media) groups=1000(media),44(video)
TARGET SOURCE FSTYPE OPTIONS
/srv/media /dev/mapper/media ext4 ro,relatime
docker_client=28.0.0-example docker_server=28.0.0-example
2.0.0-example
image=plexinc/pms-docker:<RECORDED_TAG> id=<CONTAINER_ID>
/srv/plex/config -> /config rw= true
/srv/media -> /media rw= false
["plexinc/pms-docker@sha256:<RECORDED_DIGEST>"]
The values are synthetic. Keep your actual command, working directory, time, exit code and output in private recovery notes. Redact hostnames, addresses, usernames, media names and tokens before sharing; do not turn the sample output into a claimed result.
Friday: Create the Rollback Point
Use Plex's platform-specific backup guidance for the real installation. Identify the running service/container and all app-data/settings paths first; Windows registry or macOS preferences may be separate. The illustrative paths above are not permission to stop an arbitrary container or archive an unverified directory.
In a planned viewing outage, stop the identified Plex instance and confirm it is stopped before copying its database state. Write a new, private backup artifact to a verified destination outside every live Plex data directory; preserve required ownership/metadata and do not overwrite an earlier backup. If stopping, copying, space checks or verification fails, stop the backup procedure and diagnose it. Restart the identified original service only as a separate deliberate recovery step, then verify playback. Do not continue migration on a partial archive.
Retain the matching image/version, configuration, mount definitions and protected credentials. A checksum detects later file changes, not database consistency or successful restoration. Test recovery on isolated storage before relying on this rollback point; never restore over the still-working Plex installation for practice.
Saturday: Start a Read-Only Jellyfin Pilot
This Compose file is a reference configuration, not a TechGeeks-deployed result. Replace every typed marker, store secrets outside the file, and record the image digest after pulling. The long volume syntax makes the media permission visible in review. Remove the graphics-device mapping if the host does not have /dev/dri; its presence alone does not prove hardware acceleration.
name: jellyfin-migration-pilot
services:
jellyfin:
image: ${JELLYFIN_IMAGE:?Set JELLYFIN_IMAGE in the dedicated pilot environment}
container_name: jellyfin-migration
user: "${PUID:?Set PUID in the dedicated pilot environment}:${PGID:?Set PGID in the dedicated pilot environment}"
group_add:
- "${JELLYFIN_RENDER_GID:?Set JELLYFIN_RENDER_GID in the dedicated pilot environment}"
ports:
- "${JELLYFIN_BIND_IP:?Set JELLYFIN_BIND_IP in the dedicated pilot environment}:8096:8096/tcp"
volumes:
- type: bind
source: ${JELLYFIN_CONFIG:?Set JELLYFIN_CONFIG in the dedicated pilot environment}
bind:
create_host_path: false
target: /config
- type: bind
source: ${JELLYFIN_CACHE:?Set JELLYFIN_CACHE in the dedicated pilot environment}
bind:
create_host_path: false
target: /cache
- type: bind
source: ${MEDIA_PATH:?Set MEDIA_PATH in the dedicated pilot environment}
bind:
create_host_path: false
target: /media
read_only: true
devices:
- /dev/dri/renderD128:/dev/dri/renderD128
restart: "no"
Create a dedicated, private pilot directory and a new environment file there; never overwrite a shared .env. The following is configuration text, not a shell script: replace every marker and let Compose parse it rather than sourcing it. Pin JELLYFIN_IMAGE to a reviewed real digest. Verify the private bind address and unique project/container names before starting. Required variables now fail when empty, and bind paths are not automatically created.
JELLYFIN_IMAGE=jellyfin/jellyfin@sha256:<RECORDED_DIGEST>
JELLYFIN_BIND_IP=<PRIVATE_LAN_IP>
JELLYFIN_CONFIG=/srv/jellyfin/config
JELLYFIN_CACHE=/srv/jellyfin/cache
MEDIA_PATH=/srv/media
PUID=<MEDIA_UID>
PGID=<MEDIA_GID>
JELLYFIN_RENDER_GID=<HOST_RENDER_DEVICE_GID>
Set JELLYFIN_RENDER_GID to the numeric group that owns the selected host render device; determine it from stat -c '%g' /dev/dri/renderD128. Some systems use a group named render, video, or input. Remove group_add and devices when hardware acceleration is not part of the pilot. A mapped device and supplementary group make access possible but do not prove acceleration.
Prepare only new, separate pilot config/cache directories with the documented numeric user/group permissions; do not recursively change a shared media tree. Before every start, verify the actual host media filesystem/share with the inventory and read an authorized sample. A directory existing is not proof that its NAS mount succeeded. If the expected mount is missing or wrong, stop: do not start Jellyfin, create a marker or let app writes proceed. This reference uses restart: "no" so reboot does not bypass that manual gate.
After the mount and private-directory checks pass, validate the explicit pilot file/environment with Compose config --quiet; inspect any fully rendered configuration only in private because it may contain secrets. Use the same explicit -f, --env-file and unique -p values for pull, start, status and stop. Pull the reviewed image, record its digest, then start only jellyfin. Stop at any failure and consult Jellyfin's container guide. No automatic provisioning/start script is supplied because actual paths, ownership and mount identity have not been verified here.
Before adding a library, inspect the container mount without writing into the source tree. The Bash example below requires jq and an exported MEDIA_PATH equal to the exact reviewed host source. It asserts one bind mount at /media with Docker's read-only flag. Any failure means stop the pilot through its explicitly selected Compose project and investigate; never interpret an arbitrary failed command as proof of read-only protection.
(
set -euo pipefail
: "${MEDIA_PATH:?Export the exact reviewed media source path}"
docker inspect jellyfin-migration | jq -e --arg source "$MEDIA_PATH" '
length == 1 and
([.[0].Mounts[] | select(.Destination == "/media")] |
length == 1 and .[0].Type == "bind" and
.[0].Source == $source and .[0].RW == false)
'
)
On success the assertion prints true and exits zero; no result is claimed here. This checks Docker's mount metadata, not every nested mount, host-side writer or storage failure mode.
Do not probe production media with a fixed-name touch followed by deletion: it can alter or remove an existing file, and failure may mean a missing container or denied permission rather than a read-only filesystem. Keep any destructive negative test in a disposable fixture, outside production media. After mount review, inspect the application health endpoint and read an authorized sample before adding a small library. Preserve all failures in the test notes.
Metadata, Users, and Watched State
Keep the source library unchanged while learning how Jellyfin matches it. Jellyfin can read and write local NFO files, and its current documentation says local NFO metadata takes priority over remote providers. A read-only mount prevents Jellyfin from writing new NFO or artwork beside the media. If the library already contains NFO files, inspect a synthetic or authorized sample before scanning because those files can affect matches and selected user data.
- Create a separate non-admin test user for each policy pattern: adult, child, remote, or restricted library.
- Record library visibility, downloads, remote access, transcoding rights, and password-recovery ownership.
- Treat Plex users, Plex Home relationships, collections, playlists, posters, and client preferences as manual acceptance items unless a reviewed tool explicitly handles them.
- If a watched-state tool is evaluated, use a dedicated least-privilege token, export before and after samples, revoke the token, and keep Plex authoritative until the comparison is accepted.
- Never put server tokens, household email addresses, library names, or viewing history into a public screenshot or command transcript.
Security, Privacy, Legal, and Recovery Boundaries
- Security: use separate normal-user accounts because the media application may serve playback and dashboard routes on one endpoint; keep unrelated host-management services off the playback path, run the pilot with the recorded user/group identity, patch both servers during overlap, and revoke temporary migration tokens.
- Privacy: media names, posters, users, watch history, addresses, logs, and tool exports can reveal household behavior. Use synthetic labels for evidence, minimize retention, and review every redaction at original resolution.
- Legal: migrate only authorized media and metadata. A copied library, NFO file, subtitle, or watched-state record does not create new content or distribution rights.
- Recovery: keep Plex state, media paths, and the prior remote route intact; back up Jellyfin separately; and maintain a local administrator path that does not depend on public DNS, a proxy, or a third-party migration service.
Sunday: Run the Real-Client Acceptance Matrix
Use lawful test media that represents the actual failure classes, not private title names. Record container, video codec, bit depth, high dynamic range (HDR) format, audio codec and channels, subtitle type, bitrate, duration, and a checksum. This command records media characteristics without changing the file:
(
set -eu
: "${SAMPLE:?Export an absolute path to an authorized local sample}"
case "$SAMPLE" in /*) ;; *) exit 1 ;; esac
test -f "$SAMPLE"
ffprobe -v error \
-show_entries format=filename,format_name,duration,bit_rate \
-show_entries stream=index,codec_type,codec_name,profile,pix_fmt,width,height,channels \
-of json "$SAMPLE"
)
Illustrative output shape only; not performed. Values and filename are synthetic:
{
"streams": [
{"index": 0, "codec_type": "video", "codec_name": "hevc", "profile": "Main 10", "pix_fmt": "yuv420p10le", "width": 3840, "height": 2160},
{"index": 1, "codec_type": "audio", "codec_name": "eac3", "channels": 6},
{"index": 2, "codec_type": "subtitle", "codec_name": "hdmv_pgs_subtitle"}
],
"format": {"filename": "/srv/test-media/synthetic-hdr-pgs.mkv", "format_name": "matroska,webm", "duration": "120.000000", "bit_rate": "25000000"}
}
| Client and network | Sample | Plex baseline | Jellyfin result | Account steps | Pass condition | Artifact ID |
|---|---|---|---|---|---|---|
| Main TV on LAN | Ordinary H.264 plus text subtitle | Record actual mode | Planned | Record sign-in and recovery | Starts, seeks, subtitles, audio, and resume work | Planned |
| Streaming box on LAN | HEVC/HDR plus image subtitle | Record actual mode | Planned | Record app availability | Expected HDR or documented tone map; no unexplained transcode | Planned |
| Browser on LAN | Unsupported codec trigger | Record reason | Planned | Normal user only | Dashboard and host telemetry agree on the conversion path | Planned |
| Phone off LAN | Capped remote stream | Record direct or relayed path | Planned | Record VPN, proxy, or account path | Works on a separate network without administrator privileges or unrelated management exposure | Planned |
For every row, capture server and client versions, connection path, playback decision, transcode reason, startup and seek outcome, delivered bitrate, subtitle and audio behavior, and central processing unit (CPU) or graphics processing unit (GPU) telemetry. A dashboard label alone does not prove image quality or hardware use. Corroborate it with the server log and process or GPU telemetry.
Validation and Cutover Gate
- Every household-critical client passes the agreed sample set locally.
- Every remote user can sign in from a separate network using the selected private or hardened path.
- Library visibility and least-privilege permissions match the written matrix.
- Metadata mismatches, collections, playlists, posters, and watched-state exceptions have owners and accepted outcomes.
- Expected Direct Play, Direct Stream, and transcode decisions are explained; no purchase is justified by an unexplained dashboard state.
- Jellyfin configuration and database state have a versioned backup outside the live config path.
- A disposable restore and a return-to-Plex rehearsal have passed with reviewed artifacts.
Backup, Restore, and Rollback
Jellyfin's current 10.11 backup documentation describes a built-in online backup under Dashboard > Backups. The database is always included; metadata, subtitles, and trickplay are selectable. The documentation recommends low activity with no scan running and warns that the 5 GB free-space precheck may not cover optional data. It also documents restore from the web interface or with --restore-archive PATH_TO_BACKUP_ZIP. Jellyfin has no downgrade mechanism, so record the running image digest and restore matching state rather than assuming an older image can open a migrated database.
For your recovery rehearsal, create a built-in backup, retain a protected copy outside live configuration, verify its checksum, and restore it into a disposable matching-version instance with no production media access. Check the expected test library, users, permissions and settings. This restore has not been performed for the article; checksum success alone does not meet the migration acceptance gate.
- Declare the acceptance branch failed and stop adding users or state to Jellyfin.
- Remove the Jellyfin public proxy, DNS, or VPN route if one was created; keep a local operator route long enough to export logs.
- Revoke migration-tool and temporary test tokens, then stop the Jellyfin container.
- Return each test client to the unchanged Plex server and verify one local and one off-LAN playback path.
- Confirm the Plex app-data backup checksum and retain it. Restore Plex only if the still-running Plex state is actually damaged.
- Keep the read-only media library untouched. Diagnose application state separately from media recovery.
Rollback is not complete when the Plex sign-in page opens. The planned rehearsal must prove a normal user can browse the intended library, resume a known synthetic item, and play locally. If remote access is part of the household requirement, that path must also return without granting the viewer administrator privileges or exposing unrelated management services.
Troubleshooting
| Symptom | Likely boundary | First check | Recovery move |
|---|---|---|---|
| Library path is empty | Host mount, container source path, user/group ID, or permissions | Compare findmnt, stat, and docker inspect with the inventory | Stop Jellyfin; correct the mount without changing Plex |
| Unexpected transcode | Client codec, container, audio, subtitle, bitrate, or GPU mapping | Read the playback reason and compare host telemetry | Keep Plex available; change one variable and repeat the same sample |
| Wrong match or poster | Naming, existing NFO, provider priority, or manual edits | Inspect one item and its local metadata | Remove the pilot library and rescan after correcting the policy |
| Watched state is incomplete | Third-party transfer scope or user mapping | Compare a written sample set, not total counts alone | Revoke the tool token and retain Plex as authoritative |
| Remote Jellyfin fails | DNS, certificate, VPN/proxy, firewall, NAT, or per-user permission | Prove local playback first, then trace the selected access path | Remove the new route and return the client to Plex |
| Restore will not start | Version mismatch, incomplete archive, ownership, or live database | Compare digest, checksum, paths, and logs in the disposable target | Discard the disposable target; do not alter either production app |
Evidence Still Required
No TechGeeks migration, scan, playback comparison, backup restore, timing or rollback rehearsal was performed for this revision. The original planned lab evidence remains outstanding; this is not a validated weekend migration result. Only revised Bash syntax, YAML structure and the nonwriting assertion were statically checked. Do not retire Plex until your own representative clients, permissions, isolated restore and return-to-Plex rehearsal pass.
Keep versions, configuration hashes, selected media traits, observed playback modes and restore results privately with the acceptance matrix. A health response, checksum or mount flag answers only its specific check, not the complete cutover decision.
What This Evidence Does Not Prove
- A read-only parallel scan does not prove every title, edition, extra, collection, playlist, poster, or NFO will match.
- A watched-state transfer does not prove complete user, permission, metadata, or database migration.
- One client matrix does not establish support for untested firmware, apps, codecs, subtitle formats, audio chains, or remote networks.
- A dashboard playback label does not prove viewer-perceived quality or that every decode, filter, tone-map, and encode stage used hardware.
- A successful backup checksum does not prove restore; a disposable restore does not prove a cross-platform or downgrade path.
- This documentation-backed revision reports no TechGeeks scan time, migration duration, resource measurement, playback-quality result, or rollback result.
Useful Gear And Buyer Notes
Buy only after the client matrix names a bottleneck. An external disk may protect application state; a compatible client may remove a transcode; a graphics processor matters only when required conversions exceed measured capacity; and network gear matters only when the tested network path is the constraint. The search links remain category references because models, bundles, sellers, and prices change.
Affiliate disclosure: As an Amazon Associate, TechGeeks may earn from qualifying purchases. The product links below are buying references, not a requirement to buy a specific brand or seller. Verify compatibility, seller quality, warranty, and current specs before ordering.
- Amazon search: external hard drive backup 12TB
- Amazon search: SATA SSD Docker appdata
- Amazon search: Intel N100 mini PC Jellyfin
- Amazon search: 2.5GbE unmanaged switch
- Amazon search: USB 2.5GbE adapter Linux
Related TechGeeks Resources
- Plex vs Jellyfin Remote Streaming owns the account, client, and access decision.
- Jellyfin Remote Access with Tailscale is a separate private-access topic; remote access is outside this local migration procedure.
- Plex Homelab Architecture covers storage, GPU, and library design.
- Media Server Storage Design covers NAS mounts, permissions, and cache placement.
- Monitoring and Health Checks covers ongoing server and automation visibility.
Series Navigation
References
- Jellyfin: Migrating
- Jellyfin: Backup and Restore
- Jellyfin: Container Installation
- Jellyfin: Intel GPU Container Access
- Jellyfin: Libraries
- Jellyfin: Local NFO Metadata
- Plex: Backing Up Plex Media Server Data
- Plex: Naming and Organizing Movie Files
Community discussions informed the migration questions but are not product authority: Plex versus Jellyfin discussion and Jellyfin client-experience discussion.
Final Thought
A migration is complete only when the new service works for normal viewers and the old service remains a proven exit. Keep Plex intact, make Jellyfin earn the cutover through the client matrix, then preserve both the Jellyfin restore artifact and the Plex rollback point.


