13 KiB
Plex Migration — Windows → Linux, nothing lost
✅ EXECUTED 2026-08-01 — M0 through M4 complete in one day
Plex is live on Linux with its original identity (
eb69ec8a…), watch state intact, direct play verified with a real session. In soak (M5). This file is now the record of the procedure; deviations that a future re-run must know about:
- VACUUM is mandatory after the remap. The no-backslash gate greps the raw DB file and hits deleted rows lingering on SQLite free pages. Plain sqlite3/python cannot open the DB (custom
icu_rootcollation — do NOT fake it); run VACUUM with Plex's own SQLite viadocker run --entrypoint.- Expect phantom rows from dead eras. 1,715 backslash rows survived the remap legitimately: Sync+ cache from old D:/E:/G: drive letters (+ orphan
media_items), 6 rows for a dead\\teslaNAS, and 115 UNC blobs indownload_queue_items.decision_result. All deleted; 3 real theme-music paths re-pathed. The remap itself was correct — these predate the current library roots.- Verify staged copies with
rsync -an --delete --stats(0 transferred / 0 deleted), neverdu -sbequality — directory apparent sizes differ across filesystems.- First boot needs
systemctl start --no-blockin cloud-init runcmd —enablealone misses the boot it runs in (fixed inuser-data).- Post-go-live, the Plex image pin was reversed (Mike, 2026-08-02):
:latest+ watchtower daily 05:00 with ntfy. Watchtower needsDOCKER_API_VERSION=1.44against Engine 29.Cleanup 2026-08-02: staging dir +
.pre-remap/.pre-syncfixsidecars deleted. Rollback net = NASmedia_pcbackup + Plex's scheduled DB backups.
Supersedes the earlier "forward-only, fresh Plex" decision. That call was made before the library was measured. At 27 TB, 25,148 video items and 337 GB of already-generated preview cache, rebuilding fresh costs one to three weeks of saturated NAS link and permanently loses every manual match fix. Migration is the cheaper and lower-risk option.
Revised 2026-07-31: no Windows-side robocopy, D: itself is the backup
until everything on it is copied to the NAS media_pc share from Linux, and
the 337 GB Media/ cache lives on the 3 TB ext4 disk — not the SSD — from
day one. D: is therefore formatted before Plex first starts, not after
the soak.
What survives
Watch history and resume points across all items, play counts, ratings, collections, playlists, every manual match correction, poster and artwork choices, added-at dates (so Recently Added stays meaningful), the 337 GB preview cache, and the server identity — shared users stay invited, clients do not need to re-add the server, and there is no claim step at all.
Calibre (25.4 GB) and Nikola iPad (1.2 GB) also survive — they move to the
NAS media_pc share. Nothing on D: is abandoned.
Plex's own article calls cross-OS moves "possible but not officially supported
and not covered here." That is a documentation gap, not a technical barrier.
The database is platform-independent; what Plex declines to help with is
mapping the settings, which gen-preferences.ps1 does.
Facts everything below depends on
| Source | PMS 1.43.3.10828, Windows 10, data dir D:\Plex\Local Data |
| Server identity | machineIdentifier eb69ec8a… — preserved, not regenerated |
| Data directory | ~374 GB total → ~364 GB after pruning (~27 GB minus Media/) |
| Library | 1,360 movies · 23,493 episodes · 295 other video · 20,798 audio |
| Path rows | 80,126 media_parts · 14,837 media_streams · 11 → 9 roots |
| Target layout | DB + Metadata/ on the SSD · Media/ (337 GB) on the 3 TB ext4 disk |
| Backup | Full copy on NAS media_pc (write share), made from Linux before D: is formatted |
| NAS mounts | 6 library shares read-only + media_pc read-write, at /mnt/nas/<exact share name> |
M0 — Prepare, while Windows is still running
Disable automatic trash emptying first. The registry confirms
autoEmptyTrash = 1. The new NAS account is read-only on the library shares,
so Plex can no longer delete files — but a missing mount at scan time would
still purge library records. Settings → Library → untick Empty trash
automatically after every scan.
Generate the Linux settings file from the registry. Write it to D:, not
C: — C: is the install target and gets wiped:
powershell -ExecutionPolicy Bypass -File .\gen-preferences.ps1 -Out D:\mbx\Preferences.xml
[xml](Get-Content -Raw D:\mbx\Preferences.xml) | Out-Null; 'XML OK'
Stop Plex Media Server completely — tray icon → Exit, then confirm no
Plex Media Server.exe remains. This is the database's final clean shutdown;
a copy taken mid-WAL is inconsistent and you will not find out until much
later. Do not start Plex on Windows again after this.
There is no robocopy here. D: is untouched by the install and is the
backup until M2 copies it off.
GATE M0 —
D:\mbx\Preferences.xmlexists, parses, and containsMachineIdentifier. Plex is fully exited.
M1 — Install Linux
Follow the main runbook.md phases 2–8 (BIOS — the iGPU toggle is mandatory,
Quick Sync is the transcode device — then USB, install, first boot, NAS
credentials and the seven mounts, shell-mcp cutover, vainfo). The
autoinstall targets Disk 0 by serial 1808AE802176 and leaves the 3 TB disk
untouched.
GATE M1 — runbook gates 4–8 all pass. In particular: all seven mounts
[ok], the six library shares refuse writes,media_pcaccepts them, andvainfolists H.264/HEVC entrypoints.
M2 — Copy everything off D:
Mount the NTFS disk read-only and copy all of it to the NAS write share:
DEV=/dev/$(lsblk -dno NAME,SERIAL | awk '$2=="WD-WCC7K3JAEDR3"{print $1}')
sudo mkdir -p /mnt/dwin
sudo mount -t ntfs3 -o ro "${DEV}1" /mnt/dwin # partition number per lsblk
# 1) Full PMS backup — excludes match the old robocopy list
sudo rsync -a --info=progress2 \
--exclude='Cache/' --exclude='Codecs/' --exclude='Drivers/' \
--exclude='Crash Reports/' --exclude='Logs/' --exclude='Updates/' \
"/mnt/dwin/Plex/Local Data/Plex Media Server/" \
"/mnt/nas/media_pc/backups/mediabox/PMS/"
# 2) The generated settings file
sudo rsync -a "/mnt/dwin/mbx/" "/mnt/nas/media_pc/backups/mediabox/mbx/"
# 3) Everything else worth keeping — nothing on D: is abandoned
sudo rsync -a --info=progress2 "/mnt/dwin/Calibre/" "/mnt/nas/media_pc/Calibre/"
sudo rsync -a --info=progress2 "/mnt/dwin/Nikola iPad/" "/mnt/nas/media_pc/Nikola iPad/"
Codecs and Drivers are Windows binaries — carrying them to Linux
breaks transcoding. Cache is regenerable.
Expect this to take a long time — Metadata/ and Media/ are millions of
small files, and small-file writes over CIFS are slow. Plan on hours, possibly
overnight. It only has to happen once.
GATE M2 — rsync exits 0 on all four copies. Spot-check on the NAS that
backups/mediabox/PMS/Plug-in Support/Databases/,Metadata/andMedia/are present and thatCalibre/andNikola iPad/file counts look right. Do not proceed to M3 on a copy you have not listed.
Optional rehearsal: before M3, run migrate-db.sh on a scratch copy of the
database from /mnt/dwin and start a throwaway Plex against it on an
--internal Docker network (no internet, so it cannot collide with the
server identity). Browse it and confirm counts and watch state. This proves
the migration while D: is still intact.
M3 — Stage, format D:, place the data
Media/ cannot stay on the SSD (337 GB, Mike's call 2026-07-31) and cannot
live on the NAS (network cost + RAID space on disposable cache). Its home is
the 3 TB disk — which currently holds the source data. So: stage through the
SSD, format, move.
# 1) Restore the config (everything EXCEPT Media/) to its real home on the SSD
sudo install -d -o 3000 -g 3000 /srv/plex/config
sudo rsync -a --info=progress2 --exclude='Media/' \
"/mnt/dwin/Plex/Local Data/Plex Media Server/" \
"/srv/plex/config/Library/Application Support/Plex Media Server/" \
--exclude='Cache/' --exclude='Codecs/' --exclude='Drivers/' \
--exclude='Crash Reports/' --exclude='Logs/' --exclude='Updates/'
sudo install -o 3000 -g 3000 -m 0644 /mnt/dwin/mbx/Preferences.xml \
"/srv/plex/config/Library/Application Support/Plex Media Server/Preferences.xml"
# 2) Stage Media/ on the SSD — temporary, ~337 GB into ~400 GB free. Check first:
df -h /
sudo rsync -a --info=progress2 \
"/mnt/dwin/Plex/Local Data/Plex Media Server/Media/" \
/srv/plex-media-staging/
# 3) Unmount and format D: — BY SERIAL, never /dev/sdX
sudo umount /mnt/dwin
lsblk -o NAME,SERIAL,SIZE,FSTYPE
DEV=/dev/$(lsblk -dno NAME,SERIAL | awk '$2=="WD-WCC7K3JAEDR3"{print $1}')
echo "$DEV" # sanity-check this before continuing
sudo wipefs -a "$DEV"
sudo parted -s "$DEV" mklabel gpt mkpart primary ext4 0% 100%
sudo mkfs.ext4 -L mediabox-data "${DEV}1"
UUID=$(blkid -s UUID -o value "${DEV}1")
echo "UUID=$UUID /srv/data ext4 defaults,nofail 0 2" | sudo tee -a /etc/fstab
sudo mkdir -p /srv/data && sudo systemctl daemon-reload && sudo mount /srv/data
# 4) Move Media/ to its permanent home (fast local move, no NAS round trip)
sudo install -d -o 3000 -g 3000 /srv/data/plex-media
sudo rsync -a --info=progress2 --remove-source-files /srv/plex-media-staging/ /srv/data/plex-media/
sudo find /srv/plex-media-staging -type d -empty -delete
sudo chown -R 3000:3000 /srv/plex/config /srv/data/plex-media
The staging copy through the SSD is deliberate: local disk-to-disk at full SATA speed instead of a 337 GB round trip through gigabit CIFS. The NAS copy from M2 is the backup, not the restore source — unless something here fails, in which case it is exactly that.
GATE M3 —
df -h /srv/datashows ~2.7 TB, survives a reboot./srv/data/plex-mediaand the config tree are owned3000:3000. The staging directory is gone and the SSD is back to mostly free.
M4 — Remap and first start
sudo /srv/mediabox-bootstrap/migration/migrate-db.sh \
"/srv/plex/config/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db"
sudo RENDER_GID=$(stat -c '%g' /dev/dri/renderD128) \
/srv/mediabox-bootstrap/scripts/50-plex.sh
migrate-db.sh keeps a .pre-remap rollback copy, asserts that no Windows
paths remain, that the counts moved 1:1, that the "Dr. Korval" tag and the
four Liaden book summaries are untouched, and finally stats 200 random
remapped paths against the real filesystem.
As executed: the no-backslash gate additionally required deleting phantom
Sync+ cache rows from dead drive-letter eras, 6 dead \\tesla rows and 115
download_queue_items UNC blobs, then VACUUM with Plex's own SQLite
(see the banner at the top) before the raw-file grep came back clean.
No claim token. The server already has its identity. The compose bind-mounts
/srv/data/plex-media over .../Plex Media Server/Media, so the migrated
thumbnails are in place from the first start.
scripts/60-media-relocate.sh is no longer needed — Media/ is born on
/srv/data. What still matters from it is the verification: after first
start, force thumbnail generation on one title and confirm the write lands
under /srv/data/plex-media — that is the EXDEV cross-filesystem question,
which can only be tested, not reasoned about.
GATE M4 —
migrate-db.shexits 0 with200/200 sampled files resolve on disk.plex.thewichersfamily.comloads and shows Magi Plex with Movies 1,360 and 23,493 episodes; watch history and Continue Watching intact; a remote client that already had this server still sees it without re-adding. Play something and confirmintel_gpu_topshows the video engine working. The one-title thumbnail test writes under/srv/data/plex-media.
M5 — Soak
One to two weeks. Confirm mounts survive a NAS reboot, the GPU survives a kernel update, and the box comes back unattended from a real power cut. That last one is the acceptance test for "headless".
Leave the NAS media_pc backup in place — it is the durable copy of the
old server. Prune it later if the space is ever needed; that is a deliberate
decision, not a cleanup step.
If something goes wrong
Before M3 step 3 (the format), D: is intact and everything is reversible on
the data side — Windows itself is gone after M1, but a reinstall from the USB
takes ~20 minutes and every copy still exists. After the format, the NAS
media_pc backup is the recovery source: it outlives every step here.
(The on-box .pre-remap/.pre-syncfix sidecars were deleted 2026-08-02
after the soak-start all-clear.)