| 1 | # Personal service cutover |
| 2 | |
| 3 | The installed host retains the original encrypted app tree at `/mnt/storage1/apps`. Clover and its Media child are already mounted at `/srv/clover` and `/srv/clover/Media`. Copy each app's state into its managed production dataset, verify it, and only then start that app. Old app trees stay untouched. Personal Forgejo repository migration and PostgreSQL restoration are separate from these file imports. |
| 4 | |
| 5 | ```sh |
| 6 | export STUDIO_DEPLOY_HOST=root@10.0.0.1 |
| 7 | export STUDIO_DEPLOY_PORT=22 |
| 8 | export STUDIO_LEGACY_HANDOFF=/mnt/storage1/apps/studio-handoff/20261004T230507Z-e75419 |
| 9 | ``` |
| 10 | |
| 11 | Provision managed service datasets and identities before import. Register a stopped production Nomad job when required: render HCL, parse with `nomad job run -output`, set the returned `Job.Stop=true`, then POST `/v1/jobs` and confirm zero allocations. Do not deploy an empty app to create its dataset. A missing job is accepted by the Shale, qBittorrent, ARR, YouTube, and personal-files importers. Jellyfin, Navidrome, and Jackett importers require `Stop=true` on an existing job. |
| 12 | |
| 13 | | App | Retained source → managed destination | Import and pre-start proof | |
| 14 | | --- | --- | --- | |
| 15 | | Shale | `apps/shale/{data,repositories_owned,repositories_mirrors}` → `/srv/prod/shale/` | `bash tools/import-shale.sh shale`; three directory checksums and SQLite integrity. Forgejo-only repositories still need import into Shale. | |
| 16 | | Jellyfin | `apps/jellyfin/` → `/srv/prod/jellyfin/config/` | `sh tools/import-jellyfin.sh jellyfin`; excludes caches/logs/transcodes; verifies both SQLite databases and copy checksums. | |
| 17 | | Navidrome | `apps/navidrone/` → `/srv/prod/navidrome/data/` | `sh tools/import-navidrome.sh navidrome`; SQLite integrity, checksum, and retained `snow` administrator. The old directory is spelled `navidrone`. | |
| 18 | | qBittorrent | `apps/qbittorrent/qBittorrent/` → `/srv/prod/qbittorrent/config/qBittorrent/` | `bash tools/import-qbittorrent.sh`; verifies resume-file count and checksum, prepares config, **then starts the service**. Load PIA secrets first. | |
| 19 | | PDS | `apps/pds/` → `/srv/prod/pds/pds/` | `ssh root@10.0.0.1 'python3 - pds' < tools/import-personal-files.py`; preserve original JWT/admin/PLC/mail secrets first. Checks every SQLite database, including actor stores, and copies actor keys/blocks. Refuses WAL files requiring a consistent SQLite backup. | |
| 20 | | Sonarr / Radarr | `apps/<app>/` → `/srv/prod/<app>/config/` | `bash tools/import-arr.sh <app>`; consistent SQLite backup, integrity and hash; excludes logs/PIDs. Configure current internal endpoints after startup. | |
| 21 | | Jackett | `apps/jackett/` → `/srv/prod/jackett/config/Jackett/` | `sh tools/import-jackett.sh jackett`; retains API key and indexer JSON, verifies checksums. | |
| 22 | | YouTube state | `apps/yt-feed/` → `/srv/prod/ytdl/data/` | `bash tools/import-yt-feed.sh ytdl`; validates JSON and checksums; stops and restarts dashboard's worker around import. Import mail secrets before activating worker. | |
| 23 | | Clover DB | `/srv/clover/.zfs/snapshot/<snapshot>/Documents/Config/paper clover/` → `/srv/prod/clover-source-of-truth/data/` | `STUDIO_MIGRATION_SNAPSHOT=<snapshot> bash tools/import-source-data.sh`; direct host-local tar stream, tree digest and SQLite integrity. Import original API key. | |
| 24 | | Tailscale / DDNS / Copyparty / YouTube Archiver / PDS | Sources and mount destinations are defined in [import-personal-files.py](import-personal-files.py). | Run the host-side importer below once per app. No app starts; it checks stopped allocations, snapshots destination, copies, checksum-verifies, and assigns its UID. | |
| 25 | |
| 26 | ```sh |
| 27 | ssh root@10.0.0.1 'python3 - tailscale' < tools/import-personal-files.py |
| 28 | ssh root@10.0.0.1 'python3 - ddns-updater' < tools/import-personal-files.py |
| 29 | ssh root@10.0.0.1 'python3 - copyparty' < tools/import-personal-files.py |
| 30 | ssh root@10.0.0.1 'python3 - ytdl' < tools/import-personal-files.py |
| 31 | ``` |
| 32 | |
| 33 | Tailscale must retain `tailscaled.state` to preserve its node identity; the copy remains root-owned and mode `0600`. The PDS importer copies every actor store, not only its three root SQLite files; `import-pds.sh` excludes nested SQLite files and is unsuitable for this offline cutover. DDNS retains update history; its provider configuration comes from imported Cloudflare secrets and the service's generated `CONFIG`. Copyparty's active state is the **nested** old `copyparty/copyparty/` directory, including `shares.db`, sessions, IdP database, and salts; it stays nested under the new `/cfg/copyparty`. The [container sets `XDG_CONFIG_HOME=/cfg`](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker/Dockerfile.ac), and Copyparty appends its app directory. The outer legacy policy config is not copied: `prepare.py` generates the current policy. Its share database contained 17 shares and three selected-file records at the October 5 UTC inspection. Per-volume histories already on Clover/Media stay on those datasets. |
| 34 | |
| 35 | YouTube Archiver's 12 retained download-archive files stay beside videos in `Media/Videos/Independent`; cache and stale locks in its old app directory are disposable. Its retained subscriptions specify `/media/jellyfin/Independent`, so the service binds that container path to the reorganized folder. The upscaler reads `/media/Videos/Independent` directly. Dashboard's worker reads the actual `Indie Shows`, `Videos/Independent`, and `Intake - Music` folders; it needs no media symlinks. |
| 36 | |
| 37 | Samba serves existing Clover/Media directly and needs only its account secret. Intake likewise uses `/srv/clover/Documents/Intake` directly and needs the retained API key. VictoriaMetrics, VictoriaLogs, and VictoriaTraces had no legacy directories at their personal app paths; start new stores. OpenSpeedTest, FlareSolverr, and Forward Auth have no retained application file state. Keycloak and Dawarich require their personal PostgreSQL restores before activation; Dawarich's file-volume migration belongs with its database importer. |
| 38 | |
| 39 | Activate telemetry and restored PostgreSQL first, then restored Keycloak and Forward Auth. Start restored Shale and complete Git repository migration. After personal files/secrets are verified, enable Copyparty, Samba, PDS, Navidrome, Jellyfin, Intake, and Clover DB. Bring up Jackett and qBittorrent before Sonarr/Radarr. Start restored YouTube state, dashboard worker, and Archiver after checking final writable media paths. DDNS starts after wildcard routing is ready; preserved Tailscale state can start once its own copy passes. |
| 40 | |
| 41 | Do not reuse preview imports for production: those branches can start jobs, use old `storage1` snapshot names, or intentionally disable writers. qBittorrent's production importer starts its job at the end; YouTube's importer restarts dashboard and may resume retained queued work immediately. All other production file importers listed above leave the imported app stopped. |
| 42 | |
| 43 | October 5 UTC execution evidence is stored root-only under `/var/lib/studio/migrations/personal-cutover-20261004`; the original app snapshot is `globe/apps@personal-cutover-20261004`. The manual no-start qBittorrent copy retained 550 saved torrents, YouTube retained six valid JSON state files, Copyparty retained 17 shares, PDS retained all four SQLite databases, and pgAdmin passed SQLite integrity. Dawarich retained its storage file, empty watched-import directory, and 95,396-byte Redis dump. YouTube state needs explicit access ACL masks (`m::rwX`) and default masks (`d:m::rwx`) so dashboard and upscaler can both write. |
| 44 | |
| 45 | Before ARR activation, the copied databases received a separate `before-arr-path-cutover-*` snapshot. Sonarr translated two root folders and 89 series paths; Radarr translated one root folder and 56 movie paths from `/data/media/jellyfin/<folder>` to `/data/media/<folder>`. All other application table values matched their pre-change hashes. Seven download-path mappings per app are derived from qBittorrent and ARR volume definitions, including `torrent` → `Seedbox` and the saved Jellyfin aliases; [configure-arr.py](configure-arr.py) updates their host alongside the download client. [Servarr maps paths only for the matching download-client host](https://github.com/Servarr/Wiki/blob/master/sonarr/settings.md). All mapped root destinations passed service UID/GID 3000 read/search checks. Three Sonarr series directories were already absent in the pre-layout Media snapshot; the other 86 and all 56 Radarr movie directories resolve. Private proofs are `<app>-path-cutover.json`, `<app>-mapped-directory-proof.json`, and `sonarr-missing-directory-origin.json` in the evidence directory above. |