277 lines
13 KiB
Markdown
277 lines
13 KiB
Markdown
# 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:
|
||
>
|
||
> 1. **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_root` collation —
|
||
> do NOT fake it); run VACUUM with **Plex's own SQLite** via
|
||
> `docker run --entrypoint`.
|
||
> 2. **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 `\\tesla` NAS, and 115 UNC
|
||
> blobs in `download_queue_items.decision_result`. All deleted; 3 real
|
||
> theme-music paths re-pathed. The remap itself was correct — these
|
||
> predate the current library roots.
|
||
> 3. **Verify staged copies with `rsync -an --delete --stats`** (0 transferred
|
||
> / 0 deleted), never `du -sb` equality — directory apparent sizes differ
|
||
> across filesystems.
|
||
> 4. **First boot needs `systemctl start --no-block`** in cloud-init runcmd —
|
||
> `enable` alone misses the boot it runs in (fixed in `user-data`).
|
||
> 5. Post-go-live, the Plex image pin was **reversed** (Mike, 2026-08-02):
|
||
> `:latest` + watchtower daily 05:00 with ntfy. Watchtower needs
|
||
> `DOCKER_API_VERSION=1.44` against Engine 29.
|
||
>
|
||
> Cleanup 2026-08-02: staging dir + `.pre-remap`/`.pre-syncfix` sidecars
|
||
> deleted. Rollback net = NAS `media_pc` backup + 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
|
||
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.xml` exists, parses, and contains
|
||
> `MachineIdentifier`. 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_pc` accepts them, and
|
||
> `vainfo` lists 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:
|
||
|
||
```bash
|
||
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/` and `Media/`
|
||
> are present and that `Calibre/` and `Nikola 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.
|
||
|
||
```bash
|
||
# 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/data` shows ~2.7 TB, survives a reboot.
|
||
> `/srv/data/plex-media` and the config tree are owned `3000:3000`. The
|
||
> staging directory is gone and the SSD is back to mostly free.
|
||
|
||
---
|
||
|
||
## M4 — Remap and first start
|
||
|
||
```bash
|
||
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.sh` exits 0 with `200/200 sampled files resolve on
|
||
> disk`. `plex.thewichersfamily.com` loads 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 confirm `intel_gpu_top` shows 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.)
|