Migrate Jellyfin on lyra back to the native NixOS service #30

Closed
opened 2026-07-01 18:53:17 +01:00 by nimmo · 6 comments
Owner

Goal

Revisit the earlier Jellyfin migration on lyra and complete it as a native NixOS service without repeating the mount and state-handling mistakes that led to the Docker reversion.

Acceptance Checklist

  • Reconstruct the current Jellyfin Docker layout and the intended host paths.
  • Define the NAS-backed mounts declaratively before touching Jellyfin itself.
  • Move Jellyfin config, cache, and metadata into a NixOS-managed path.
  • Configure hardware acceleration explicitly for the RTX 4070 path that lyra actually uses.
  • Cut over the reverse-proxy target only after the native service is healthy.
  • Keep a short rollback window where the Docker stack can be brought back quickly.

Context

This host previously had a native Jellyfin attempt that was later reverted in favour of Docker. The issue is not whether NixOS can run Jellyfin; it can. The real risk is the network-backed storage and GPU wiring around it.

Guardrails

  • Do not start by rewriting the app config in Nix if the existing UI-managed state is still acceptable.
  • Do not treat the old reversion as proof that Jellyfin should stay on Docker forever; treat it as evidence that the mount design must be settled first.
  • Validate playback, library visibility, and at least one hardware-transcoded stream before decommissioning the container.
## Goal Revisit the earlier Jellyfin migration on `lyra` and complete it as a native NixOS service without repeating the mount and state-handling mistakes that led to the Docker reversion. ## Acceptance Checklist - Reconstruct the current Jellyfin Docker layout and the intended host paths. - Define the NAS-backed mounts declaratively before touching Jellyfin itself. - Move Jellyfin config, cache, and metadata into a NixOS-managed path. - Configure hardware acceleration explicitly for the RTX 4070 path that `lyra` actually uses. - Cut over the reverse-proxy target only after the native service is healthy. - Keep a short rollback window where the Docker stack can be brought back quickly. ## Context This host previously had a native Jellyfin attempt that was later reverted in favour of Docker. The issue is not whether NixOS can run Jellyfin; it can. The real risk is the network-backed storage and GPU wiring around it. ## Guardrails - Do not start by rewriting the app config in Nix if the existing UI-managed state is still acceptable. - Do not treat the old reversion as proof that Jellyfin should stay on Docker forever; treat it as evidence that the mount design must be settled first. - Validate playback, library visibility, and at least one hardware-transcoded stream before decommissioning the container.
nimmo changed title from placeholder to Migrate Jellyfin on lyra back to the native NixOS service 2026-07-01 18:54:59 +01:00
Author
Owner

Preparation update (2026-08-24)

  • Took a stopped, consistent copy of the shared LSIO config directory at /srv/stacks/jellyfin-migration-backups/2026-08-24-pre-native-nix/config.
  • Verified 8.2 GiB across 61,485 files. The copied main database SHA-256 matched the source and SQLite PRAGMA integrity_check returned ok.
  • The NAS-backed /metadata copy stalled before transferring any files. It remains authoritative on the NAS and the backup scope note marks the empty metadata directory as incomplete.
  • Confirmed Docker and pinned native Jellyfin are both 10.11.11.
  • Prepared modules/services/jellyfin.nix on local branch feat/issue-30-native-jellyfin-prep. It is imported on Lyra but disabled, preserves the UI-managed encoding.xml, includes an idempotent first-start importer, mounts /media read-only over NFS, retains /metadata over CIFS, configures NVENC device access, and registers state for Backrest and failure monitoring.
  • SWAG already targets host.docker.internal:8096, so the proxy target is ready for native cutover.
  • Full nix flake check passed, and an explicit enabled-config evaluation produced a valid Lyra system derivation.

Remaining cutover prerequisites:

  1. Create secrets/jellyfin.yaml using an admin/YubiKey SOPS identity with key lyra/cifs-credentials in mount.cifs username/password format.
  2. Set cifsSopsFile and enable the module only during the cutover window.
  3. Take a final stopped delta snapshot, stop Docker Jellyfin, run just test, and validate both mounts, library visibility, normal playback, and one NVENC transcode.
  4. Keep the Docker container stopped but intact for rollback until the native service has passed a soak period.
Preparation update (2026-08-24) - Took a stopped, consistent copy of the shared LSIO config directory at /srv/stacks/jellyfin-migration-backups/2026-08-24-pre-native-nix/config. - Verified 8.2 GiB across 61,485 files. The copied main database SHA-256 matched the source and SQLite PRAGMA integrity_check returned ok. - The NAS-backed /metadata copy stalled before transferring any files. It remains authoritative on the NAS and the backup scope note marks the empty metadata directory as incomplete. - Confirmed Docker and pinned native Jellyfin are both 10.11.11. - Prepared modules/services/jellyfin.nix on local branch feat/issue-30-native-jellyfin-prep. It is imported on Lyra but disabled, preserves the UI-managed encoding.xml, includes an idempotent first-start importer, mounts /media read-only over NFS, retains /metadata over CIFS, configures NVENC device access, and registers state for Backrest and failure monitoring. - SWAG already targets host.docker.internal:8096, so the proxy target is ready for native cutover. - Full nix flake check passed, and an explicit enabled-config evaluation produced a valid Lyra system derivation. Remaining cutover prerequisites: 1. Create secrets/jellyfin.yaml using an admin/YubiKey SOPS identity with key lyra/cifs-credentials in mount.cifs username/password format. 2. Set cifsSopsFile and enable the module only during the cutover window. 3. Take a final stopped delta snapshot, stop Docker Jellyfin, run just test, and validate both mounts, library visibility, normal playback, and one NVENC transcode. 4. Keep the Docker container stopped but intact for rollback until the native service has passed a soak period.
Author
Owner

Path migration preparation is now included in the staged native Jellyfin module. Inventory of the stopped 10.11 snapshot found 30,802 Docker-rooted values in dedicated columns and 7,077 occurrences embedded in BaseItems.Data across 7,075 rows. The one-time importer rewrites /config/data, /config/cache, /config/log, and remaining /config prefixes transactionally in the copied jellyfin.db, migrates copied XML/JSON/NFO text files, retains a pre-migration database copy, and runs before/after integrity plus zero-remaining-path checks. /media and /metadata mount paths remain stable; 49,473 /media item rows were deliberately unchanged. A disposable copy test reached zero old paths, preserved PRAGMA integrity_check=ok, and produced an identical checksum on a second pass. The evaluated importer passes bash syntax validation and nix flake check passes. The module remains disabled; no live database or Docker service was changed by this step.

Path migration preparation is now included in the staged native Jellyfin module. Inventory of the stopped 10.11 snapshot found 30,802 Docker-rooted values in dedicated columns and 7,077 occurrences embedded in BaseItems.Data across 7,075 rows. The one-time importer rewrites /config/data, /config/cache, /config/log, and remaining /config prefixes transactionally in the copied jellyfin.db, migrates copied XML/JSON/NFO text files, retains a pre-migration database copy, and runs before/after integrity plus zero-remaining-path checks. /media and /metadata mount paths remain stable; 49,473 /media item rows were deliberately unchanged. A disposable copy test reached zero old paths, preserved PRAGMA integrity_check=ok, and produced an identical checksum on a second pass. The evaluated importer passes bash syntax validation and nix flake check passes. The module remains disabled; no live database or Docker service was changed by this step.
Author
Owner

Cutover layout revised after the first test activation. Jellyfin now uses upstream NixOS defaults: data /var/lib/jellyfin, config /var/lib/jellyfin/config, cache /var/cache/jellyfin, and logs /var/lib/jellyfin/log. Lyra owns a shared read-only NFS mount at /mnt/media for Jellyfin and future native services. The separate /metadata CIFS mount and invalid sops-nix.service dependency were removed: the active Docker system.xml, database, and logs prove current metadata lives under /config/data/metadata, and the stopped snapshot contains 5.4 GiB / 54,125 metadata files that will import to /var/lib/jellyfin/metadata. The migration now rewrites anchored /config roots and /media -> /mnt/media across dedicated columns, BaseItems.Data, text config, and the prerolls.video provider value. Disposable tests reached zero legacy roots, preserved SQLite integrity, and were checksum-idempotent on a second pass; text rewrites were also byte-idempotent. nix flake check passes. No new activation has been run after this correction; /srv/services/jellyfin is no longer referenced. The encrypted Jellyfin CIFS secret is retained but unused pending user direction.

Cutover layout revised after the first test activation. Jellyfin now uses upstream NixOS defaults: data /var/lib/jellyfin, config /var/lib/jellyfin/config, cache /var/cache/jellyfin, and logs /var/lib/jellyfin/log. Lyra owns a shared read-only NFS mount at /mnt/media for Jellyfin and future native services. The separate /metadata CIFS mount and invalid sops-nix.service dependency were removed: the active Docker system.xml, database, and logs prove current metadata lives under /config/data/metadata, and the stopped snapshot contains 5.4 GiB / 54,125 metadata files that will import to /var/lib/jellyfin/metadata. The migration now rewrites anchored /config roots and /media -> /mnt/media across dedicated columns, BaseItems.Data, text config, and the prerolls.video provider value. Disposable tests reached zero legacy roots, preserved SQLite integrity, and were checksum-idempotent on a second pass; text rewrites were also byte-idempotent. nix flake check passes. No new activation has been run after this correction; /srv/services/jellyfin is no longer referenced. The encrypted Jellyfin CIFS secret is retained but unused pending user direction.
Author
Owner

First native just test stopped safely in jellyfin-state-import.service. The database migration itself completed: direct predicates against /var/lib/jellyfin/data/jellyfin.db found zero remaining Docker-rooted /config or /media values, and the pre-migration rollback database remains alongside it. The failure was a false-positive audit: literal double quotes inside a shell double-quoted SQL argument were removed by Bash, so the /media/ check also matched migrated /mnt/media/ JSON and reported 6,459 rows. The audit now constructs JSON quote boundaries with SQLite char(34). The importer is also resumable via a completion marker: a recognized partial import with its rollback database is revalidated and completed without copying state again; ownership is secured before validation and the marker is written only after database/text audits and integrity checks pass. nix flake check passes. Next step is rerunning just test on Lyra.

First native `just test` stopped safely in `jellyfin-state-import.service`. The database migration itself completed: direct predicates against `/var/lib/jellyfin/data/jellyfin.db` found zero remaining Docker-rooted `/config` or `/media` values, and the pre-migration rollback database remains alongside it. The failure was a false-positive audit: literal double quotes inside a shell double-quoted SQL argument were removed by Bash, so the `/media/` check also matched migrated `/mnt/media/` JSON and reported 6,459 rows. The audit now constructs JSON quote boundaries with SQLite `char(34)`. The importer is also resumable via a completion marker: a recognized partial import with its rollback database is revalidated and completed without copying state again; ownership is secured before validation and the marker is written only after database/text audits and integrity checks pass. `nix flake check` passes. Next step is rerunning `just test` on Lyra.
Author
Owner

Live validation is complete: Jellyfin started from the imported native state, read media from /mnt/media, and an observed Firefox Android playback session successfully used CUDA decoding, scale_cuda, and RTX 4070 hevc_nvenc encoding. The one-time migration machinery has now been removed from the working configuration: no importSource, database/text rewrite script, completion-marker logic, or jellyfin-state-import.service remains. The evaluated Lyra configuration confirms hasImporter = false; native Jellyfin still requires mnt-media.mount. The imported state and stopped source snapshot were left untouched. Full nix flake check passes.

Live validation is complete: Jellyfin started from the imported native state, read media from `/mnt/media`, and an observed Firefox Android playback session successfully used CUDA decoding, `scale_cuda`, and RTX 4070 `hevc_nvenc` encoding. The one-time migration machinery has now been removed from the working configuration: no `importSource`, database/text rewrite script, completion-marker logic, or `jellyfin-state-import.service` remains. The evaluated Lyra configuration confirms `hasImporter = false`; native Jellyfin still requires `mnt-media.mount`. The imported state and stopped source snapshot were left untouched. Full `nix flake check` passes.
Author
Owner

Before commit, the shared /mnt/media definition was extracted from hosts/lyra/default.nix into reusable modules/services/media-library.nix. The module exposes nixosConfig.mediaLibrary.{enable,mountPoint,source}, defaults to /mnt/media and 192.168.8.238:/volume1/media, preserves the read-only NFS 4.1 mount policy, and owns NFS filesystem support plus mountpoint creation. Lyra now imports and enables it; future hosts can do the same without duplicating NAS details. Evaluated Lyra config preserves the mount and Jellyfin still requires mnt-media.mount. Full nix flake check passes.

Before commit, the shared `/mnt/media` definition was extracted from `hosts/lyra/default.nix` into reusable `modules/services/media-library.nix`. The module exposes `nixosConfig.mediaLibrary.{enable,mountPoint,source}`, defaults to `/mnt/media` and `192.168.8.238:/volume1/media`, preserves the read-only NFS 4.1 mount policy, and owns NFS filesystem support plus mountpoint creation. Lyra now imports and enables it; future hosts can do the same without duplicating NAS details. Evaluated Lyra config preserves the mount and Jellyfin still requires `mnt-media.mount`. Full `nix flake check` passes.
nimmo closed this issue 2026-08-24 21:52:17 +01:00
Sign in to join this conversation.
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
nimmo/nixos-config#30
No description provided.