19 Commits

Author SHA1 Message Date
finn.markwitz 3ee2b1a9d8 feat(immich): declare QSV transcoding via config-file settings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016e2PKH5yN31h6JgHWCQb32
2026-08-07 11:02:50 +02:00
finn.markwitz e9a5781944 feat(jupiter): add native Immich profile module 2026-08-05 16:39:07 +02:00
finn.markwitz 1f8a34d0e8 docs: Immich NixOS module implementation plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016e2PKH5yN31h6JgHWCQb32
2026-08-05 16:35:29 +02:00
finn.markwitz 1176132627 docs: resolve Immich version check (2.7.5 == stable, no vector migration)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016e2PKH5yN31h6JgHWCQb32
2026-08-05 16:33:53 +02:00
finn.markwitz 01b31a3493 docs: Immich docker→NixOS module migration design
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016e2PKH5yN31h6JgHWCQb32
2026-08-05 16:32:46 +02:00
finn.markwitz 28fc71dbbe Merge pull request 'feat: Athena Firewall' (#9) from athena-introduction into main
Reviewed-on: #9
2026-08-05 16:16:31 +02:00
finn.markwitz c7392f2dd9 feat: Athena Firewall 2026-08-05 16:15:49 +02:00
finn.markwitz f5f8e88966 Merge pull request 'revert(mibook): remove boot-time terminal/desktop choice feature' (#7) from revert/mibook-boot-choice into main
Reviewed-on: #7
2026-07-28 15:21:01 +02:00
finn.markwitz bf874d962a revert(mibook): remove boot-time terminal/desktop choice feature
Back out the entire "choose terminal-only vs KDE at boot" work. The first
attempt (default multi-user.target + on-demand desktop) locked mibook out —
it is WiFi-only with credentials in KWallet, so a headless boot has no
network and no SSH. The follow-up specialisation redesign is no longer
wanted either.

mibook returns to plain always-KDE boot (the pre-feature baseline):
- machines/mibook/configuration.nix: drop specialisation.terminal and the
  NetworkManager-wait-online override.
- machines/mibook/environments.nix: drop kde-desktop.startOnBoot.
- modules/environments/kde-desktop/default.nix: drop the startOnBoot option.
- remove the boot-desktop-choice design spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1wvPy5kbGp8pUQX61WBCY
2026-07-28 14:51:31 +02:00
finn.markwitz f01b1f2c4f Merge pull request 'Feat/jellyfin hw transcoding' (#3) from feat/jellyfin-hw-transcoding into main
Reviewed-on: #3
2026-07-27 09:56:11 +02:00
finn.markwitz ff34fe762e Merge pull request 'Feat/mibook boot desktop choice' (#4) from feat/mibook-boot-desktop-choice into main
Reviewed-on: #4
2026-07-27 09:55:47 +02:00
finn.markwitz 7e4407a1f8 feat(mibook): boot to terminal, launch KDE on demand
Add my.profiles.kde-desktop.startOnBoot (default true, no change for
existing consumers). When false, boot to multi-user.target, keep SDDM
out of any boot target's wants, and provide a 'desktop' command that
starts the display manager on demand. Enable this on mibook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CfozKLQdUh5TzqyjSigLUx
2026-07-27 09:48:13 +02:00
finn.markwitz 539fb26791 docs: spec for mibook boot-time terminal/desktop choice
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CfozKLQdUh5TzqyjSigLUx
2026-07-27 09:27:37 +02:00
marthsincemelee 003a2f77dd docs: record post-deploy QSV/OpenCL runtime fixes in spec
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:42:41 +02:00
marthsincemelee 86e7f9c1a8 fix(jupiter): add Intel compute-runtime for OpenCL HDR tone-mapping
HDR HEVC transcodes were failing with "Failed to get number of OpenCL
platforms: -1001" (CL_PLATFORM_NOT_FOUND_KHR). The tonemap_opencl
filter jellyfin-ffmpeg uses for HDR-to-SDR tone-mapping needs an OpenCL
ICD, which intel-media-driver/vpl-gpu-rt don't provide on their own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:34:36 +02:00
marthsincemelee adb7fcfad7 fix(jupiter): add oneVPL/MFX runtime for QSV hardware transcoding
HEVC HDR transcodes were failing with "Error creating a MFX session: -9"
because intel-media-driver only provides VAAPI, not the separate
oneVPL/MFX runtime QSV needs to create a hardware session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:27:31 +02:00
marthsincemelee 5235f5abcb feat(jellyfin): grant iGPU access for Quick Sync hardware transcoding 2026-07-26 14:51:01 +02:00
marthsincemelee f53f2331d0 docs: add implementation plan for Jellyfin hardware transcoding
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:31:20 +02:00
marthsincemelee 84cec9e935 docs: add design spec for Jellyfin hardware transcoding on jupiter
Jupiter's Intel iGPU is configured at the OS level but Jellyfin has no
access to it, so transcodes run on CPU only and stutter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:27:30 +02:00
19 changed files with 803 additions and 827 deletions
@@ -1,289 +0,0 @@
# DankMaterialShell (niri) Profile Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a `my.profiles.dank` NixOS profile that runs DankMaterialShell (DMS) on the niri Wayland compositor as a login session parallel to KDE on mibook, backed by a new opt-in `my.profiles.home-manager` module.
**Architecture:** Three flake inputs (`home-manager`, `niri` = niri-flake, `dank` = DankMaterialShell) are added to `flake.nix`. A `my.profiles.home-manager` module unconditionally imports the home-manager NixOS module and, when enabled, sets global HM settings. A `my.profiles.dank` module enables `my.profiles.home-manager` as a dependency, turns on niri (system session) + the DMS nixosModule (daemons/packages), and configures `home-manager.users.finn` with the DMS + niri-flake HM modules for keybinds/spawn. mibook enables `dank`.
**Tech Stack:** Nix flakes, flake-parts, NixOS modules, home-manager (as NixOS module), niri-flake, DankMaterialShell flake, quickshell (from `pkgs.unstable`).
## Global Constraints
- Do **not** modify `machines/configuration.nix`. `inputs` and `self` are already threaded into every module via `_module.args`; consume them as module arguments.
- **Top-level `imports` must reference flake modules via `self.inputs.*`, not `inputs.*`.** `self` is a specialArg (available during `imports` resolution); `inputs` comes from `_module.args` (config-derived) and using it in an `imports` list causes infinite recursion. Inside the `config` body (including nested `home-manager.users.<name>.imports`), either `inputs` or `self.inputs` is fine.
- Base channel is pinned `nixpkgs/nixos-26.05`; `pkgs.unstable` overlay is available everywhere. quickshell must come from `pkgs.unstable.quickshell` (needs ≥ 0.3.0).
- home-manager scoped to user `finn` only; `home.stateVersion = "26.05"`.
- Follow the existing profile pattern exactly: `let cfg = config.my.profiles.<name>; in { options.my.profiles.<name>.enable = lib.mkEnableOption "..."; config = lib.mkIf cfg.enable { ... }; }`.
- Format every new/edited `.nix` file with `nixfmt-rfc-style` before committing.
- Commit messages end with the repo's trailer lines (Co-Authored-By + Claude-Session) as seen in recent history.
- KDE (`my.profiles.kde-desktop`) stays enabled on mibook; do not remove it.
---
### Task 1: Add flake inputs
**Files:**
- Modify: `flake.nix:4-15` (the `inputs = { ... }` block)
**Interfaces:**
- Produces: flake inputs `inputs.home-manager`, `inputs.niri`, `inputs.dank`, reachable from any NixOS module (via the existing `_module.args.inputs = self.inputs`). Later tasks consume `inputs.home-manager.nixosModules.home-manager`, `inputs.niri.homeModules.niri`, `inputs.dank.nixosModules.dank-material-shell`, `inputs.dank.homeModules.dank-material-shell`, `inputs.dank.homeModules.niri`.
- [ ] **Step 1: Add the three inputs**
In `flake.nix`, inside the `inputs = { ... }` block, after the `nixos-generators` entry and before the closing `};`, add:
```nix
home-manager = {
url = "github:nix-community/home-manager/release-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
niri.url = "github:sodiboo/niri-flake";
dank.url = "github:AvengeMedia/DankMaterialShell";
```
- [ ] **Step 2: Resolve the lock file**
Run: `nix flake lock`
Expected: completes without error; `flake.lock` gains `home-manager`, `niri`, `niri-flake`-transitive, and `dank` (DankMaterialShell) nodes. If it reports a bad URL, re-check the `github:owner/repo` strings.
- [ ] **Step 3: Verify inputs expose the expected outputs**
Run: `nix eval --raw '.#nixosConfigurations.mibook.pkgs.system'` first to confirm the flake still evaluates (expected: `x86_64-linux`).
Then run: `nix flake show 'github:AvengeMedia/DankMaterialShell' --allow-import-from-derivation 2>/dev/null | grep -E 'dank-material-shell|nixosModules|homeModules'`
Expected: output lists `nixosModules.dank-material-shell` and `homeModules.dank-material-shell` and `homeModules.niri`. If the niri output name differs (e.g. `dankMaterialShell.niri`), note the actual name — Task 3 Step 1 must use whatever this shows.
- [ ] **Step 4: Format and commit**
```bash
nixfmt-rfc-style flake.nix
git add flake.nix flake.lock
git commit -m "chore: add home-manager, niri-flake, and DankMaterialShell flake inputs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sv2QAunLcLo3tS1YsfLgN"
```
---
### Task 2: `my.profiles.home-manager` module
**Files:**
- Create: `modules/environments/home-manager/default.nix`
- Modify: `modules/environments/default.nix` (add `./home-manager` to `imports`)
**Interfaces:**
- Consumes: `inputs.home-manager.nixosModules.home-manager` (Task 1).
- Produces: option `my.profiles.home-manager.enable`. When enabled, sets `home-manager.useGlobalPkgs = true`, `home-manager.useUserPackages = true`, `home-manager.extraSpecialArgs = { inherit inputs self; }`. Also makes the `home-manager.users.<name>` option available (from the unconditional import) so Task 3 can populate `home-manager.users.finn`.
- [ ] **Step 1: Create the module**
Create `modules/environments/home-manager/default.nix`:
```nix
{
config,
lib,
inputs,
self,
...
}:
let
cfg = config.my.profiles.home-manager;
in
{
imports = [ self.inputs.home-manager.nixosModules.home-manager ];
options.my.profiles.home-manager.enable =
lib.mkEnableOption "home-manager integration";
config = lib.mkIf cfg.enable {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.extraSpecialArgs = { inherit inputs self; };
};
}
```
- [ ] **Step 2: Register the module**
In `modules/environments/default.nix`, add `./home-manager` to the `imports` list (place it after `./hyprland`):
```nix
./hyprland
./home-manager
```
- [ ] **Step 3: Verify it evaluates disabled (no behavior change)**
Run: `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel' --dry-run`
Expected: evaluates and shows a build plan with no errors. Because `my.profiles.home-manager.enable` defaults to false, importing the HM module must not change the build outcome.
- [ ] **Step 4: Verify the option exists and defaults false**
Run: `nix eval '.#nixosConfigurations.mibook.config.my.profiles.home-manager.enable'`
Expected: `false`
- [ ] **Step 5: Format and commit**
```bash
nixfmt-rfc-style modules/environments/home-manager/default.nix modules/environments/default.nix
git add modules/environments/home-manager/default.nix modules/environments/default.nix
git commit -m "feat(home-manager): add opt-in my.profiles.home-manager module
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sv2QAunLcLo3tS1YsfLgN"
```
---
### Task 3: `my.profiles.dank` module
**Files:**
- Create: `modules/environments/dank/default.nix`
- Modify: `modules/environments/default.nix` (add `./dank` to `imports`)
**Interfaces:**
- Consumes: `my.profiles.home-manager.enable` (Task 2); `inputs.dank.nixosModules.dank-material-shell`, `inputs.dank.homeModules.dank-material-shell`, `inputs.dank.homeModules.niri`, `inputs.niri.homeModules.niri` (Task 1); `pkgs.unstable.quickshell`.
- Produces: option `my.profiles.dank.enable`.
- [ ] **Step 1: Create the module**
Create `modules/environments/dank/default.nix`. Use the exact `homeModules`/`nixosModules` output names confirmed in Task 1 Step 3 — the block below assumes `dank-material-shell` and `niri`:
```nix
{
config,
lib,
pkgs,
inputs,
self,
...
}:
let
cfg = config.my.profiles.dank;
in
{
imports = [ self.inputs.dank.nixosModules.dank-material-shell ];
options.my.profiles.dank.enable = lib.mkEnableOption "DankMaterialShell on niri";
config = lib.mkIf cfg.enable {
# Dependency: declarative user config comes from home-manager.
my.profiles.home-manager.enable = true;
# System side: niri Wayland session + DMS daemons/packages.
programs.niri.enable = true;
programs.dank-material-shell = {
enable = true;
systemd.enable = true;
quickshell.package = pkgs.unstable.quickshell;
};
# Home side: DMS shell + niri spawn for finn (keybinds come via includes).
home-manager.users.finn = {
imports = [
inputs.niri.homeModules.niri
inputs.dank.homeModules.dank-material-shell
inputs.dank.homeModules.niri
];
programs.dank-material-shell = {
enable = true;
systemd.enable = true;
quickshell.package = pkgs.unstable.quickshell;
# niri.enableKeybinds intentionally omitted: DMS's niri `includes.enable`
# (default true) already brings in the generated binds.kdl plus theming;
# setting enableKeybinds too triggers an upstream "not recommended" warning.
niri.enableSpawn = true;
};
home.stateVersion = "26.05";
};
};
}
```
- [ ] **Step 2: Register the module**
In `modules/environments/default.nix`, add `./dank` to the `imports` list (after `./home-manager`):
```nix
./home-manager
./dank
```
- [ ] **Step 3: Verify it evaluates disabled**
Run: `nix eval '.#nixosConfigurations.mibook.config.my.profiles.dank.enable'`
Expected: `false`
Run: `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel' --dry-run`
Expected: evaluates without error (dank disabled → no behavior change yet).
- [ ] **Step 4: Format and commit**
```bash
nixfmt-rfc-style modules/environments/dank/default.nix modules/environments/default.nix
git add modules/environments/dank/default.nix modules/environments/default.nix
git commit -m "feat(dank): add DankMaterialShell (niri) profile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sv2QAunLcLo3tS1YsfLgN"
```
---
### Task 4: Enable on mibook and build the full toplevel
**Files:**
- Modify: `machines/mibook/environments.nix:7-17` (the `my.profiles = { ... }` block)
**Interfaces:**
- Consumes: `my.profiles.dank.enable` (Task 3).
- [ ] **Step 1: Enable the profile**
In `machines/mibook/environments.nix`, inside the `my.profiles = { ... }` block, add:
```nix
dank.enable = true;
```
- [ ] **Step 2: Build the full toplevel (the real test)**
Run: `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'`
Expected: builds successfully. This exercises the DMS package, quickshell from unstable, niri, and the home-manager generation for finn.
If it fails on a **quickshell/Qt6 version mismatch** (see spec risks): drop `quickshell.package = pkgs.unstable.quickshell;` from **both** the system and HM `programs.dank-material-shell` blocks in `modules/environments/dank/default.nix` and rebuild, letting DMS's module pick its own default quickshell. If it fails on an **unknown HM option** (e.g. `niri.enableKeybinds`), run `nix eval '.#nixosConfigurations.mibook.config.home-manager.users.finn.programs.dank-material-shell' --apply builtins.attrNames 2>&1 | head` to list the real option names and adjust. Re-run the build until it passes.
- [ ] **Step 3: Run flake check**
Run: `nix flake check`
Expected: passes (all nixosConfigurations evaluate). If `nix flake check` is slow or pulls DMS's own checks, `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'` from Step 2 passing is the authoritative gate.
- [ ] **Step 4: Format and commit**
```bash
nixfmt-rfc-style machines/mibook/environments.nix
git add machines/mibook/environments.nix modules/environments/dank/default.nix
git commit -m "feat(mibook): enable DankMaterialShell (niri) session
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sv2QAunLcLo3tS1YsfLgN"
```
- [ ] **Step 5: Manual acceptance (human, on hardware)**
Run: `sudo nixos-rebuild test --flake '.#mibook'`
Then log out, pick the **niri** session at SDDM, log in as finn.
Expected: DMS bar/shell appears; `Mod+Space` opens the launcher; `Mod+V` opens the clipboard; the KDE session is still selectable and works. Note this step cannot be automated — it requires the physical machine.
---
## Notes for the implementer
- **Order matters:** Task 1 must land first (inputs must resolve before any module can import them). Tasks 2→3→4 are strictly sequential.
- **Output-name drift is the top risk.** The DMS flake has renamed outputs over time (there are deprecated aliases like `dankMaterialShell.niri`). Task 1 Step 3 pins down the real names against the locked revision; use those, not the names in this plan, if they differ.
- **No unit-test framework here.** Nix evaluation + a successful `toplevel` build *is* the test. Do not fabricate a test harness.
- Do not touch `machines/configuration.nix`, `machines/jupiter/*`, or the KDE profile.
@@ -0,0 +1,154 @@
# Jellyfin Hardware Transcoding (jupiter) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Give jupiter's Jellyfin service access to the Intel iGPU's VAAPI render node so Quick Sync hardware transcoding can be enabled, instead of every transcode falling back to CPU.
**Architecture:** One NixOS module change (`modules/environments/jellyfin/default.nix`) grants the `jellyfin` systemd service supplementary access to the `video`/`render` groups and installs `libva-utils` for verification. This is declarative and build-verifiable from the Mac. Enabling Quick Sync inside Jellyfin's own dashboard, and the on-machine verification, is a manual step run by the user on jupiter after deploy — the NixOS module has no option for it and this environment's convention is that the assistant never SSHes into jupiter directly (see `docs/superpowers/specs/2026-07-26-jellyfin-hw-transcoding.md`).
**Tech Stack:** NixOS (flake-parts), nixpkgs `services.jellyfin` module, VAAPI/`intel-media-driver`, `libva-utils`.
## Global Constraints
- No SSH from the assistant into jupiter — all on-machine commands are given to the user to run and paste back.
- Follow the existing profile pattern in `modules/environments/jellyfin/default.nix` (`config = lib.mkIf cfg.enable { ... }`); don't introduce a new toggle option — hardcode the hardware-acceleration wiring on, per the approved spec.
- Verify locally via `nix eval` / `nix build` before asking the user to deploy.
---
### Task 1: Grant Jellyfin access to the iGPU and verify the build
**Files:**
- Modify: `modules/environments/jellyfin/default.nix`
**Interfaces:**
- Produces: `systemd.services.jellyfin.serviceConfig.SupplementaryGroups = [ "video" "render" ];` — verified via `nix eval` in Step 2.
- [ ] **Step 1: Add the device-access config and `libva-utils` package**
Read the current file first (`modules/environments/jellyfin/default.nix`), then edit the `config = lib.mkIf cfg.enable { ... }` block so it reads:
```nix
config = lib.mkIf cfg.enable {
services.jellyfin = {
enable = true;
openFirewall = true;
};
environment.systemPackages = [ pkgs.libva-utils ];
my.homepage.services = [
{
group = "Media";
name = "Jellyfin";
description = "Media server";
href = "http://${hostName}:${toString port}";
icon = "jellyfin.png";
}
];
systemd.services.jellyfin = {
after = [ "network-online.target" ];
serviceConfig.SupplementaryGroups = [
"video"
"render"
];
};
};
```
Note the two existing `systemd.services.jellyfin` keys (`after`) and the new `serviceConfig.SupplementaryGroups` now live in the same attrset — don't create a second `systemd.services.jellyfin = { ... }` block, it would overwrite the first.
- [ ] **Step 2: Verify the rendered config with `nix eval`**
Run (from the repo root on the Mac):
```bash
nix eval '.#nixosConfigurations.jupiter.config.systemd.services.jellyfin.serviceConfig.SupplementaryGroups' \
--extra-experimental-features 'nix-command flakes'
```
Expected output: `[ "video" "render" ]`
- [ ] **Step 3: Verify the machine still builds**
Run:
```bash
nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel' \
--extra-experimental-features 'nix-command flakes' --no-link
```
Expected: build succeeds with no errors (may take a while; watch for any evaluation error mentioning `jellyfin` or `libva-utils`).
- [ ] **Step 4: Format and commit**
```bash
nixfmt-rfc-style modules/environments/jellyfin/default.nix
git add modules/environments/jellyfin/default.nix
git commit -m "feat(jellyfin): grant iGPU access for Quick Sync hardware transcoding"
```
---
### Task 2: Deploy on jupiter and enable Quick Sync (user-executed)
**Files:** none (on-machine deploy + Jellyfin dashboard UI)
**Interfaces:**
- Consumes: the `SupplementaryGroups` change from Task 1, already merged into the flake.
These steps run **on jupiter**, by the user — paste the output back so we can confirm each one before moving to the next.
- [ ] **Step 1: Deploy**
```bash
sudo nixos-rebuild switch --flake '.#jupiter'
```
Expected: switch succeeds, no errors mentioning `jellyfin`.
- [ ] **Step 2: Confirm the service picked up the new groups**
```bash
systemctl show jellyfin -p SupplementaryGroups
systemctl status jellyfin --no-pager
```
Expected: `SupplementaryGroups=video render` (order may vary) and the service is `active (running)`.
- [ ] **Step 3: Confirm VAAPI driver loads**
```bash
vainfo
```
Expected: output starts with something like `vainfo: VA-API version: 1.x` and `Driver version: Intel iHD driver`, followed by a list of supported VAProfiles/VAEntrypoints (e.g. `VAProfileH264Main : VAEntrypointVLD`, `VAEntrypointEncSlice`).
If this instead prints a permissions or "no VA display" error, paste it back — that means the group grant isn't reaching the process and Task 1 needs a follow-up fix (e.g. the jellyfin service may be more sandboxed than expected, requiring an explicit `DeviceAllow=char-drm rw` in `serviceConfig` as well).
- [ ] **Step 4: Enable Quick Sync in the Jellyfin dashboard**
In the Jellyfin web UI:
1. **Dashboard → Playback**.
2. Hardware acceleration: **Intel QuickSync (QSV)**.
3. VA-API device: `/dev/dri/renderD128`.
4. Enable hardware decoding for the codecs your library uses (H264 at minimum).
5. If the library has HDR content, enable tone-mapping.
6. Save.
- [ ] **Step 5: Functional check**
Play a file that requires transcoding (or force a lower quality in the client's playback settings to trigger one), then:
```bash
journalctl -u jellyfin -n 50 --no-pager
```
Look for a line referencing `qsv` or `vaapi` in the transcode command. Separately, watch CPU usage (`htop`) during playback — it should stay low on the core doing the transcode, rather than pegging at 100%, since the iGPU is now doing the encode/decode work.
## Self-Review Notes
- Spec coverage: NixOS change (Task 1) ✓, manual dashboard step (Task 2 Step 4) ✓, verification via `vainfo`/build (Task 1 Step 2-3, Task 2 Step 3) ✓, functional check (Task 2 Step 5) ✓. Toggle option explicitly excluded per approved spec — not present, correctly.
- No placeholders — every step has literal commands/code.
- `SupplementaryGroups` key/value matches exactly between Task 1 (produced) and Task 2 (consumed/checked).
@@ -0,0 +1,299 @@
# Immich NixOS Module Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
>
> **Note on nature:** Task 1 is repo work verifiable with `nix build` (no runtime tests exist for declarative config). Tasks 26 are a **manual migration runbook executed on jupiter by the operator** — they are destructive and cannot be run from the dev machine (mibook). Do not attempt to automate or execute Tasks 26 from an agent session; present them for the operator to run and confirm.
**Goal:** Replace jupiter's docker-compose Immich with the native `services.immich` NixOS module, preserving all data (albums, faces, shares, library).
**Architecture:** A standard `my.profiles.immich` module wraps `services.immich` (native Postgres+VectorChord over unix socket, Redis, server, machine-learning). Media stays at the default local `/var/lib/immich`. The existing docker Postgres dump is restored same-version (2.7.5 → 2.7.5, no schema/vector migration). GPU is exposed for VAAPI/QSV transcoding.
**Tech Stack:** NixOS (flake-parts), `services.immich` from nixpkgs 25.11, PostgreSQL, Intel QSV/VAAPI, docker (source only).
## Global Constraints
- Machine: **jupiter** only. Do not enable on mibook.
- Immich version: source docker == target nixpkgs == **2.7.5** (stable). No `package` override. Do NOT bump nixpkgs Immich during this work.
- Media location: default `/var/lib/immich` (local disk). Do not point at the NAS.
- Database: local PostgreSQL over **unix socket + peer auth** — no password, no sops secret.
- HW accel: **video transcoding only**. ML stays on CPU (`machine-learning.enable = true`, no OpenVINO).
- Access: LAN + VPN, `openFirewall = true`, port **2283**. No reverse proxy/TLS.
- Rebuild command: `sudo nixos-rebuild switch --flake '.#jupiter'`.
- Build-check command: `nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'`.
- Format Nix with `nixfmt-rfc-style` before committing.
- Do not delete docker DB or upload data until Task 6 sign-off.
---
## File Structure
- **Create** `modules/environments/immich/default.nix` — the `my.profiles.immich` module (single responsibility: declare Immich).
- **Modify** `modules/environments/default.nix` — add `./environments/immich` to the import list.
- **Modify** `machines/jupiter/environments.nix` — set `immich.enable = true`.
No other files change. The DB/media migration touches only runtime state on jupiter, not the repo.
---
### Task 1: Author the `immich` profile module
**Files:**
- Create: `modules/environments/immich/default.nix`
- Modify: `modules/environments/default.nix` (import list)
- Modify: `machines/jupiter/environments.nix` (`my.profiles.immich.enable`)
**Interfaces:**
- Produces: NixOS option `my.profiles.immich.enable` (bool). When true, configures `services.immich`, adds `immich` user to `video`/`render` groups, and appends an entry to `my.homepage.services`.
- Consumes: existing `my.homepage.services` aggregator; `config.networking.hostName`.
- [ ] **Step 1: Read a reference module to match repo style**
Read `modules/environments/jellyfin/default.nix` (same shape: `cfg`, `hostName`, `port`, `mkIf`, `my.homepage.services`). Match its formatting and header-comment convention.
- [ ] **Step 2: Create the module file**
Create `modules/environments/immich/default.nix`:
```nix
# Immich self-hosted photo & video server
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.immich;
hostName = config.networking.hostName;
port = 2283;
in
{
options.my.profiles.immich = with lib; {
enable = mkEnableOption "Immich photo server";
};
config = lib.mkIf cfg.enable {
services.immich = {
enable = true;
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}";
};
# The native module does not add GPU groups; required 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";
}
];
};
}
```
- [ ] **Step 3: Register the module in the environments import list**
Open `modules/environments/default.nix` and add `./environments/immich` (or `./immich`, matching the exact relative style already used in that file — check how `jellyfin` is listed and mirror it).
- [ ] **Step 4: Enable it on jupiter**
In `machines/jupiter/environments.nix`, inside the `my.profiles = { ... }` block, add:
```nix
immich.enable = true;
```
- [ ] **Step 5: Format**
Run: `nixfmt-rfc-style modules/environments/immich/default.nix`
- [ ] **Step 6: Build-check (this is the "test")**
Run: `nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'`
Expected: builds successfully. If it fails on an unknown option (e.g. `accelerationDevices`, `settings.server.externalDomain`), reconcile against the module at `$(nix eval --raw '.#nixosConfigurations.jupiter.pkgs.path')/nixos/modules/services/web-apps/immich.nix` and fix.
- [ ] **Step 7: Confirm the option evaluates on**
Run: `nix eval '.#nixosConfigurations.jupiter.config.services.immich.enable'`
Expected: `true`
- [ ] **Step 8: Commit**
```bash
git add modules/environments/immich/default.nix modules/environments/default.nix machines/jupiter/environments.nix
git commit -m "feat(jupiter): add native Immich profile module"
```
---
### Task 2: Pre-flight & backup on jupiter (operator-run)
**Files:** none (runtime state on jupiter). Run all commands on jupiter.
**Interfaces:**
- Produces: `immich-db.sql` dump file and a known-good copy/snapshot of the docker upload folder; recorded `UPLOAD_LOCATION` path and DB container name.
- [ ] **Step 1: Record docker facts**
From the docker-compose dir on jupiter, note `UPLOAD_LOCATION`, the DB service/container name, and `POSTGRES_USER`/`POSTGRES_DB` from `.env`/compose. Confirm server version is **2.7.5** (web UI footer or `docker exec <server> immich --version`). If it is not 2.7.5, STOP — this plan assumes a same-version restore.
- [ ] **Step 2: Stop the docker stack (DB may stay up for the dump)**
Run: `docker compose stop immich-server immich-machine-learning` (leave the DB container running).
- [ ] **Step 3: Dump the database**
Run: `docker exec -t <db-container> pg_dumpall --clean --if-exists --username=<POSTGRES_USER> > ~/immich-db.sql`
Expected: a non-trivial `immich-db.sql` (check it is not near-empty: `wc -l ~/immich-db.sql`).
- [ ] **Step 4: Stop the DB and record the media size**
Run: `docker compose down` then `du -sh <UPLOAD_LOCATION>` and note the size. Do NOT copy yet. Do NOT delete anything.
---
### Task 3: First switch — let the module create empty state (operator-run)
**Files:** none at runtime (repo change already committed in Task 1). Run on jupiter after pulling the committed branch.
**Interfaces:**
- Consumes: `immich-db.sql`, `UPLOAD_LOCATION` from Task 2.
- Produces: an `immich` system user, an empty `immich` Postgres DB + role, and `/var/lib/immich` created with correct ownership, with services then stopped.
- [ ] **Step 1: Deploy the config**
On jupiter, check out the branch containing Task 1's commit and run:
`sudo nixos-rebuild switch --flake '.#jupiter'`
Expected: `immich-server`, `immich-machine-learning`, postgres, and redis units come up; UI reachable at `http://jupiter:2283` showing a fresh/empty instance.
- [ ] **Step 2: Stop immich so data can be swapped underneath**
Run: `sudo systemctl stop immich-server immich-machine-learning`
Expected: both inactive. PostgreSQL and Redis stay running.
- [ ] **Step 3: Verify the DB and user exist**
Run: `sudo -u postgres psql -c '\l' | grep immich` and `sudo -u postgres psql -c '\du' | grep immich`
Expected: an `immich` database and `immich` role are present.
---
### Task 4: Restore database and media (operator-run, destructive)
**Files:** none in repo. Run on jupiter. This overwrites the freshly-created empty DB.
**Interfaces:**
- Consumes: `immich-db.sql`, `<UPLOAD_LOCATION>`, the running NixOS PostgreSQL.
- Produces: the migrated DB and populated `/var/lib/immich`.
- [ ] **Step 1: Restore the dump into the NixOS Postgres**
`pg_dumpall` output includes role/DB creation. Load it as the `postgres` superuser over the unix socket:
Run: `sudo -u postgres psql -f ~/immich-db.sql`
Expected: completes without fatal errors. Harmless "role already exists"/"database already exists" notices are OK because of `--clean --if-exists`. If the immich DB ends up owned by the wrong role, reassign: `sudo -u postgres psql -c 'ALTER DATABASE immich OWNER TO immich;'`.
- [ ] **Step 2: Sanity-check the restored data**
Run: `sudo -u postgres psql -d immich -c 'SELECT count(*) FROM assets;'`
Expected: a count matching your library size (non-zero). If the table name differs by version, list tables with `\dt` and check an obviously-populated one.
- [ ] **Step 3: Move the media into the default location**
Immich's upload folder holds subdirs `library/ upload/ thumbs/ encoded-video/ profile/ backups/`. Move (not copy, if same filesystem) the contents of `<UPLOAD_LOCATION>` into `/var/lib/immich`:
Run: `sudo rsync -aHAX --info=progress2 <UPLOAD_LOCATION>/ /var/lib/immich/`
(Use `rsync` — safe if partially interrupted. Keep the source until Task 6 sign-off.)
- [ ] **Step 4: Fix ownership**
Run: `sudo chown -R immich:immich /var/lib/immich`
Expected: everything under `/var/lib/immich` owned by `immich`.
---
### Task 5: Start and verify (operator-run)
**Files:** none. Run on jupiter.
**Interfaces:**
- Consumes: migrated DB + media from Task 4.
- Produces: a running, verified native Immich.
- [ ] **Step 1: Start the server and watch logs**
Run: `sudo systemctl start immich-server && journalctl -u immich-server -f`
Expected: it connects to the DB, runs same-version startup checks (no destructive migration since 2.7.5==2.7.5), and reports listening on 2283. Leave the follow running through the next step.
- [ ] **Step 2: Start machine-learning**
Run: `sudo systemctl start immich-machine-learning`
Expected: active, no crash loop in `journalctl -u immich-machine-learning`.
- [ ] **Step 3: Functional spot-check in the web UI**
At `http://jupiter:2283`: log in with an existing account; confirm the timeline loads; open an **album**; open the **People/faces** view; open a **shared link**; open one photo so a **thumbnail and its full original both load** (this proves DB↔file paths align after the media move).
Expected: all present, images render.
- [ ] **Step 4: Confirm homepage dashboard tile**
Open the homepage dashboard; confirm the Immich tile appears under "Media" and links to `http://jupiter:2283`.
- [ ] **Step 5: Enable and verify hardware transcoding**
In Immich **Administration → Settings → Video Transcoding**, set hardware acceleration to **Quick Sync** (QSV) (or VAAPI). Trigger a transcode (upload/play a video that needs transcoding, or run the transcoding job). Then:
Run: `journalctl -u immich-server | grep -iE 'qsv|vaapi|hwaccel|transcode'`
Expected: log shows the hardware path in use, not a CPU-fallback error. Confirm `/dev/dri/renderD128` is accessible to the service (the `video`/`render` groups + `accelerationDevices` from Task 1 handle this).
---
### Task 6: Sign-off and cleanup (operator-run)
**Files:** none in repo. Merge the branch; then, only after a confidence window, remove docker.
**Interfaces:**
- Consumes: a verified running instance (Task 5).
- [ ] **Step 1: Merge the feature branch**
Open a PR from `feat/immich-nixos-module` into `main` and merge it (repo convention: PRs via the Gitea remote).
- [ ] **Step 2: Confidence window**
Use Immich normally for a few days. Keep the docker `<UPLOAD_LOCATION>` source copy and `~/immich-db.sql` untouched as the rollback path.
- [ ] **Step 3: Rollback (only if needed, before cleanup)**
If something is wrong: `sudo systemctl stop immich-server immich-machine-learning`, set `immich.enable = false` (or check out the pre-migration commit), `sudo nixos-rebuild switch --flake '.#jupiter'`, then `docker compose up -d` in the old stack. Original docker DB + upload folder are intact until Step 4.
- [ ] **Step 4: Cleanup (after sign-off)**
Remove the docker Immich stack (`docker compose down --rmi all --volumes` in the old dir if the DB volume is dedicated — verify first), delete the now-duplicated `<UPLOAD_LOCATION>` source, and remove `~/immich-db.sql`. Optionally disable the `docker` profile on jupiter if Immich was its only consumer (check other services first — jupiter's `docker.enable` may still be needed).
---
## Self-Review
**Spec coverage:**
- Native `services.immich` → Task 1. ✓
- Version target 2.7.5==stable, no override → Global Constraints + Task 2 Step 1. ✓
- Media at default `/var/lib/immich` → Task 1 + Task 4 Step 3. ✓
- DB migrate keep-everything → Tasks 24. ✓
- HW transcoding only → Task 1 (`accelerationDevices`, groups) + Task 5 Step 5. ✓
- LAN+VPN, port 2283, openFirewall, homepage tile → Task 1 + Task 5 Steps 34. ✓
- Rollback path → Task 6 Step 3. ✓
- Deferred (OpenVINO/NAS/proxy) → correctly absent. ✓
**Placeholder scan:** No TBD/TODO; every command is concrete. Placeholders like `<db-container>`, `<UPLOAD_LOCATION>`, `<POSTGRES_USER>` are runtime values the operator reads in Task 2 Step 1 — intentional, not gaps.
**Type consistency:** Option name `my.profiles.immich.enable` and path `/var/lib/immich` used consistently across all tasks. Media subfolder list matches between Task 4 Step 3 and the spec.
@@ -1,234 +0,0 @@
# DankMaterialShell (niri) Profile — Design
**Date:** 2026-07-24
**Branch:** `feat/dank-material-shell`
**Status:** Approved (brainstorming)
## Summary
Add a `my.profiles.dank` NixOS profile that provides a complete
[DankMaterialShell](https://danklinux.com) (DMS) desktop running on the
**niri** Wayland compositor, selectable at the SDDM login screen **alongside**
the existing KDE session on **mibook** (user `finn`).
DMS's keybind, spawn, and declarative-settings integration is delivered only
through home-manager modules, so this work also introduces home-manager into the
flake — as its own opt-in profile (`my.profiles.home-manager`) that the `dank`
profile enables as a dependency. Everything stays declarative inside the flake;
there is no separate home-manager entrypoint.
## Background / Research
DMS is a Quickshell-based Wayland desktop **shell** (bar, launcher, control
center, lock screen, notifications) — not a compositor. Its repository
(`github:AvengeMedia/DankMaterialShell`) ships a flake with:
- `nixosModules.dank-material-shell` — system side: installs the `dms-shell`
package + `quickshell`, a `dms` systemd **user** service, `dgop` (system
monitoring), matugen (dynamic theming), and enables polkit /
power-profiles-daemon / accounts-daemon / geoclue2. Configured via
`programs.dank-material-shell.*`.
- `homeModules.dank-material-shell` — home side: declarative
`~/.config/DankMaterialShell/settings.json`, the `dms` user service, plugin
management, and the `programs.quickshell` HM program.
- `homeModules.niri` — niri-specific integration: generates niri keybinds wired
to `dms ipc` (launcher, clipboard, notifications, media/brightness keys, power
menu, lock) and `spawn-at-startup` for `dms run`.
`homeModules.niri` builds on **niri-flake**'s home-manager API
(`programs.niri.settings`, `config.lib.niri.actions`); DMS does **not** bundle
niri-flake, so it must be added as its own input. quickshell ≥ 0.3.0 is
recommended and is available in nixos-unstable → use `pkgs.unstable.quickshell`.
### Key repo facts
- This repo is flake-parts based, pins `nixpkgs/nixos-26.05`, and exposes
`pkgs.unstable` via an overlay in `machines/configuration.nix`.
- `inputs` and `self` are already threaded into every NixOS module via
`_module.args` (set in `machines/configuration.nix`), so `modules/` files may
take `inputs` / `self` as arguments and `imports = [ inputs.<x>... ]`.
- The repo currently uses **no home-manager**; compositor profiles (e.g.
`hyprland`) install packages via `users.users.finn.packages`.
- mibook currently runs the KDE desktop (`my.profiles.kde-desktop`).
## Scope
- **In:** mibook only; niri+DMS as a session parallel to KDE; home-manager
scoped to user `finn`; a reusable `my.profiles.home-manager` module.
- **Out:** enabling on jupiter (headless server); Dank Greeter as the login
manager (SDDM stays); declarative DMS `settings.json` content (DMS is
configured through its own GUI at runtime; the HM `settings` option remains
available for later use but is left empty); any change to `configuration.nix`.
## Design
### 1. New flake inputs (`flake.nix`)
```nix
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
niri.url = "github:sodiboo/niri-flake";
dank.url = "github:AvengeMedia/DankMaterialShell";
```
Notes:
- `dank` tracks nixos-unstable internally (its own `nixpkgs`), which is expected
for the DMS package build.
- `niri-flake` provides both the compositor session (nixosModule) and the HM
`programs.niri.settings` API that DMS's niri module extends.
- `home-manager` follows the repo `nixpkgs` (nixos-26.05).
`machines/configuration.nix` is **not** modified — the existing
`_module.args.inputs = self.inputs` line already makes these inputs reachable
from any module.
### 2. Home-manager profile (`modules/environments/home-manager/default.nix`)
A new opt-in `my.profiles.home-manager` profile. Because module `imports` cannot
be gated on an option value, the home-manager NixOS module is imported
unconditionally (harmless — it only adds options and does nothing until
`home-manager.users.*` is populated); only the global settings are guarded by
`mkIf cfg.enable`:
```nix
{ config, lib, inputs, self, ... }:
let
cfg = config.my.profiles.home-manager;
in
{
imports = [ inputs.home-manager.nixosModules.home-manager ];
options.my.profiles.home-manager.enable =
lib.mkEnableOption "home-manager integration";
config = lib.mkIf cfg.enable {
home-manager.useGlobalPkgs = true; # HM shares the system pkgs (+ unstable overlay)
home-manager.useUserPackages = true;
home-manager.extraSpecialArgs = { inherit inputs self; };
};
}
```
Registered by adding `./environments/home-manager` to
`modules/environments/default.nix`.
> Placement note: the profile lives under `modules/environments/` (where
> `my.profiles.*` modules live) to match the namespace, even though its function
> is infrastructural.
### 3. Dank profile (`modules/environments/dank/default.nix`)
Standard profile pattern; enables the home-manager profile as a dependency and
splits configuration between the system module (session + daemons) and the
home-manager module (keybinds / spawn / settings):
```nix
{ config, lib, pkgs, inputs, ... }:
let
cfg = config.my.profiles.dank;
in
{
imports = [ inputs.dank.nixosModules.dank-material-shell ];
options.my.profiles.dank.enable = lib.mkEnableOption "DankMaterialShell on niri";
config = lib.mkIf cfg.enable {
# dependency: pull in home-manager integration
my.profiles.home-manager.enable = true;
# --- system side: niri session + DMS daemons/packages ---
programs.niri.enable = true; # niri-flake nixosModule → Wayland session in SDDM
programs.dank-material-shell = {
enable = true;
systemd.enable = true;
quickshell.package = pkgs.unstable.quickshell; # >= 0.3.0 from unstable
# enableSystemMonitoring / enableVPN / enableDynamicTheming / enableAudioWavelength
# / enableCalendarEvents all default true — left on.
};
# --- home side: DMS + niri keybinds for finn ---
home-manager.users.finn = {
imports = [
inputs.niri.homeModules.niri
inputs.dank.homeModules.dank-material-shell
inputs.dank.homeModules.niri
];
programs.dank-material-shell = {
enable = true;
systemd.enable = true;
quickshell.package = pkgs.unstable.quickshell;
niri.enableKeybinds = true; # Mod+Space launcher, Mod+V clipboard, media/brightness, power, lock…
niri.enableSpawn = true; # spawn `dms run` at niri startup
};
home.stateVersion = "26.05"; # match nixpkgs release; required by HM
};
};
}
```
Registered by adding `./environments/dank` to
`modules/environments/default.nix`.
Open validation point (resolve during implementation, not a blocker):
- Confirm the exact HM output name for the niri integration
(`homeModules.niri` vs `homeModules.dankMaterialShell.niri`) and that
`programs.niri.enable` is the correct niri-flake nixosModule option; adjust to
match the pinned input revisions.
- Confirm whether `programs.dank-material-shell.quickshell.package` needs to be
set on both the system and HM sides or only one; set consistently.
### 4. Enable on mibook (`machines/mibook/environments.nix`)
Add to the `my.profiles` block:
```nix
dank.enable = true;
```
KDE (`kde-desktop.enable = true`) stays; both sessions are offered by SDDM.
### 5. Documentation
No homepage dashboard entry (DMS is not a web service). CLAUDE.md is left
unchanged unless, after implementation, the home-manager pattern warrants a short
note — decided at the end, not up front (YAGNI).
## Data / control flow
1. `nixos-rebuild` builds the mibook toplevel; niri-flake registers a `niri`
Wayland session, DMS nixosModule installs packages + the `dms` user service
template, home-manager renders finn's niri config (with DMS keybinds) and DMS
config.
2. At login, SDDM lists **KDE** and **niri**. Selecting niri starts the
compositor; `spawn-at-startup` launches `dms run`, bringing up the DMS shell.
3. Keybinds invoke `dms ipc …` (launcher, clipboard, notifications, media,
brightness, power, lock). DMS is further configured via its own GUI, persisted
under `~/.config/DankMaterialShell/`.
## Risks / open questions
- **unstable/stable skew:** the repo pins nixos-26.05 while `dank` and
`niri-flake` track nixos-unstable, and quickshell comes from `pkgs.unstable`.
Qt6/quickshell version mismatch between the DMS package and the stable base is
the most likely failure. Mitigation: validate with a full toplevel build and,
if it fails, align quickshell/qt sourcing (e.g. take the DMS package's own
quickshell) before switching.
- **HM output/option names** may differ from the researched revision — pin the
inputs first, then read the resolved module options and adjust (see §3 open
validation point).
- **First HM activation** for finn: ensure `home.stateVersion` is set so the
build doesn't error.
## Verification
1. `nix flake check` succeeds.
2. `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'`
succeeds.
3. `sudo nixos-rebuild test --flake '.#mibook'`, then log into the **niri**
session: DMS bar appears, `Mod+Space` opens the launcher, KDE session still
works.
@@ -0,0 +1,145 @@
# Intel Quick Sync hardware transcoding for Jellyfin on jupiter
## Problem
Jellyfin streams stutter on jupiter whenever a client needs a transcode
(unsupported codec/container, bitrate cap, or a client that can't
direct-play). Transcoding currently runs entirely on CPU.
jupiter's Intel iGPU is already usable at the OS level:
- `hardware.graphics.enable = true` with `intel-media-driver` (the `iHD`
VAAPI driver) is configured in
`machines/jupiter/hardware-configuration.nix:20-27`.
- The commented-out `i915.force_probe = "9a49"` kernel param there
corresponds to a Quick-Sync-capable Intel UHD iGPU, confirming the
hardware supports it.
But `modules/environments/jellyfin/default.nix` never grants the
`jellyfin` systemd service access to `/dev/dri`, so Jellyfin has no path
to the GPU and silently falls back to software transcoding.
## Goal
Give the Jellyfin service access to the iGPU's VAAPI render node, so
Quick Sync can be enabled in Jellyfin's own dashboard and transcodes are
offloaded from the CPU.
## Non-goals
- Remote/external access or reverse-proxy tuning.
- General CPU/RAM headroom review of jupiter.
- A toggle option (`my.profiles.jellyfin.hardwareAcceleration.enable`) —
jupiter only has the one iGPU, so this is hardcoded on rather than
made configurable.
## Design
### NixOS change (declarative)
In `modules/environments/jellyfin/default.nix`, inside the existing
`config = lib.mkIf cfg.enable { ... }` block, grant the systemd service
supplementary access to the `video` and `render` groups (the groups that
own `/dev/dri/card*` and `/dev/dri/renderD*`):
```nix
systemd.services.jellyfin.serviceConfig.SupplementaryGroups = [
"video"
"render"
];
```
This is additive to the existing `systemd.services.jellyfin.after = [
"network-online.target" ];` block already in the file — both apply to
the same service.
Also add `libva-utils` to `environment.systemPackages` (or scoped to
this module) so `vainfo` is available on jupiter to verify the driver
loads correctly.
### Manual step (not declarative)
Jellyfin stores its transcoding/hardware-acceleration choice in its own
internal `encoding.xml`, which the NixOS module does not expose as an
option. After deploying the Nix change, one-time manual configuration in
the Jellyfin dashboard is required:
1. **Dashboard → Playback**.
2. Hardware acceleration: **Intel QuickSync (QSV)**.
3. VA-API device: `/dev/dri/renderD128`.
4. Enable hardware decoding for the codecs your library actually uses
(H264 at minimum; HEVC/VP9 depending on iGPU generation).
5. If any HDR content exists in the library, enable tone-mapping — this
is one of the more CPU-expensive operations Quick Sync can offload.
## Verification
Build-time (from the Mac, no SSH needed):
```
nix eval '.#nixosConfigurations.jupiter.config.systemd.services.jellyfin.serviceConfig.SupplementaryGroups' \
--extra-experimental-features 'nix-command flakes'
```
Expect `[ "video" "render" ]`.
On jupiter after `sudo nixos-rebuild switch --flake '.#jupiter'`:
```
systemctl status jellyfin
journalctl -u jellyfin -n 50 --no-pager
vainfo
```
`vainfo` should list the `iHD` driver and print supported VAEntrypoints
(VLD decode / encode profiles for H264/HEVC).
Functional check: play a file on a client that forces transcoding (or
force it manually via Jellyfin's playback quality setting), then in
Jellyfin's dashboard **Activity/Now Playing** panel confirm the
transcode reason and check that CPU usage on jupiter (`htop`) stays low
during playback rather than pegging a core — Quick Sync offload should
show up as low CPU, some GPU (`intel_gpu_top`) activity instead.
## Open items
- Exact supported codec list depends on the iGPU generation (device ID
`9a49`) — confirm via `vainfo` output once run, and enable only the
hardware decode paths it actually reports.
## Post-deploy fix: two additional runtime packages required
After the initial deploy (Task 1's `SupplementaryGroups` grant) and
enabling QSV in the dashboard, HEVC HDR playback hung indefinitely
(Direct Play worked for some titles; titles that needed a real
transcode+tonemap never produced output). Root-caused via
`journalctl -u jellyfin` and the per-session ffmpeg transcode log
(`find / -xdev -iname '*ffmpeg-transcode*'`) — two separate runtimes
were missing beyond `intel-media-driver` (which only provides VAAPI):
1. **QSV session creation failed:** `Error creating a MFX session: -9`
/ `Error initializing an MFX session: -3` on
`-init_hw_device qsv=qs@va`. VAAPI and QSV are separate runtimes on
Linux — QSV needs the oneVPL/MFX GPU implementation. Fix: added
`pkgs.vpl-gpu-rt` ("oneAPI Video Processing Library Intel GPU
implementation"; note `onevpl-intel-gpu` is the old, renamed
attribute) to `hardware.graphics.extraPackages` in
`machines/jupiter/hardware-configuration.nix`.
2. **OpenCL device creation failed:** `Failed to get number of OpenCL
platforms: -1001` (`CL_PLATFORM_NOT_FOUND_KHR`) on
`-init_hw_device opencl=ocl@va`. The `tonemap_opencl` filter jellyfin
uses for HDR→SDR tone-mapping needs a working OpenCL ICD, which
nothing installed so far provides. Fix: added
`pkgs.intel-compute-runtime` ("Intel Graphics Compute Runtime oneAPI
Level Zero and OpenCL, supporting 12th Gen and newer" — matches
jupiter's Tiger Lake/Xe iGPU) to the same `extraPackages` list.
Confirmed working end-to-end: HEVC HDR transcode with QSV encode +
OpenCL tone-map runs at `speed=2.68x` realtime on jupiter's iGPU, and
plays smoothly on Apple TV (JellyTV app).
Both packages live in `machines/jupiter/hardware-configuration.nix`
(`hardware.graphics.extraPackages`), alongside `intel-media-driver`,
rather than in the jellyfin module itself — they're iGPU runtime
capabilities, not something specific to the jellyfin service.
@@ -0,0 +1,121 @@
# 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 | **Resolved: docker runs 2.7.5 == stable nixpkgs 2.7.5.** Use the stable module as-is; no `package` override. Same-version restore, no forward schema migration |
| 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 <server> immich --version` or web UI footer).
2. **Resolved 2026-08-05: running version is 2.7.5, equal to stable nixpkgs.**
Use the stable module as-is (no `package` override). Kept for reference:
- running ≤ 2.7.5 → stable module as-is ← **this case**
- 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)
4. `docker compose down` (DB may stay up for the dump).
5. Dump DB: `docker exec -t <db> 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 — RESOLVED
The main risk was step 9 crossing the **pgvecto.rs → VectorChord** vector-extension
boundary. With source and target both at **2.7.5**, both use VectorChord — no
boundary crossing and no forward schema migration. The restore is a same-version
dump/load. Residual risk is limited to routine dump/restore mechanics
(roles, extension availability in the NixOS Postgres, ownership on restore).
Generated
+12 -225
View File
@@ -1,41 +1,5 @@
{
"nodes": {
"dank": {
"inputs": {
"dank-qml-common": "dank-qml-common",
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1784830217,
"narHash": "sha256-6MvMw45RSORjINmFd3Lfz9VX9sH8+xTb5WNZiWIKbDw=",
"owner": "AvengeMedia",
"repo": "DankMaterialShell",
"rev": "8af71036cca800064f3fb22dd201109ade6c5ee9",
"type": "github"
},
"original": {
"owner": "AvengeMedia",
"repo": "DankMaterialShell",
"type": "github"
}
},
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1784647395,
"narHash": "sha256-kcLSJC8XBzmWoSBc90Zn81qrNsqWMkwQ1m/zZixOw30=",
"owner": "AvengeMedia",
"repo": "dank-qml-common",
"rev": "1919595f0dde4f5bf5ac0baa1902ff6d070971d5",
"type": "github"
},
"original": {
"owner": "AvengeMedia",
"repo": "dank-qml-common",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
@@ -52,22 +16,6 @@
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
@@ -131,89 +79,12 @@
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1784350909,
"narHash": "sha256-ZWyzLbS1yKUTeFJLmdVuWNnHttL333/ldJbEE+KzCrM=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "4ce190229c73d44536caa7072f6308fb2d8feeb3",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-26.05",
"repo": "home-manager",
"type": "github"
}
},
"niri": {
"inputs": {
"niri-stable": "niri-stable",
"niri-unstable": "niri-unstable",
"nixpkgs": "nixpkgs_2",
"nixpkgs-stable": "nixpkgs-stable",
"xwayland-satellite-stable": "xwayland-satellite-stable",
"xwayland-satellite-unstable": "xwayland-satellite-unstable"
},
"locked": {
"lastModified": 1784874881,
"narHash": "sha256-u4jhSIf/Un0qZB+Cn3hTTaHSI20XeAxYbA5o4faqdJs=",
"owner": "sodiboo",
"repo": "niri-flake",
"rev": "ef7a2a3d719af46b906c22a3ebfb7d65627b2cd2",
"type": "github"
},
"original": {
"owner": "sodiboo",
"repo": "niri-flake",
"type": "github"
}
},
"niri-stable": {
"flake": false,
"locked": {
"lastModified": 1756556321,
"narHash": "sha256-RLD89dfjN0RVO86C/Mot0T7aduCygPGaYbog566F0Qo=",
"owner": "YaLTeR",
"repo": "niri",
"rev": "01be0e65f4eb91a9cd624ac0b76aaeab765c7294",
"type": "github"
},
"original": {
"owner": "YaLTeR",
"ref": "v25.08",
"repo": "niri",
"type": "github"
}
},
"niri-unstable": {
"flake": false,
"locked": {
"lastModified": 1784570726,
"narHash": "sha256-9EMn69JBcFWFgUM7f0VBAX+jBby5b9H3M59U75+5yI4=",
"owner": "YaLTeR",
"repo": "niri",
"rev": "7f26c3ee804fb6ed458ef7fb0e3c794f14e0b3bc",
"type": "github"
},
"original": {
"owner": "YaLTeR",
"repo": "niri",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": "flake-compat_2",
"flake-compat": "flake-compat",
"flake-parts": "flake-parts_2",
"git-hooks-nix": "git-hooks-nix",
"nixpkgs": "nixpkgs_3",
"nixpkgs": "nixpkgs",
"nixpkgs-23-11": "nixpkgs-23-11",
"nixpkgs-regression": "nixpkgs-regression"
},
@@ -269,7 +140,7 @@
},
"nixos-hardware": {
"inputs": {
"nixpkgs": "nixpkgs_4"
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1784723954,
@@ -287,18 +158,15 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1783224372,
"narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d407951447dcd00442e97087bf374aad70c04cea",
"type": "github"
"lastModified": 1783148766,
"narHash": "sha256-H9+N+GFtsbVC8ZniHliChM7ndizxtqVZs6bnGOLM3WQ=",
"rev": "a50de1b7d8a586adc18d2395c19de7d6058e6030",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.4193.a50de1b7d8a5/nixexprs.tar.xz"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
"type": "tarball",
"url": "https://channels.nixos.org/nixos-26.05/nixexprs.tar.xz"
}
},
"nixpkgs-23-11": {
@@ -348,22 +216,6 @@
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1782847189,
"narHash": "sha256-twXPFqFsrrY5r28Zh7Homgcp2gUMBgQ6WDS98Q/3xFI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b6018f87da91d19d0ab4cf979885689b469cdd41",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1784796856,
@@ -379,35 +231,6 @@
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1784796856,
"narHash": "sha256-wWFrV5/Qbm+lyt5x20E/bSbfJiGKMo4RCxZV8cl/WZI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e2587caef70cea85dd97d7daab492899902dbf5d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1783148766,
"narHash": "sha256-H9+N+GFtsbVC8ZniHliChM7ndizxtqVZs6bnGOLM3WQ=",
"rev": "a50de1b7d8a586adc18d2395c19de7d6058e6030",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.4193.a50de1b7d8a5/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-26.05/nixexprs.tar.xz"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1767892417,
"narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=",
@@ -420,7 +243,7 @@
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
}
},
"nixpkgs_5": {
"nixpkgs_3": {
"locked": {
"lastModified": 1784707089,
"narHash": "sha256-DUedXhD2Rg8q4Xyd07Sb90eZGy4gg6W+Vl/WbLNwAZo=",
@@ -436,49 +259,13 @@
},
"root": {
"inputs": {
"dank": "dank",
"flake-parts": "flake-parts",
"home-manager": "home-manager",
"niri": "niri",
"nix": "nix",
"nixos-generators": "nixos-generators",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs_5",
"nixpkgs": "nixpkgs_3",
"nixpkgs-unstable": "nixpkgs-unstable"
}
},
"xwayland-satellite-stable": {
"flake": false,
"locked": {
"lastModified": 1755491097,
"narHash": "sha256-m+9tUfsmBeF2Gn4HWa6vSITZ4Gz1eA1F5Kh62B0N4oE=",
"owner": "Supreeeme",
"repo": "xwayland-satellite",
"rev": "388d291e82ffbc73be18169d39470f340707edaa",
"type": "github"
},
"original": {
"owner": "Supreeeme",
"ref": "v0.7",
"repo": "xwayland-satellite",
"type": "github"
}
},
"xwayland-satellite-unstable": {
"flake": false,
"locked": {
"lastModified": 1784679892,
"narHash": "sha256-Mb7jpqnrcYCfNSItIkkHpuR3YxWFxPuIBfcwNKlRBkk=",
"owner": "Supreeeme",
"repo": "xwayland-satellite",
"rev": "8d135d3b2854b30fd01ea6cd6c27e523dd50a839",
"type": "github"
},
"original": {
"owner": "Supreeeme",
"repo": "xwayland-satellite",
"type": "github"
}
}
},
"root": "root",
-6
View File
@@ -11,12 +11,6 @@
url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager/release-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
niri.url = "github:sodiboo/niri-flake";
dank.url = "github:AvengeMedia/DankMaterialShell";
};
+1
View File
@@ -21,6 +21,7 @@ in
sonarr.enable = true;
jellyfin.enable = true;
jellyseerr.enable = true;
immich.enable = true;
development.enable = true;
home-assistant.enable = true;
@@ -35,6 +35,8 @@
#vaapiIntel # LIBVA_DRIVER_NAME=i965 (older but works better for Firefox/Chromium)
libva-vdpau-driver
libvdpau-va-gl
vpl-gpu-rt # oneVPL/MFX runtime, required for QSV (h264_qsv/hevc_qsv) session creation
intel-compute-runtime # OpenCL runtime, required for tonemap_opencl (HDR tone-mapping)
];
};
+1
View File
@@ -10,6 +10,7 @@
./disks.nix
./hardware-configuration.nix
./environments.nix
./network.nix
# ./system.nix use docker here
];
-1
View File
@@ -6,7 +6,6 @@ in
{
my.profiles = {
kde-desktop.enable = true;
dank.enable = false;
zsh.enable = true;
apps = {
desktop_apps = true;
+8
View File
@@ -0,0 +1,8 @@
_: {
# Athena (local AI): allow LAN access to the Hermes web dashboard.
# Bound to 0.0.0.0:9119 in the athena docker stack; NixOS default-deny
# firewall otherwise blocks inbound connections from other devices.
networking.firewall.allowedTCPPorts = [
9119 # athena hermes dashboard
];
}
-48
View File
@@ -1,48 +0,0 @@
{
config,
lib,
pkgs,
inputs,
self,
...
}:
let
cfg = config.my.profiles.dank;
in
{
imports = [ self.inputs.dank.nixosModules.dank-material-shell ];
options.my.profiles.dank.enable = lib.mkEnableOption "DankMaterialShell on niri";
config = lib.mkIf cfg.enable {
# Dependency: declarative user config comes from home-manager.
my.profiles.home-manager.enable = true;
# System side: niri Wayland session + DMS daemons/packages.
programs.niri.enable = true;
programs.dank-material-shell = {
enable = true;
systemd.enable = true;
quickshell.package = pkgs.unstable.quickshell;
};
# Home side: DMS shell + niri spawn for finn (keybinds come via includes).
home-manager.users.finn = {
imports = [
inputs.niri.homeModules.niri
inputs.dank.homeModules.dank-material-shell
inputs.dank.homeModules.niri
];
programs.dank-material-shell = {
enable = true;
systemd.enable = true;
quickshell.package = pkgs.unstable.quickshell;
niri.enableSpawn = true;
};
home.stateVersion = "26.05";
};
};
}
+1 -2
View File
@@ -8,8 +8,6 @@
./development
./home-assistant
./hyprland
./home-manager
./dank
./zsh
./paperless
./prowlarr
@@ -21,5 +19,6 @@
./sonarr
./jellyfin
./jellyseerr
./immich
];
}
@@ -1,21 +0,0 @@
{
config,
lib,
inputs,
self,
...
}:
let
cfg = config.my.profiles.home-manager;
in
{
imports = [ self.inputs.home-manager.nixosModules.home-manager ];
options.my.profiles.home-manager.enable = lib.mkEnableOption "home-manager integration";
config = lib.mkIf cfg.enable {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.extraSpecialArgs = { inherit inputs self; };
};
}
+52
View File
@@ -0,0 +1,52 @@
# Immich self-hosted photo & video server
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.immich;
hostName = config.networking.hostName;
port = 2283;
in
{
options.my.profiles.immich = with lib; {
enable = mkEnableOption "Immich photo server";
};
config = lib.mkIf cfg.enable {
services.immich = {
enable = true;
host = "0.0.0.0";
inherit port;
openFirewall = true;
mediaLocation = "/var/lib/immich";
machine-learning.enable = true;
accelerationDevices = [ "/dev/dri/renderD128" ];
# Setting `settings` puts Immich in config-file mode: the admin settings
# UI becomes read-only and system config is managed declaratively here.
settings = {
server.externalDomain = "http://${hostName}:${toString port}";
# Intel Quick Sync hardware transcoding (jupiter's iGPU).
ffmpeg.accel = "qsv";
};
};
# The native module does not add GPU groups; required 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";
}
];
};
}
@@ -22,6 +22,8 @@ in
openFirewall = true;
};
environment.systemPackages = [ pkgs.libva-utils ];
my.homepage.services = [
{
group = "Media";
@@ -34,6 +36,10 @@ in
systemd.services.jellyfin = {
after = [ "network-online.target" ];
serviceConfig.SupplementaryGroups = [
"video"
"render"
];
};
};
}