Reverses the forward-only decision. That call predated measuring the library; at 27 TB / 25,148 video items / 337 GB of generated preview cache, rebuilding costs 1-3 weeks of saturated gigabit and permanently loses every manual match fix across 23,493 episodes. Migration preserves watch history, resume points, collections, playlists, manual matches, artwork choices, added-at dates, the 337 GB cache, and server identity - so shared users stay invited, clients do not re-add, and there is no claim step at all. migration/remap.sql Four path columns remapped: media_parts.file (80,126), section_locations .root_path (11 -> 9), media_streams.url (14,837) and metadata_items.guid (9). Two formats, not one: backslash/UNC for the first two, file:// with %20 encoding for the last two - decoding those %20s would break every subtitle reference containing a space, which given share names like Radio Shows is most of them. A scan of all 80 text columns across 82 tables found SIX columns matching korval. Only four are paths. taggings.text is one row reading Dr. Korval, and metadata_items.summary is four Liaden Universe blurbs about Clan Korval. A bare REPLACE on the word would have corrupted a cast credit and four book summaries, so every statement is anchored to a path prefix. Proven against a synthetic database built from the real path shapes: 12/12, including both false positives surviving byte-identical. Also drops the Audio Books and Music Organized roots - configured as library roots but holding 0 files / 0 bytes. The consolidation had already happened; only the dead roots remained. migration/migrate-db.sh Runs the remap through Plex own SQLite build borrowed from the container image, keeps a .pre-remap rollback, asserts the counts moved 1:1 and the false positives did not, then stats 200 random remapped paths against the real filesystem. That last check is the one that matters - SQL running without error proves nothing. migration/gen-preferences.ps1 Plex live settings store on Windows is the REGISTRY, not Preferences.xml, and the two disagree here. Folds ~50 values into one Linux file, dropping Windows-only keys including the per-GPU limit keyed by 10de:1b81, the GTX 1070 PCI ID. Preserves MachineIdentifier and the online token, which is what keeps the server identity. The existing Preferences.xml is malformed anyway (duplicate allowedNetworks) and will not parse strictly. scripts/20-cifs.sh Eight shares down to six. Mount points now mirror the share names verbatim - /mnt/nas/Home Movies, space and all - because that reduces the remap to one uniform prefix substitution instead of eleven special cases. New requirement this creates: \040 escaping in BOTH fstab fields, not just the share name. Verified, plus a round-trip check that a remapped DB path lands under the generated mount point. plex/docker-compose.yml Six read-only NAS binds whose source path equals target path, so database, host and container agree with no translation. No PLEX_CLAIM. Phase 2 Media relocation present but commented. scripts/60-media-relocate.sh Post-soak, optional: moves the 337 GB cache to the 3TB ext4 disk so future growth (~28 MB per content-hour) stops eating the SSD. Plex does not support this, so the script forces generation on one title afterwards to prove writes survive the EXDEV boundary, and rollback is deleting a compose override. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mediabox-bootstrap
Everything the media box (10.0.1.20, hostname mediabox) needs after the
Ubuntu 24.04 autoinstall has laid down the base system.
The autoinstall USB is deliberately thin: sshd, a key, DHCP, Docker CE, /srv,
and a first-boot unit that clones this repo and runs bootstrap.sh. That
split exists so the interesting parts stay in git, reviewable and re-runnable,
instead of frozen onto a USB stick that nobody can diff.
Full context, decisions and the phase-by-phase runbook live in the workspace repo
at projects/mediabox-linux/.
Layout
bootstrap.sh orchestrator — runs the stages, fails soft
scripts/
00-preflight.sh asserts hardware/BIOS state. Changes nothing.
10-secrets.sh idempotent ADD-ONLY seeder for /srv/secrets/stacks.env
20-cifs.sh the 8 NAS mounts (--verify mode included)
30-nvidia.sh NVIDIA 580 + container toolkit
40-shell-mcp.sh builds and starts the native shell-mcp
50-plex.sh Plex. Run BY HAND — needs a live claim token.
shell-mcp/ amd64 build of the MCP server (server.py, Dockerfile,
docker-compose.yml, pinned requirements.txt)
plex/docker-compose.yml Plex service definition
secrets/stacks.env.example placeholder master env
Running it
sudo /srv/mediabox-bootstrap/bootstrap.sh # all stages
sudo /srv/mediabox-bootstrap/scripts/20-cifs.sh # one stage
sudo /srv/mediabox-bootstrap/scripts/20-cifs.sh --verify
Every stage is idempotent. Re-running is the normal way to use this, not an emergency measure.
Design rules
Nothing may leave the box unreachable. sshd is up before any of this runs
and no stage may compromise that. bootstrap.sh catches stage failures and
continues; the systemd unit declares SuccessExitStatus=0 1 so a bad stage can
never wedge boot. There is no keyboard attached to this machine.
Secrets never ride on removable media. The CIFS credentials file is created
empty by the autoinstall and filled in post-boot over the MCP. PLEX_CLAIM
tokens expire in four minutes and are passed as a one-shot environment variable,
never written to disk or committed.
One secrets file per host. This box has its own /srv/secrets/stacks.env,
using the same [OPERATOR] / [VALUES] / [MANIFEST] tiering as arrsstack, but
it is not shared or mounted from anywhere. Canonical copies live in Vaultwarden.
The seeder only ever adds. 10-secrets.sh will never overwrite an existing
key, change its value, or move it. If KEY= is present it is skipped entirely.
Verified against a pre-populated file: existing values, unrelated keys, the
operator tier and existing manifest lines all survive byte-for-byte, and a second
run is a no-op.
Notes that will save you an evening
shell-mcp was originally built on arm64. It builds clean on amd64 — the whole Python dependency tree resolves to prebuilt manylinux x86_64 wheels, so no compiler is needed. Two real changes were required:
- Dependencies are now pinned. The original installed
mcp starlette uvicornunpinned.starlettehas since gone 1.x. Unpinned installs are tolerable in a hand-run build and dangerous inside a first-boot script. - Port and description. It listens on 8103 (not 8085) so NPM proxy host 42 can simply be repointed, and the tool description says media box, not arrsstack — otherwise every session starts with the wrong idea of which host it is touching.
The GTX 1070 is Pascal, and NVIDIA branch 580 is the last one that supports it.
There will be no 590 for this card. 30-nvidia.sh installs 580 explicitly and pins
against newer branches. Never use ubuntu-drivers autoinstall here — it will
cheerfully install a branch that drops the card. And it must be the proprietary
module, not -open: the open kernel modules need Turing or newer.
Secure Boot must stay off. It is off today. A BIOS update resets ASUS defaults
and turns it back on, at which point the DKMS module needs interactive MOK
enrollment at a console this box does not have. 00-preflight.sh checks for this.
nofail on every CIFS mount is not optional. Without it, an unreachable NAS
drops a headless box to an emergency shell waiting for a root password on a
keyboard that is not plugged in.
autoEmptyTrash is the most dangerous default here. The NAS account is
read-write. If a mount is missing when Plex scans, Plex sees an empty library and
will act on that. 50-plex.sh disables it before any library is added.
Plex's PUID/PGID must equal the CIFS uid=/gid=. Both are 3000 (media).
If they ever drift, every file is permission-denied.