diff --git a/docs/superpowers/specs/2026-08-05-immich-nixos-module-design.md b/docs/superpowers/specs/2026-08-05-immich-nixos-module-design.md new file mode 100644 index 0000000..c8ed56c --- /dev/null +++ b/docs/superpowers/specs/2026-08-05-immich-nixos-module-design.md @@ -0,0 +1,119 @@ +# Immich: Docker → NixOS module migration + +**Date:** 2026-08-05 +**Machine:** jupiter (home server, Intel iGPU) +**Status:** Design approved, pending implementation plan + +## Goal + +Replace the existing docker-compose Immich deployment on jupiter with the +native `services.immich` NixOS module, wrapped in the repo's standard +`my.profiles.*` pattern. Preserve all existing data (albums, faces, shared +links, metadata) and photo/video library. + +## Decisions + +| Topic | Decision | +|-------|----------| +| Approach | Native `services.immich` (nixpkgs), not `oci-containers` | +| Version target | Stable nixpkgs ships **2.7.5**, unstable **3.0.3**. Pin target ≥ running docker version (forward-migration only) | +| Media location | Default local path `/var/lib/immich`. NAS deferred to a future read-only external library | +| Database | Migrate via dump/restore — keep everything | +| HW acceleration | Video transcoding only (VAAPI/QSV via existing Intel graphics stack). ML on CPU | +| Access | LAN + VPN only: open port 2283, register on homepage dashboard. No reverse proxy/TLS | + +### Deliberately deferred (YAGNI) +- OpenVINO ML acceleration +- NAS-backed external library +- Reverse proxy / TLS / public hostname + +## Part 1 — The module + +New file `modules/environments/immich/default.nix` following the profile +pattern; add `./environments/immich` to `modules/environments/default.nix`; +enable `my.profiles.immich.enable = true` in +`machines/jupiter/environments.nix`. + +```nix +{ config, lib, pkgs, ... }: +let + cfg = config.my.profiles.immich; + hostName = config.networking.hostName; + port = 2283; +in { + options.my.profiles.immich.enable = lib.mkEnableOption "Immich photo server"; + + config = lib.mkIf cfg.enable { + services.immich = { + enable = true; + # package = pkgs.unstable.immich; # only if docker :release is > 2.7.5 + host = "0.0.0.0"; + inherit port; + openFirewall = true; + mediaLocation = "/var/lib/immich"; + machine-learning.enable = true; + accelerationDevices = [ "/dev/dri/renderD128" ]; + settings.server.externalDomain = "http://${hostName}:${toString port}"; + }; + + # native module does not add GPU groups; needed for VAAPI/QSV transcoding + users.users.immich.extraGroups = [ "video" "render" ]; + + my.homepage.services = [{ + group = "Media"; + name = "Immich"; + description = "Photo & video server"; + href = "http://${hostName}:${toString port}"; + icon = "immich.png"; + }]; + }; +} +``` + +**Provided for free by the native module:** local PostgreSQL with the required +vector extension over a **unix socket + peer auth** (so no DB password / sops +secret needed), Redis, `immich-server` and `immich-machine-learning` systemd +units, the `immich` system user, and `mediaLocation` created via tmpfiles. + +**Transcoding is two parts:** (a) NixOS exposes the GPU device + `video`/`render` +groups (above); (b) the hwaccel backend (QSV/VAAPI) is chosen in Immich's +**admin → video transcoding** settings after cutover — a UI toggle, not Nix. + +## Part 2 — Migration runbook (on jupiter) + +### Pre-flight (hard blocker) +1. Get running docker Immich version (`docker exec immich --version` or web UI footer). +2. Compare to target (stable 2.7.5 / unstable 3.0.3). Target **must be ≥ running**. + - running ≤ 2.7.5 → stable module as-is + - 2.7.6–3.0.3 → set `package = pkgs.unstable.immich` + - `> 3.0.3` → bump nixpkgs first; **stop and re-plan** +3. Record docker `UPLOAD_LOCATION` and DB container name/credentials. + +### Backup (before touching anything) +4. `docker compose down` (DB may stay up for the dump). +5. Dump DB: `docker exec -t pg_dumpall --clean --if-exists --username=postgres > immich-db.sql` +6. Verify upload folder intact; note size (no copy yet). + +### Cutover +7. Add the module to jupiter's `environments.nix` (leave `database.createDB` default). +8. `sudo nixos-rebuild switch --flake '.#jupiter'` → creates user, empty DB + role, `mediaLocation`. Then `systemctl stop immich-server immich-machine-learning`. +9. Restore the DB into the NixOS Postgres (drop the freshly-created empty `immich` DB, load `immich-db.sql`) per Immich's restore docs. +10. Move media into `/var/lib/immich` (subfolders `library/`, `upload/`, `thumbs/`, `encoded-video/`, `profile/`); `chown -R immich:immich /var/lib/immich`. +11. `systemctl start immich-server`; it runs schema migrations forward. Watch `journalctl -u immich-server -f`. + +### Verify +12. UI at `http://jupiter:2283` loads; log in; spot-check albums, faces, a shared link, and that thumbnails/originals actually load. +13. Homepage tile works. +14. Enable QSV/VAAPI in admin settings; transcode one video; confirm `journalctl` shows the hw path, not a CPU fallback error. + +### Rollback +Before deleting any docker data: `systemctl stop immich-*`, disable the profile, +`nixos-rebuild switch`, `docker compose up -d`. Original docker DB + upload +folder remain untouched until explicitly removed after a few days of confidence. + +## Known risk + +Step 9 crosses the **pgvecto.rs → VectorChord** vector-extension boundary if the +docker version predates VectorChord. Follow Immich's official "migrate vector +database" guidance during restore. The exact case is known only after the +pre-flight version check.