Merge remote-tracking branch 'origin/master' into feat/mars-hermes-mnemosyne
# Conflicts: # README.md # hosts/jupiter/secrets.nix # services/dev/gitea-hermes-webhook-relay.nix # services/dev/gitea.nix
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
../../services/network/caddy.nix
|
||||
../../services/vpn/tailscale.nix
|
||||
../../services/monitoring/node-exporter.nix
|
||||
../../services/monitoring/victoriametrics.nix
|
||||
../../services/media/jellyfin.nix
|
||||
../../services/media/sabnzbd.nix
|
||||
../../services/media/prowlarr.nix
|
||||
@@ -23,6 +24,7 @@
|
||||
../../services/media/seerr.nix
|
||||
../../services/media/immich.nix
|
||||
../../services/dev/gitea.nix
|
||||
../../services/dev/obsidian-livesync.nix
|
||||
];
|
||||
|
||||
# sabnzbd's unrar dependency is unfree; scope the allowance to just that
|
||||
@@ -38,16 +40,11 @@
|
||||
# systemd-boot for UEFI. If ZimaBlade boots legacy/BIOS, switch to grub.
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
# common.nix's cap of 5 comes from this box's own 34-generation incident,
|
||||
# but at ~5G free on a 29G eMMC even 5 is too many — override down to 2.
|
||||
# common.nix's default of 5 is still too many boot entries for a 29G eMMC — override down to 2.
|
||||
boot.loader.systemd-boot.configurationLimit = lib.mkForce 2;
|
||||
|
||||
# A `switch` pins the old generation as a GC root until the box reboots onto
|
||||
# the new one (booted-system vs current-system) — common.nix's nix.gc is
|
||||
# weekly, far too slow to catch that on a 29G eMMC. 2026-08-19: one switch
|
||||
# alone took 14G -> 19G used; only reboot (releases the old root) + this GC
|
||||
# brought it back to 14G. Run a full collect right after every boot instead
|
||||
# of waiting on the weekly timer.
|
||||
# A `switch` pins the old generation as a GC root until reboot; common.nix's weekly
|
||||
# nix.gc is too slow for a 29G eMMC, so collect garbage on every boot instead.
|
||||
systemd.services.gc-on-boot = {
|
||||
description = "Full nix-collect-garbage on every boot";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
@@ -68,46 +65,24 @@
|
||||
boot.kernelParams = [ "reboot=pci" ];
|
||||
|
||||
# ---- GPU (jellyfin hardware transcoding) ----
|
||||
# Apollo Lake N3450 / HD Graphics 500 (Gen9, pci 8086:5A85). The i915 KERNEL
|
||||
# driver binds on its own — /dev/dri/{card1,renderD128} exist without this —
|
||||
# but the libva USERSPACE driver only ships when hardware.graphics is on, and
|
||||
# nothing else here pulled it in. Without it VAAPI init fails with "unknown
|
||||
# libva error" and jellyfin-ffmpeg exits 251 on EVERY transcode, which the
|
||||
# client shows as generic playback failure: the server log only says "FFmpeg
|
||||
# exited with code 251", never that a driver is missing. Verified on the box:
|
||||
# the same h264_vaapi encode goes 251 -> 0 once iHD is on LIBVA_DRIVERS_PATH.
|
||||
#
|
||||
# iHD (intel-media-driver) is the right one for Gen9; i965 is for Gen8 and
|
||||
# older. Note the render node is 0666 but card1 is 0660 root:video, so the
|
||||
# group membership in services/media/jellyfin.nix matters for the card node.
|
||||
# Apollo Lake N3450 / HD Graphics 500 (Gen9). i915 binds on its own, but VAAPI needs
|
||||
# the iHD userspace driver (Gen9; i965 is Gen8-only) or jellyfin-ffmpeg exits 251 on
|
||||
# every transcode with no clearer error than "FFmpeg exited with code 251" in the log.
|
||||
hardware.graphics = {
|
||||
enable = true;
|
||||
extraPackages = [ pkgs.intel-media-driver ];
|
||||
};
|
||||
# ⚠️ This buys VAAPI only — jellyfin must be set to VAAPI, NOT QSV, in its
|
||||
# web UI (Dashboard -> Playback -> Transcoding). QSV needs an MFX runtime on
|
||||
# top of the libva driver: ffmpeg's `-init_hw_device qsv=qs@va` dies with
|
||||
# "Error creating a MFX session: -9" -> exit 171, the SECOND failure hiding
|
||||
# behind the first (fixing the missing driver only moved 251 -> 171).
|
||||
# There is no good way to provide it here: vpl-gpu-rt is Gen12+, and the
|
||||
# Gen9 runtime `intel-media-sdk` is marked INSECURE in nixpkgs (EOL, 5 CVEs
|
||||
# incl. local privilege escalation) — not worth it when VAAPI does the same
|
||||
# job on this chip at ~3.5x realtime for 1080p->720p.
|
||||
#
|
||||
# Also: 4K HDR (the 2160p HEVC/DV remuxes) can NOT be tone-mapped here.
|
||||
# tonemap_opencl needs OpenCL, which has no platform on this box, and
|
||||
# tonemap_vaapi is Gen11+ — both fail. Only a plain scale_vaapi=format=nv12
|
||||
# succeeds, which drops HDR without tone-mapping (washed-out picture).
|
||||
# Those files need to direct-play, or be kept as 1080p SDR versions.
|
||||
# ⚠️ Use VAAPI, not QSV, in jellyfin's UI — QSV needs an MFX runtime not safely
|
||||
# available for this Gen9 chip (only insecure/EOL options) and fails with exit 171.
|
||||
# 4K HDR remuxes also can't be tone-mapped here (needs OpenCL or Gen11+); keep those
|
||||
# as 1080p SDR or let them direct-play.
|
||||
|
||||
# ---- NAS data array ----
|
||||
# Existing ext4 on the mdadm RAID0 over sda+sdb (md0, 29.1T).
|
||||
# Mounted, NOT formatted; kept out of disko so it is never wiped.
|
||||
# ⚠️ RAID0 = no redundancy: either 16TB disk failing loses ALL data.
|
||||
boot.swraid.enable = true; # assemble the mdadm array at boot
|
||||
# Silences "mdmon service will crash" eval warning. RAID0 here uses native
|
||||
# superblocks so mdmon (external-metadata arrays only) never actually runs,
|
||||
# but the module warns unconditionally without SOME MAILADDR/PROGRAM set.
|
||||
# Existing ext4 on mdadm RAID0 (sda+sdb, md0, 29.1T) — mounted, not formatted, kept
|
||||
# out of disko. ⚠️ RAID0 has no redundancy: either disk failing loses ALL data.
|
||||
boot.swraid.enable = true;
|
||||
# Silences the "mdmon service will crash" eval warning — mdmon never actually runs
|
||||
# here (native superblocks, not external-metadata) but the module warns regardless.
|
||||
boot.swraid.mdadmConf = "MAILADDR root";
|
||||
fileSystems."/mnt/data" = {
|
||||
# fs UUID (stable) — the array may enumerate as /dev/md127, so avoid /dev/md0.
|
||||
@@ -116,69 +91,48 @@
|
||||
options = [ "nofail" ]; # don't block boot if the array is degraded/absent
|
||||
};
|
||||
|
||||
# `nofail` above is necessary but NOT sufficient — any mount layered on the
|
||||
# array (prowlarr/seerr binds) is RequiredBy local-fs.target and will fail it
|
||||
# regardless, and emergency mode on this box is a dead end: root is locked, so
|
||||
# sulogin drops you at a prompt you cannot answer, with no ssh. 2026-08-06: a
|
||||
# drive that failed to enumerate after the rack move did exactly this —
|
||||
# "Timed out waiting for device /dev/disk/by-uuid/dadbff6f-…" -> Dependency
|
||||
# failed for Local File Systems -> Reached target Emergency Mode, twice.
|
||||
# Boot as far as possible instead and leave the failed units to be read over
|
||||
# ssh. The array-backed services carry RequiresMountsFor=/mnt/data so they
|
||||
# still refuse to start rather than writing to the eMMC.
|
||||
# `nofail` alone isn't enough — mounts layered on the array (prowlarr/seerr binds)
|
||||
# are RequiredBy local-fs.target and can still trip Emergency Mode, which is a dead
|
||||
# end here (root locked, no ssh). Boot as far as possible instead; the array-backed
|
||||
# services carry RequiresMountsFor=/mnt/data so they still won't write to the eMMC.
|
||||
systemd.enableEmergencyMode = false;
|
||||
|
||||
# ---- Heavy state moved off the eMMC ----
|
||||
# A deploy holds TWO full closures (~9G each) on a 29G disk at once, so the
|
||||
# OS disk has no room for state that grows on its own. 2026-08-09: it hit 0
|
||||
# bytes free with both gen 39 and gen 40 resident, and postgres died on
|
||||
# "No space left on device" — note ext4 reserves 5% for root, so non-root
|
||||
# services see zero while df still shows ~300M free.
|
||||
#
|
||||
# Paths live under /mnt/data/AppData like every other service's state. Both
|
||||
# settings below are jupiter-only on purpose: services/containers.nix stays
|
||||
# engine- and host-agnostic (mercury runs pihole on podman with no array).
|
||||
# A deploy holds two full closures (~9G each) on this 29G disk at once, so state
|
||||
# that grows on its own can't live there — moved under /mnt/data/AppData like every
|
||||
# other service's state. Settings below are jupiter-only; services/containers.nix
|
||||
# stays engine/host-agnostic (mercury runs podman with no array).
|
||||
|
||||
# podman: CI images dominate and keep growing — the gitea runner's
|
||||
# act-latest is 1.7G, and the act-22.04 label in services/dev/gitea.nix
|
||||
# pulls another ~1.7G the first time a job requests it.
|
||||
# runroot stays on /run: it is per-boot tmpfs state, not a growing store.
|
||||
# runroot stays on /run (per-boot tmpfs, doesn't grow); graphroot moves to the array
|
||||
# since the gitea runner's CI images alone run several GB.
|
||||
virtualisation.containers.storage.settings.storage = {
|
||||
driver = "overlay";
|
||||
graphroot = "/mnt/data/AppData/containers/storage";
|
||||
runroot = "/run/containers/storage";
|
||||
};
|
||||
|
||||
# immich's postgres cluster. Version component mirrors the upstream default
|
||||
# (`/var/lib/postgresql/${psqlSchema}`) so a major bump gets its own dir
|
||||
# instead of silently reusing the old cluster's files.
|
||||
# ⚠️ This puts the DB in the SAME failure domain as the photos it indexes:
|
||||
# /mnt/data is RAID0, so either 16TB disk now loses both, where before an
|
||||
# eMMC failure and an array failure each took only one. Chosen deliberately
|
||||
# — the two are useless apart — but neither is backed up.
|
||||
# immich's postgres cluster. Version-qualified path (matches upstream default) so a
|
||||
# major bump gets a fresh dir instead of reusing the old cluster's files.
|
||||
# ⚠️ Puts the DB in the same RAID0 failure domain as the photos it indexes —
|
||||
# deliberate (the two are useless apart) but neither is backed up.
|
||||
services.postgresql.dataDir =
|
||||
"/mnt/data/AppData/postgresql/${config.services.postgresql.package.psqlSchema}";
|
||||
|
||||
# /mnt/data/AppData is drwx--x--- darman:users, so postgres needs group
|
||||
# "users" just to TRAVERSE into its own dataDir — exactly the reason immich
|
||||
# has the same line. The cluster dir itself keeps the mode it was initdb'd
|
||||
# with (0750 postgres:postgres) — postgres only accepts 0700, or 0750 when
|
||||
# the cluster was created with group access, and refuses to start otherwise.
|
||||
# /mnt/data/AppData is drwx--x--- darman:users, so postgres needs the "users" group
|
||||
# just to traverse into its dataDir (same reason immich needs it) — postgres itself
|
||||
# refuses to start unless the cluster dir is 0700 or 0750.
|
||||
users.users.postgres.extraGroups = [ "users" ];
|
||||
|
||||
# Neither path is under /var/lib, so no module creates it: the postgresql
|
||||
# module's own tmpfiles entry only adjusts a dataDir that already exists,
|
||||
# the same way immich's mediaLocation rule does.
|
||||
# Neither path is under /var/lib, so no module creates it automatically — same
|
||||
# reason immich needs its own mediaLocation tmpfiles rule.
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /mnt/data/AppData/postgresql 0750 postgres postgres -"
|
||||
"d /mnt/data/AppData/containers 0700 root root -"
|
||||
];
|
||||
|
||||
# graphroot is not a systemd path dependency the way dataDir is, so nothing
|
||||
# derives a mount ordering from it. Without these, podman would recreate an
|
||||
# empty store on the eMMC under the mountpoint when the array is late or
|
||||
# absent, and the runner would re-pull every image into it.
|
||||
# (podman-clonarr already carries this from services/media/clonarr.nix.)
|
||||
# Without this, podman would recreate an empty store on the eMMC if the array mounts
|
||||
# late or is absent, and the runner would re-pull every image.
|
||||
# (podman-clonarr already sets this in services/media/clonarr.nix.)
|
||||
systemd.services.podman.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
|
||||
systemd.services.gitea-runner-jupiter.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
|
||||
|
||||
|
||||
+28
-19
@@ -1,14 +1,8 @@
|
||||
{ config, ... }:
|
||||
|
||||
# sops-nix secret wiring (real host only; not imported by vm.nix).
|
||||
# Encrypted values live in ../../secrets/jupiter.yaml, decrypted at activation to
|
||||
# /run/secrets/<name>.
|
||||
#
|
||||
# The host decrypts with its OWN SSH host key (age identity derived via
|
||||
# ssh-to-age, recipient listed in ../../.sops.yaml). The key is pre-generated on
|
||||
# the laptop and shipped once at install as /etc/ssh/ssh_host_ed25519_key
|
||||
# (nixos-anywhere --extra-files) — so decryption works on boot #1 and there is
|
||||
# no separate sops-only key to manage.
|
||||
# sops-nix secret wiring (real host only; not imported by vm.nix). Decrypts with the
|
||||
# host's own SSH host key (ssh-to-age), shipped once at install via nixos-anywhere
|
||||
# --extra-files, so there's no separate sops-only key to manage.
|
||||
{
|
||||
sops.defaultSopsFile = ../../secrets/jupiter.yaml;
|
||||
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||
@@ -26,18 +20,14 @@
|
||||
# Headscale pre-auth key for tailscale auto-registration (see configuration.nix).
|
||||
sops.secrets.tailscale_authkey = { };
|
||||
|
||||
# Immich's OIDC client secret, from its Authentik application (a SEPARATE
|
||||
# app from headscale's and headplane's — see hosts/neptun/secrets.nix).
|
||||
# Referenced as settings.oauth.clientSecret._secret in
|
||||
# services/media/immich.nix; the module resolves it through systemd
|
||||
# LoadCredential, which reads as root before dropping privileges, so the
|
||||
# sops default of root:root 0400 is correct — do NOT set `owner`.
|
||||
# Immich's OIDC client secret (separate Authentik app from headscale/headplane, see
|
||||
# hosts/neptun/secrets.nix). Resolved via systemd LoadCredential as root before
|
||||
# privilege drop, so sops's default root:root 0400 is correct — do NOT set `owner`.
|
||||
sops.secrets.immich_oauth_client_secret = { };
|
||||
|
||||
# Gitea Actions runner registration token (services/dev/gitea.nix). Gitea
|
||||
# generates this itself once Actions is enabled — it is not a password
|
||||
# chosen up front. Rendered into a `TOKEN=...` env file because
|
||||
# gitea-actions-runner takes an EnvironmentFile, not a raw secret path.
|
||||
# Gitea Actions runner registration token — gitea generates this itself once Actions
|
||||
# is enabled. Rendered into an env file since gitea-actions-runner takes an
|
||||
# EnvironmentFile, not a raw secret path.
|
||||
sops.secrets.gitea_runner_token = { };
|
||||
sops.templates."gitea-runner.env".content =
|
||||
"TOKEN=${config.sops.placeholder.gitea_runner_token}";
|
||||
@@ -48,6 +38,11 @@
|
||||
# ci-bot access token to allow the ci-bot user to push to repos
|
||||
sops.secrets.gitea_ci_bot_token.owner = "gitea";
|
||||
|
||||
# Add the same value to secrets/jupiter.yaml before deploying Jupiter.
|
||||
sops.secrets.gitea_hermes_webhook_secret = {
|
||||
owner = "gitea";
|
||||
};
|
||||
|
||||
# SABnzbd credentials (web UI login, API keys, eweka.nl usenet server) —
|
||||
# migrated off the reused ini in services/media/sabnzbd.nix into
|
||||
# services.sabnzbd.settings + secretValues. sabnzbd_api_key predates this
|
||||
@@ -64,4 +59,18 @@
|
||||
sops.secrets.sabnzbd_eweka_username.owner = "sabnzbd";
|
||||
sops.secrets.sabnzbd_eweka_password.owner = "sabnzbd";
|
||||
|
||||
# CouchDB admin account for Obsidian LiveSync — rendered into an [admins] ini
|
||||
# fragment instead of services.couchdb.adminPass, which would put the plaintext in
|
||||
# the world-readable store.
|
||||
# owner = couchdb on both: couchdb re-reads the ini as its own user after privilege
|
||||
# drop, and without this sops's default root:root 0400 leaves it with no admin
|
||||
# configured (every request 401s).
|
||||
sops.secrets.couchdb_admin_password.owner = "couchdb";
|
||||
sops.templates."couchdb-admins.ini" = {
|
||||
owner = "couchdb";
|
||||
content = ''
|
||||
[admins]
|
||||
obsidian = ${config.sops.placeholder.couchdb_admin_password}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user