Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae6c81bf54 |
@@ -1,299 +0,0 @@
|
|||||||
# 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 2–6 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 2–6 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 2–4. ✓
|
|
||||||
- HW transcoding only → Task 1 (`accelerationDevices`, groups) + Task 5 Step 5. ✓
|
|
||||||
- LAN+VPN, port 2283, openFirewall, homepage tile → Task 1 + Task 5 Steps 3–4. ✓
|
|
||||||
- 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.
|
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# mibook: choose terminal-only vs desktop at boot
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Let mibook offer a choice, each time it boots, between the normal KDE desktop
|
||||||
|
and a terminal-only ("server") mode with no graphical session — decided at
|
||||||
|
boot, without rebuilding the system.
|
||||||
|
|
||||||
|
## Background: why the first attempt failed
|
||||||
|
|
||||||
|
The first implementation booted mibook to a text console by default
|
||||||
|
(`systemd.defaultUnit = "multi-user.target"`, display manager not started) and
|
||||||
|
provided a `desktop` command to start KDE on demand. On real hardware this
|
||||||
|
locked the machine out:
|
||||||
|
|
||||||
|
- **No SSH.** mibook is WiFi-only and its WiFi credentials are stored per-user
|
||||||
|
in KWallet ("agent-owned"). NetworkManager only receives the password once a
|
||||||
|
desktop session is running, so a headless boot never joins the network and
|
||||||
|
the machine has no IP — nothing to SSH into.
|
||||||
|
- **No usable console.** The boot appeared to "hang" with no login prompt:
|
||||||
|
`NetworkManager-wait-online` stalled waiting for a network that never came
|
||||||
|
up, and the `getty` login prompt on tty1 was buried under later service
|
||||||
|
messages.
|
||||||
|
|
||||||
|
Conclusion: a headless WiFi laptop cannot be reached remotely, and the plain
|
||||||
|
console was hard to use. The design must (a) keep the desktop as the reliable
|
||||||
|
default, (b) make the terminal path a deliberate, self-sufficient choice, and
|
||||||
|
(c) not depend on the network being up.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- The **default GRUB entry** boots straight into KDE — unchanged from the
|
||||||
|
known-working baseline.
|
||||||
|
- A **separate GRUB entry, `mibook (terminal)`** (a NixOS *specialisation*),
|
||||||
|
boots to a text console with **autologin** for `finn`. From there the user
|
||||||
|
can work in the shell or run `desktop` to bring KDE up (via SDDM).
|
||||||
|
- The choice is made in the GRUB menu at boot — matching the original request
|
||||||
|
to "decide each time I boot."
|
||||||
|
- Booting never stalls on the network.
|
||||||
|
|
||||||
|
### Known limitation (documented, not fixed in config)
|
||||||
|
|
||||||
|
In terminal mode WiFi will not connect on its own, because the password is
|
||||||
|
stored per-user in KWallet. To reach mibook over SSH from terminal mode, the
|
||||||
|
user must first save the WiFi as a **system** connection in KDE:
|
||||||
|
network settings → the WiFi network → "All users may connect to this network".
|
||||||
|
Until then, terminal mode is local-console-only. This is a one-time manual
|
||||||
|
step outside the scope of the Nix config.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### `machines/mibook/configuration.nix`
|
||||||
|
|
||||||
|
- Add a NixOS specialisation `specialisation.terminal.configuration`:
|
||||||
|
- `system.nixos.tags = [ "terminal" ];` — labels the generated boot entry.
|
||||||
|
- `systemd.defaultUnit = lib.mkForce "multi-user.target";` — boots to the
|
||||||
|
text console. `graphical.target` is what pulls in the display manager (via
|
||||||
|
its embedded `Wants=display-manager.service`), so defaulting to
|
||||||
|
`multi-user.target` leaves SDDM installed but not started at boot.
|
||||||
|
- `services.getty.autologinUser = "finn";` — guarantees a usable shell on
|
||||||
|
the console instead of a login prompt that can scroll off screen.
|
||||||
|
- A `desktop` command via
|
||||||
|
`pkgs.writeShellScriptBin "desktop" "exec sudo systemctl start display-manager.service"`
|
||||||
|
in `environment.systemPackages`, to start KDE on demand.
|
||||||
|
- Add `systemd.services.NetworkManager-wait-online.enable = false;` (applies to
|
||||||
|
both the default and terminal boots) so boot never stalls waiting for the
|
||||||
|
network.
|
||||||
|
|
||||||
|
### Reverted from the first attempt
|
||||||
|
|
||||||
|
- `modules/environments/kde-desktop/default.nix` — remove the `startOnBoot`
|
||||||
|
option and its `mkMerge`/`mkIf` machinery; back to the original profile that
|
||||||
|
simply enables SDDM + Plasma 6.
|
||||||
|
- `machines/mibook/environments.nix` — remove `kde-desktop.startOnBoot = false;`
|
||||||
|
(back to just `kde-desktop.enable = true;`).
|
||||||
|
|
||||||
|
## Why a specialisation
|
||||||
|
|
||||||
|
A specialisation generates a second boot-menu entry automatically from a
|
||||||
|
modified copy of the configuration. It is the idiomatic NixOS mechanism for a
|
||||||
|
boot-time choice and avoids fragile hand-written GRUB `extraEntries` that would
|
||||||
|
need to track kernel/initrd paths across generations. The default entry remains
|
||||||
|
byte-for-byte the working desktop configuration.
|
||||||
|
|
||||||
|
## Testing / verification
|
||||||
|
|
||||||
|
- `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'`
|
||||||
|
builds both `nixos-system-mibook` and `nixos-system-mibook-terminal`.
|
||||||
|
- Verified on the built closures:
|
||||||
|
- Parent `default.target` → `graphical.target`; no console autologin
|
||||||
|
(identical to the pre-change baseline).
|
||||||
|
- Specialisation `default.target` → `multi-user.target`; tty1 getty wrapper
|
||||||
|
contains `--autologin finn`; `desktop` present in the system profile.
|
||||||
|
- `NetworkManager-wait-online` disabled in both.
|
||||||
|
- Post-`switch` manual check on mibook: default GRUB entry boots to KDE; the
|
||||||
|
`terminal` entry boots to an autologged-in console; running `desktop` there
|
||||||
|
starts SDDM and a working Plasma session.
|
||||||
|
|
||||||
|
## Trade-offs
|
||||||
|
|
||||||
|
- Autologin on the terminal console means physical access grants a shell
|
||||||
|
without a password. Acceptable for a personal laptop the user controls; the
|
||||||
|
desktop (default) boot is unaffected.
|
||||||
|
- `desktop` relies on `sudo`; the user has sudo access, so no extra config is
|
||||||
|
required.
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
# 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.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 <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
-17
@@ -79,22 +79,6 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"grafana-content": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1785940786,
|
|
||||||
"narHash": "sha256-flH5pwYtB3QWnZ/OYJKoj9twb1fxYrowr9FCh8L1GMY=",
|
|
||||||
"ref": "refs/heads/master",
|
|
||||||
"rev": "4017b678ffa3bde4136a3468f18f1eb3bba52374",
|
|
||||||
"revCount": 1,
|
|
||||||
"type": "git",
|
|
||||||
"url": "ssh://git@rechberg.online:222/finn.markwitz/dashboards.git"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "ssh://git@rechberg.online:222/finn.markwitz/dashboards.git"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nix": {
|
"nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
@@ -276,7 +260,6 @@
|
|||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-parts": "flake-parts",
|
"flake-parts": "flake-parts",
|
||||||
"grafana-content": "grafana-content",
|
|
||||||
"nix": "nix",
|
"nix": "nix",
|
||||||
"nixos-generators": "nixos-generators",
|
"nixos-generators": "nixos-generators",
|
||||||
"nixos-hardware": "nixos-hardware",
|
"nixos-hardware": "nixos-hardware",
|
||||||
|
|||||||
@@ -12,12 +12,6 @@
|
|||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Grafana content (dashboards, datasources, alert rules) — provisioned from the store.
|
|
||||||
grafana-content = {
|
|
||||||
url = "git+ssh://git@rechberg.online:222/finn.markwitz/dashboards.git";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ in
|
|||||||
sonarr.enable = true;
|
sonarr.enable = true;
|
||||||
jellyfin.enable = true;
|
jellyfin.enable = true;
|
||||||
jellyseerr.enable = true;
|
jellyseerr.enable = true;
|
||||||
immich.enable = true;
|
|
||||||
monitoring.enable = true;
|
|
||||||
development.enable = true;
|
development.enable = true;
|
||||||
home-assistant.enable = true;
|
home-assistant.enable = true;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
./disks.nix
|
./disks.nix
|
||||||
./hardware-configuration.nix
|
./hardware-configuration.nix
|
||||||
./environments.nix
|
./environments.nix
|
||||||
./network.nix
|
|
||||||
# ./system.nix use docker here
|
# ./system.nix use docker here
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@
|
|||||||
useOSProber = true;
|
useOSProber = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
# Configure keymap in X11
|
# Configure keymap in X11
|
||||||
services.xserver.xkb = {
|
services.xserver.xkb = {
|
||||||
layout = "de";
|
layout = "de";
|
||||||
@@ -35,7 +33,6 @@
|
|||||||
services.printing.enable = true;
|
services.printing.enable = true;
|
||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
|
|
||||||
|
|
||||||
hardware.nvidia.prime = {
|
hardware.nvidia.prime = {
|
||||||
sync.enable = false;
|
sync.enable = false;
|
||||||
|
|
||||||
@@ -45,6 +42,37 @@
|
|||||||
|
|
||||||
services.openssh.enable = true;
|
services.openssh.enable = true;
|
||||||
|
|
||||||
|
# Don't let boot stall waiting for a network that may never come up
|
||||||
|
# (WiFi credentials live in KWallet and need a desktop session), which
|
||||||
|
# otherwise hangs the terminal boot before the login prompt appears.
|
||||||
|
systemd.services.NetworkManager-wait-online.enable = false;
|
||||||
|
|
||||||
|
# Boot-time choice: the default GRUB entry boots straight into KDE.
|
||||||
|
# A separate "terminal" entry (a NixOS specialisation) boots to a text
|
||||||
|
# console with autologin, where you can work or run `desktop` to bring
|
||||||
|
# KDE up. Pick the entry you want in the GRUB menu at boot.
|
||||||
|
#
|
||||||
|
# NOTE: in terminal mode WiFi will not connect on its own (the password
|
||||||
|
# is stored per-user in KWallet). To reach the machine over SSH from
|
||||||
|
# terminal mode, first save the WiFi as a system connection in KDE:
|
||||||
|
# network settings -> your WiFi -> "All users may connect to this network".
|
||||||
|
specialisation.terminal.configuration = {
|
||||||
|
system.nixos.tags = [ "terminal" ];
|
||||||
|
|
||||||
|
# Boot to a text console. graphical.target is what pulls in the display
|
||||||
|
# manager (via its embedded Wants=display-manager.service), so defaulting
|
||||||
|
# to multi-user.target leaves SDDM installed but not started at boot.
|
||||||
|
systemd.defaultUnit = lib.mkForce "multi-user.target";
|
||||||
|
|
||||||
|
# Guarantee a usable shell on the console (no login prompt to hunt for).
|
||||||
|
services.getty.autologinUser = "finn";
|
||||||
|
|
||||||
|
# Bring the desktop up on demand from the terminal.
|
||||||
|
environment.systemPackages = [
|
||||||
|
(pkgs.writeShellScriptBin "desktop" "exec sudo systemctl start display-manager.service")
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
# KDE (PowerDevil) power settings: do nothing on lid close while on AC power.
|
# KDE (PowerDevil) power settings: do nothing on lid close while on AC power.
|
||||||
# Shipped as a system-wide default; KConfig cascades so a user's own
|
# Shipped as a system-wide default; KConfig cascades so a user's own
|
||||||
# ~/.config/powerdevilrc will override this if present.
|
# ~/.config/powerdevilrc will override this if present.
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
_: {
|
|
||||||
# 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
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -15,11 +15,9 @@
|
|||||||
./docker
|
./docker
|
||||||
./homepage
|
./homepage
|
||||||
./kde-desktop
|
./kde-desktop
|
||||||
./monitoring
|
|
||||||
./readarr
|
./readarr
|
||||||
./sonarr
|
./sonarr
|
||||||
./jellyfin
|
./jellyfin
|
||||||
./jellyseerr
|
./jellyseerr
|
||||||
./immich
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
# 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";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# Grafana Alloy: ships the full systemd journal to Loki.
|
|
||||||
# The nixpkgs module reads /etc/alloy/*.alloy and already runs with the
|
|
||||||
# systemd-journal supplementary group. Alloy exposes /metrics + UI on :12345.
|
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.my.profiles.monitoring;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
services.alloy.enable = true;
|
|
||||||
|
|
||||||
environment.etc."alloy/config.alloy".text = ''
|
|
||||||
loki.source.journal "journal" {
|
|
||||||
max_age = "24h"
|
|
||||||
forward_to = [loki.write.local.receiver]
|
|
||||||
relabel_rules = loki.relabel.journal.rules
|
|
||||||
labels = { host = "jupiter" }
|
|
||||||
}
|
|
||||||
|
|
||||||
loki.relabel "journal" {
|
|
||||||
forward_to = []
|
|
||||||
rule {
|
|
||||||
source_labels = ["__journal__systemd_unit"]
|
|
||||||
target_label = "unit"
|
|
||||||
}
|
|
||||||
rule {
|
|
||||||
source_labels = ["__journal_priority_keyword"]
|
|
||||||
target_label = "priority"
|
|
||||||
}
|
|
||||||
rule {
|
|
||||||
source_labels = ["__journal__transport"]
|
|
||||||
target_label = "transport"
|
|
||||||
}
|
|
||||||
rule {
|
|
||||||
source_labels = ["__journal__boot_id"]
|
|
||||||
target_label = "boot_id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loki.write "local" {
|
|
||||||
endpoint {
|
|
||||||
url = "http://127.0.0.1:3100/loki/api/v1/push"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Observability stack: Grafana + Prometheus + Loki + Alloy.
|
|
||||||
# Content (dashboards, datasources, alert rules) lives in a separate repo,
|
|
||||||
# consumed as the `grafana-content` flake input and provisioned from the store.
|
|
||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./grafana.nix
|
|
||||||
./prometheus.nix
|
|
||||||
./loki.nix
|
|
||||||
./alloy.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
options.my.profiles.monitoring = {
|
|
||||||
enable = lib.mkEnableOption "Grafana + Prometheus + Loki + Alloy observability stack";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# Grafana service + provisioning from the grafana-content flake input.
|
|
||||||
# Provisioning shape verified against nixpkgs 26.05 grafana module:
|
|
||||||
# - datasources.path takes a directory (lndir'd into the provisioning dir)
|
|
||||||
# - dashboard provider is generated here so the store path can be injected
|
|
||||||
# - each alerting resource takes its own YAML *file* (they share one target dir,
|
|
||||||
# so pointing them at a directory would collide)
|
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
inputs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.my.profiles.monitoring;
|
|
||||||
content = inputs.grafana-content;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
services.grafana = {
|
|
||||||
enable = true;
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
server = {
|
|
||||||
http_addr = "0.0.0.0";
|
|
||||||
http_port = 3000;
|
|
||||||
domain = "jupiter.solar.internal";
|
|
||||||
root_url = "http://jupiter.solar.internal:3000/";
|
|
||||||
};
|
|
||||||
security.admin_password = "$__file{/etc/grafana/admin_password}";
|
|
||||||
# 26.05 removed the built-in default; encrypts secrets in Grafana's DB.
|
|
||||||
security.secret_key = "$__file{/etc/grafana/secret_key}";
|
|
||||||
"auth.anonymous" = {
|
|
||||||
enabled = true;
|
|
||||||
org_role = "Viewer";
|
|
||||||
};
|
|
||||||
metrics.enabled = true; # /metrics for Prometheus self-scrape
|
|
||||||
unified_alerting.enabled = true;
|
|
||||||
alerting.enabled = false; # disable legacy alerting
|
|
||||||
};
|
|
||||||
|
|
||||||
provision = {
|
|
||||||
enable = true;
|
|
||||||
|
|
||||||
# static datasources dir → all *.yaml linked in
|
|
||||||
datasources.path = "${content}/provisioning/datasources";
|
|
||||||
|
|
||||||
# dashboard provider generated here with the store path injected
|
|
||||||
dashboards.settings.providers = [
|
|
||||||
{
|
|
||||||
name = "content";
|
|
||||||
type = "file";
|
|
||||||
disableDeletion = true;
|
|
||||||
allowUiUpdates = false;
|
|
||||||
options = {
|
|
||||||
path = "${content}/dashboards";
|
|
||||||
foldersFromFilesStructure = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
# per-resource alerting files (share one target dir → must be files)
|
|
||||||
alerting = {
|
|
||||||
rules.path = "${content}/provisioning/alerting/rules.yaml";
|
|
||||||
contactPoints.path = "${content}/provisioning/alerting/contactpoints.yaml";
|
|
||||||
policies.path = "${content}/provisioning/alerting/policies.yaml";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 3000 ];
|
|
||||||
|
|
||||||
# Register a homepage-dashboard tile (matches the other profiles).
|
|
||||||
my.homepage.services = [
|
|
||||||
{
|
|
||||||
group = "Monitoring";
|
|
||||||
name = "Grafana";
|
|
||||||
description = "Metrics & logs dashboards";
|
|
||||||
href = "http://jupiter.solar.internal:3000";
|
|
||||||
icon = "grafana.png";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Loki single-binary, filesystem storage, tsdb schema, 90d retention via compactor.
|
|
||||||
# Localhost only — Grafana is the sole consumer.
|
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.my.profiles.monitoring;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
services.loki = {
|
|
||||||
enable = true;
|
|
||||||
configuration = {
|
|
||||||
server = {
|
|
||||||
http_listen_address = "127.0.0.1";
|
|
||||||
http_listen_port = 3100;
|
|
||||||
};
|
|
||||||
auth_enabled = false;
|
|
||||||
|
|
||||||
common = {
|
|
||||||
instance_addr = "127.0.0.1";
|
|
||||||
path_prefix = "/var/lib/loki";
|
|
||||||
storage.filesystem = {
|
|
||||||
chunks_directory = "/var/lib/loki/chunks";
|
|
||||||
rules_directory = "/var/lib/loki/rules";
|
|
||||||
};
|
|
||||||
replication_factor = 1;
|
|
||||||
ring.kvstore.store = "inmemory";
|
|
||||||
};
|
|
||||||
|
|
||||||
schema_config.configs = [
|
|
||||||
{
|
|
||||||
from = "2024-01-01";
|
|
||||||
store = "tsdb";
|
|
||||||
object_store = "filesystem";
|
|
||||||
schema = "v13";
|
|
||||||
index = {
|
|
||||||
prefix = "index_";
|
|
||||||
period = "24h";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
limits_config.retention_period = "2160h"; # 90d
|
|
||||||
compactor = {
|
|
||||||
working_directory = "/var/lib/loki/compactor";
|
|
||||||
retention_enabled = true;
|
|
||||||
delete_request_store = "filesystem";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# Prometheus + node_exporter. Prometheus UI reachable on the LAN (:9090);
|
|
||||||
# node_exporter bound to localhost. Scrapes the host plus the stack itself.
|
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.my.profiles.monitoring;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
services.prometheus = {
|
|
||||||
enable = true;
|
|
||||||
listenAddress = "0.0.0.0";
|
|
||||||
port = 9090;
|
|
||||||
retentionTime = "90d";
|
|
||||||
|
|
||||||
exporters.node = {
|
|
||||||
enable = true;
|
|
||||||
listenAddress = "127.0.0.1";
|
|
||||||
port = 9100;
|
|
||||||
# default collectors
|
|
||||||
};
|
|
||||||
|
|
||||||
scrapeConfigs = [
|
|
||||||
{
|
|
||||||
job_name = "node";
|
|
||||||
static_configs = [ { targets = [ "127.0.0.1:9100" ]; } ];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
job_name = "prometheus";
|
|
||||||
static_configs = [ { targets = [ "127.0.0.1:9090" ]; } ];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
job_name = "grafana";
|
|
||||||
static_configs = [ { targets = [ "127.0.0.1:3000" ]; } ];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
job_name = "loki";
|
|
||||||
static_configs = [ { targets = [ "127.0.0.1:3100" ]; } ];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
job_name = "alloy";
|
|
||||||
static_configs = [ { targets = [ "127.0.0.1:12345" ]; } ];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 9090 ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user