Files
nixos/docs/superpowers/specs/2026-08-05-immich-nixos-module-design.md
T

5.2 KiB
Raw Blame History

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.

{ 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 <server> 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.63.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)

  1. docker compose down (DB may stay up for the dump).
  2. Dump DB: docker exec -t <db> pg_dumpall --clean --if-exists --username=postgres > immich-db.sql
  3. Verify upload folder intact; note size (no copy yet).

Cutover

  1. Add the module to jupiter's environments.nix (leave database.createDB default).
  2. sudo nixos-rebuild switch --flake '.#jupiter' → creates user, empty DB + role, mediaLocation. Then systemctl stop immich-server immich-machine-learning.
  3. Restore the DB into the NixOS Postgres (drop the freshly-created empty immich DB, load immich-db.sql) per Immich's restore docs.
  4. Move media into /var/lib/immich (subfolders library/, upload/, thumbs/, encoded-video/, profile/); chown -R immich:immich /var/lib/immich.
  5. systemctl start immich-server; it runs schema migrations forward. Watch journalctl -u immich-server -f.

Verify

  1. UI at http://jupiter:2283 loads; log in; spot-check albums, faces, a shared link, and that thumbnails/originals actually load.
  2. Homepage tile works.
  3. 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.