From 08e8b5edbe4ce03114153a60a12bdd5d35f419ed Mon Sep 17 00:00:00 2001 From: Aner Zakobar Date: Wed, 20 May 2026 23:21:36 +0300 Subject: [PATCH] REWRITE --- modules/backup.nix | 39 ++--- modules/caddy.nix | 274 ++++++------------------------ modules/monitoring.nix | 26 ++- modules/services/authelia.nix | 24 ++- modules/services/eurovote.nix | 30 +++- modules/services/gitea-runner.nix | 2 +- modules/services/gitea.nix | 24 ++- modules/services/jellyfin.nix | 24 ++- modules/services/mealie.nix | 24 ++- modules/services/nextcloud.nix | 50 +++++- modules/services/ntfy.nix | 50 +++--- modules/services/openldap.nix | 16 +- modules/services/paperless.nix | 28 ++- modules/services/phpldapadmin.nix | 13 +- modules/services/transmission.nix | 24 ++- modules/services/uptime-kuma.nix | 23 ++- modules/storage.nix | 70 ++++---- 17 files changed, 419 insertions(+), 322 deletions(-) diff --git a/modules/backup.nix b/modules/backup.nix index d0fb14d..3b904a2 100644 --- a/modules/backup.nix +++ b/modules/backup.nix @@ -42,6 +42,18 @@ in options.homey.backup = { enable = lib.mkEnableOption "Restic backup jobs"; + extraPaths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "Paths to include in the restic backup. Each service module contributes its own entries."; + }; + + extraExcludePaths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "Paths to exclude from the restic backup. Each service module contributes its own entries."; + }; + repository = lib.mkOption { type = lib.types.str; example = "sftp:user@nas.local:/backups/homey"; @@ -127,32 +139,15 @@ in # Runtime env file written by homey-backup-pre.service (which runs first) environmentFile = "/run/restic-homey-secrets.env"; - paths = [ - "${dataDir}/openldap" - "${dataDir}/authelia" - "${dataDir}/gitea" - "${dataDir}/nextcloud" - # media and transmission config included when those services are enabled: - "${dataDir}/jellyfin" - "${dataDir}/transmission" - # Deliberately excluded: media/* (large, can be re-downloaded) - # Monitoring — uptime-kuma has monitors/history, ntfy has user accounts - "${dataDir}/uptime-kuma" - "${dataDir}/ntfy" - # Eurovision Vote — SQLite DB with votes and rankings - "/var/lib/eurovote" - "${dataDir}/paperless" - "${dataDir}/mealie" - ]; + # Paths are contributed by individual service modules via homey.backup.extraPaths. + paths = config.homey.backup.extraPaths; - # Exclude Nextcloud's raw DB directory in favour of the pg_dump file exclude = [ - "${dataDir}/nextcloud/db" + # restic's own local cache is never worth backing up "${dataDir}/restic-cache" + # media is large and can be re-downloaded; services exclude their own consume dirs "${dataDir}/media" - # consume dir holds unprocessed drop files; usually empty after ingestion - "${dataDir}/paperless/consume" - ]; + ] ++ config.homey.backup.extraExcludePaths; timerConfig = { OnCalendar = cfg.schedule; diff --git a/modules/caddy.nix b/modules/caddy.nix index 6bb9019..536532b 100644 --- a/modules/caddy.nix +++ b/modules/caddy.nix @@ -65,6 +65,38 @@ in default = "admin@zakobar.com"; description = "Email for Let's Encrypt ACME registration."; }; + + virtualHosts = lib.mkOption { + type = lib.types.listOf (lib.types.submodule { + options = { + subdomain = lib.mkOption { + type = lib.types.str; + description = "Subdomain under homeyConfig.domain (e.g. \"mealie\" → mealie.zakobar.com)."; + }; + port = lib.mkOption { + type = lib.types.port; + description = "Host port to reverse-proxy to."; + }; + auth = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Prepend Authelia forward_auth to this vhost."; + }; + extraConfig = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Replaces the auto-generated 'reverse_proxy localhost:' for HTTPS. Empty = use default."; + }; + extraHttpConfig = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Replaces the auto-generated cfProxy for the HTTP loopback vhost. Empty = use default."; + }; + }; + }); + default = []; + description = "Virtual hosts to generate. Each service module contributes its own entries."; + }; }; config = lib.mkIf cfg.enable { @@ -89,229 +121,27 @@ in acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN} ''; - # Each virtual host. + # Each virtual host is generated from homey.caddy.virtualHosts entries. + # Each service module contributes its own entries to that list. # - # Each service gets two vhost entries: - # - "host" (no scheme) → Caddy handles HTTPS + auto cert (for LAN access) - # - "http://host" → plain HTTP for cloudflared on loopback (no redirect) - # - # Caddy auto-redirects HTTP→HTTPS only when no explicit http:// vhost exists. - # By defining http:// explicitly we suppress that redirect so cloudflared - # (which talks plain HTTP on port 80) gets a direct response. - virtualHosts = { - - # ------------------------------------------------------------------ - # Authelia — public, no auth gate (it IS the auth gate) - # ------------------------------------------------------------------ - "auth.${domain}" = { - extraConfig = '' - reverse_proxy localhost:9091 - ''; - }; - "http://auth.${domain}" = { - extraConfig = cfProxy 9091; - }; - - # ------------------------------------------------------------------ - # Gitea — no forward_auth; git HTTP clients can't handle SSO redirects. - # Access control is handled by Gitea itself (LDAP auth + private repos). - # ------------------------------------------------------------------ - "git.${domain}" = { - extraConfig = '' - reverse_proxy localhost:3000 - ''; - }; - "http://git.${domain}" = { - extraConfig = cfProxy 3000; - }; - - # ------------------------------------------------------------------ - # Nextcloud — public auth (Nextcloud manages its own users + LDAP) - # ------------------------------------------------------------------ - "nextcloud.${domain}" = { - extraConfig = '' - # Redirect CardDAV/CalDAV discovery - redir /.well-known/carddav /remote.php/dav/ 301 - redir /.well-known/caldav /remote.php/dav/ 301 - - # Large uploads (5 GB) - request_body { - max_size 5GB - } - - reverse_proxy localhost:8080 { - header_up X-Forwarded-For {remote_host} - } - ''; - }; - "http://nextcloud.${domain}" = { - extraConfig = '' - redir /.well-known/carddav /remote.php/dav/ 301 - redir /.well-known/caldav /remote.php/dav/ 301 - request_body { - max_size 5GB - } - reverse_proxy localhost:8080 { - header_up X-Forwarded-Proto https - header_up X-Forwarded-For {remote_host} - } - ''; - }; - - # ------------------------------------------------------------------ - # phpLDAPadmin — two_factor, admins only (enforced by authelia policy) - # ------------------------------------------------------------------ - "ldapadmin.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - reverse_proxy localhost:8081 - ''; - }; - "http://ldapadmin.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - ${cfProxy 8081} - ''; - }; - - # ------------------------------------------------------------------ - # Jellyfin — no forward_auth; Jellyfin has its own login UI and - # native app clients can't handle SSO redirects. - # ------------------------------------------------------------------ - "jellyfin.${domain}" = { - extraConfig = '' - reverse_proxy localhost:8096 - ''; - }; - "http://jellyfin.${domain}" = { - extraConfig = cfProxy 8096; - }; - - # ------------------------------------------------------------------ - # Transmission — two_factor, admins only (enforced by authelia policy) - # ------------------------------------------------------------------ - "torrent.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - reverse_proxy localhost:9092 - ''; - # NOTE: transmission is bound to 9092 to avoid clash with authelia on 9091. - }; - "http://torrent.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - ${cfProxy 9092} - ''; - }; - - # ------------------------------------------------------------------ - # Uptime Kuma — two_factor, admins only (enforced by authelia policy) - # ------------------------------------------------------------------ - "uptime.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - reverse_proxy localhost:3001 - ''; - }; - "http://uptime.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - ${cfProxy 3001} - ''; - }; - - # ------------------------------------------------------------------ - # Ntfy — no forward_auth; ntfy has its own token/password auth so the - # mobile app can connect without Authelia SSO complications. - # ------------------------------------------------------------------ - "ntfy.${domain}" = { - extraConfig = '' - reverse_proxy localhost:2586 - ''; - }; - "http://ntfy.${domain}" = { - extraConfig = cfProxy 2586; - }; - - # ------------------------------------------------------------------ - # Eurovision Vote — one_factor for all authenticated users. - # /admin/* is restricted to group:admins by Authelia access_control. - # Caddy passes Remote-User → X-Remote-User so Django auto-logs in - # the SSO-authenticated user via RemoteUserMiddleware. - # ------------------------------------------------------------------ - "eurovision-vote.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - reverse_proxy localhost:8007 { - header_up X-Remote-User {http.request.header.Remote-User} - } - ''; - }; - "http://eurovision-vote.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - reverse_proxy localhost:8007 { - header_up X-Forwarded-Proto https - header_up X-Remote-User {http.request.header.Remote-User} - } - ''; - }; - - # ------------------------------------------------------------------ - # Paperless — one_factor for all authenticated users (authelia policy). - # Authelia sets Remote-User; Caddy copies it to the upstream request; - # Paperless trusts HTTP_REMOTE_USER for automatic login (no separate - # Paperless login page shown). - # ------------------------------------------------------------------ - "paperless.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - reverse_proxy localhost:8083 - ''; - }; - "http://paperless.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - ${cfProxy 8083} - ''; - }; - - # ------------------------------------------------------------------ - # Mealie — no forward_auth; LDAP handles auth via Mealie's login page. - # ------------------------------------------------------------------ - "mealie.${domain}" = { - extraConfig = '' - reverse_proxy localhost:9093 - ''; - }; - "http://mealie.${domain}" = { - extraConfig = cfProxy 9093; - }; - - # ------------------------------------------------------------------ - # Grafana — two_factor, admins only (enforced by authelia policy). - # After Authelia verifies the user, Caddy maps the Remote-User header - # to X-WEBAUTH-USER so Grafana's proxy auth auto-signs the user in. - # ------------------------------------------------------------------ - "grafana.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - reverse_proxy localhost:3002 { - header_up X-WEBAUTH-USER {http.request.header.Remote-User} - } - ''; - }; - "http://grafana.${domain}" = { - extraConfig = '' - ${autheliaForwardAuth} - reverse_proxy localhost:3002 { - header_up X-Forwarded-Proto https - header_up X-WEBAUTH-USER {http.request.header.Remote-User} - } - ''; - }; - - }; + # Each entry produces two Caddy vhosts: + # - "subdomain.domain" → HTTPS (LAN access + Let's Encrypt cert) + # - "http://subdomain.domain" → plain HTTP for cloudflared loopback + virtualHosts = lib.listToAttrs ( + lib.concatMap (vh: + let + d = "${vh.subdomain}.${domain}"; + authSnip = lib.optionalString vh.auth autheliaForwardAuth; + httpsBody = if vh.extraConfig != "" then vh.extraConfig + else "reverse_proxy localhost:${toString vh.port}\n"; + httpBody = if vh.extraHttpConfig != "" then vh.extraHttpConfig + else cfProxy vh.port; + in [ + { name = d; value.extraConfig = "${authSnip}${httpsBody}"; } + { name = "http://${d}"; value.extraConfig = "${authSnip}${httpBody}"; } + ] + ) cfg.virtualHosts + ); }; # ----------------------------------------------------------------------- diff --git a/modules/monitoring.nix b/modules/monitoring.nix index 85c628a..ed06475 100644 --- a/modules/monitoring.nix +++ b/modules/monitoring.nix @@ -38,7 +38,7 @@ let in { options.homey.monitoring = { - enable = lib.mkEnableOption "Prometheus + Grafana monitoring stack"; + enable = lib.mkEnableOption "Prometheus + Grafana monitoring stack" // { default = true; }; prometheusPort = lib.mkOption { type = lib.types.port; @@ -205,6 +205,30 @@ in mode = "0444"; }; + # ----------------------------------------------------------------------- + # Caddy virtual host — forward_auth; Caddy maps Remote-User → X-WEBAUTH-USER + # so Grafana's proxy auth auto-signs the user in + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "grafana"; + port = cfg.grafanaPort; + auth = true; + extraConfig = '' + reverse_proxy localhost:${toString cfg.grafanaPort} { + header_up X-WEBAUTH-USER {http.request.header.Remote-User} + } + ''; + extraHttpConfig = '' + reverse_proxy localhost:${toString cfg.grafanaPort} { + header_up X-Forwarded-Proto https + header_up X-WEBAUTH-USER {http.request.header.Remote-User} + } + ''; + }]; + + # Grafana and Prometheus use system state dirs (/var/lib/grafana, + # /var/lib/prometheus2) — no extraDirs or backup entries needed. + # ----------------------------------------------------------------------- # Uptime Kuma monitor for Grafana # ----------------------------------------------------------------------- diff --git a/modules/services/authelia.nix b/modules/services/authelia.nix index c4c73b7..ed5cafc 100644 --- a/modules/services/authelia.nix +++ b/modules/services/authelia.nix @@ -163,7 +163,7 @@ let in { options.homey.authelia = { - enable = lib.mkEnableOption "Authelia SSO gateway"; + enable = lib.mkEnableOption "Authelia SSO gateway" // { default = true; }; image = lib.mkOption { type = lib.types.str; @@ -241,6 +241,28 @@ in requires = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" "podman-homey-network.service" ]; }; + # ----------------------------------------------------------------------- + # Caddy virtual host — no forward_auth (Authelia IS the auth gateway) + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "auth"; + port = cfg.port; + auth = false; + }]; + + # ----------------------------------------------------------------------- + # Storage directories + # ----------------------------------------------------------------------- + homey.storage.extraDirs = [ + { path = "authelia"; } + { path = "authelia/config"; } + ]; + + # ----------------------------------------------------------------------- + # Backup + # ----------------------------------------------------------------------- + homey.backup.extraPaths = [ "${dataDir}/authelia" ]; + # ----------------------------------------------------------------------- # Uptime Kuma monitor for this service # ----------------------------------------------------------------------- diff --git a/modules/services/eurovote.nix b/modules/services/eurovote.nix index 8d96c82..200b931 100644 --- a/modules/services/eurovote.nix +++ b/modules/services/eurovote.nix @@ -23,7 +23,7 @@ let in { options.homey.eurovote = { - enable = lib.mkEnableOption "Eurovision Vote app"; + enable = lib.mkEnableOption "Eurovision Vote app" // { default = true; }; }; config = lib.mkIf cfg.enable { @@ -48,6 +48,34 @@ in logoutRedirectUrl = "https://auth.${domain}/logout"; }; + # ----------------------------------------------------------------------- + # Caddy virtual host — forward_auth; X-Remote-User passed to Django's + # RemoteUserMiddleware for automatic SSO login + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "eurovision-vote"; + port = 8007; + auth = true; + extraConfig = '' + reverse_proxy localhost:8007 { + header_up X-Remote-User {http.request.header.Remote-User} + } + ''; + extraHttpConfig = '' + reverse_proxy localhost:8007 { + header_up X-Forwarded-Proto https + header_up X-Remote-User {http.request.header.Remote-User} + } + ''; + }]; + + # Eurovision Vote uses DynamicUser + /var/lib/eurovote — no extraDirs needed. + + # ----------------------------------------------------------------------- + # Backup — /var/lib/eurovote holds the SQLite DB with votes + # ----------------------------------------------------------------------- + homey.backup.extraPaths = [ "/var/lib/eurovote" ]; + # ----------------------------------------------------------------------- # Uptime Kuma monitor # ----------------------------------------------------------------------- diff --git a/modules/services/gitea-runner.nix b/modules/services/gitea-runner.nix index 6f2589f..8bb86d9 100644 --- a/modules/services/gitea-runner.nix +++ b/modules/services/gitea-runner.nix @@ -30,7 +30,7 @@ let in { options.homey.giteaRunner = { - enable = lib.mkEnableOption "Gitea Actions runner"; + enable = lib.mkEnableOption "Gitea Actions runner" // { default = true; }; name = lib.mkOption { type = lib.types.str; diff --git a/modules/services/gitea.nix b/modules/services/gitea.nix index c025ff3..57b0054 100644 --- a/modules/services/gitea.nix +++ b/modules/services/gitea.nix @@ -32,7 +32,7 @@ let in { options.homey.gitea = { - enable = lib.mkEnableOption "Gitea Git server"; + enable = lib.mkEnableOption "Gitea Git server" // { default = true; }; image = lib.mkOption { type = lib.types.str; @@ -188,6 +188,28 @@ in requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; + # ----------------------------------------------------------------------- + # Caddy virtual host — no forward_auth; git clients can't handle SSO redirects + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "git"; + port = cfg.port; + auth = false; + }]; + + # ----------------------------------------------------------------------- + # Storage directories (UID 1000 = Gitea's internal user) + # ----------------------------------------------------------------------- + homey.storage.extraDirs = [ + { path = "gitea"; user = "1000"; group = "1000"; } + { path = "gitea/data"; user = "1000"; group = "1000"; } + ]; + + # ----------------------------------------------------------------------- + # Backup + # ----------------------------------------------------------------------- + homey.backup.extraPaths = [ "${dataDir}/gitea" ]; + # ----------------------------------------------------------------------- # Uptime Kuma monitor for this service # ----------------------------------------------------------------------- diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix index aea74c1..acd3270 100644 --- a/modules/services/jellyfin.nix +++ b/modules/services/jellyfin.nix @@ -14,7 +14,7 @@ let in { options.homey.jellyfin = { - enable = lib.mkEnableOption "Jellyfin media server"; + enable = lib.mkEnableOption "Jellyfin media server" // { default = true; }; image = lib.mkOption { type = lib.types.str; @@ -51,5 +51,27 @@ in after = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; + + # ----------------------------------------------------------------------- + # Caddy virtual host — no forward_auth; Jellyfin has its own login UI + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "jellyfin"; + port = cfg.port; + auth = false; + }]; + + # ----------------------------------------------------------------------- + # Storage directories + # ----------------------------------------------------------------------- + homey.storage.extraDirs = [ + { path = "jellyfin"; } + { path = "jellyfin/config"; } + ]; + + # ----------------------------------------------------------------------- + # Backup + # ----------------------------------------------------------------------- + homey.backup.extraPaths = [ "${dataDir}/jellyfin" ]; }; } diff --git a/modules/services/mealie.nix b/modules/services/mealie.nix index 54dcff0..c4ab44b 100644 --- a/modules/services/mealie.nix +++ b/modules/services/mealie.nix @@ -23,7 +23,7 @@ let in { options.homey.mealie = { - enable = lib.mkEnableOption "Mealie recipe manager"; + enable = lib.mkEnableOption "Mealie recipe manager" // { default = true; }; image = lib.mkOption { type = lib.types.str; @@ -96,6 +96,28 @@ in requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; + # ----------------------------------------------------------------------- + # Caddy virtual host — no forward_auth; Mealie uses LDAP login page + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "mealie"; + port = cfg.port; + auth = false; + }]; + + # ----------------------------------------------------------------------- + # Storage directories + # ----------------------------------------------------------------------- + homey.storage.extraDirs = [ + { path = "mealie"; } + { path = "mealie/data"; mode = "0755"; } + ]; + + # ----------------------------------------------------------------------- + # Backup + # ----------------------------------------------------------------------- + homey.backup.extraPaths = [ "${dataDir}/mealie" ]; + # ----------------------------------------------------------------------- # Uptime Kuma monitor # ----------------------------------------------------------------------- diff --git a/modules/services/nextcloud.nix b/modules/services/nextcloud.nix index ab1f2f8..d9df1b6 100644 --- a/modules/services/nextcloud.nix +++ b/modules/services/nextcloud.nix @@ -52,7 +52,7 @@ let in { options.homey.nextcloud = { - enable = lib.mkEnableOption "Nextcloud file server"; + enable = lib.mkEnableOption "Nextcloud file server" // { default = true; }; image = lib.mkOption { type = lib.types.str; @@ -166,6 +166,54 @@ in ]; }; + # ----------------------------------------------------------------------- + # Caddy virtual host — no forward_auth; Nextcloud manages its own auth + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "nextcloud"; + port = cfg.port; + auth = false; + extraConfig = '' + redir /.well-known/carddav /remote.php/dav/ 301 + redir /.well-known/caldav /remote.php/dav/ 301 + request_body { + max_size 5GB + } + reverse_proxy localhost:${toString cfg.port} { + header_up X-Forwarded-For {remote_host} + } + ''; + extraHttpConfig = '' + redir /.well-known/carddav /remote.php/dav/ 301 + redir /.well-known/caldav /remote.php/dav/ 301 + request_body { + max_size 5GB + } + reverse_proxy localhost:${toString cfg.port} { + header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} + } + ''; + }]; + + # ----------------------------------------------------------------------- + # Storage directories + # UID 33 = www-data in the Nextcloud container + # UID 999 = postgres — must own the db dir (creates files directly in it) + # ----------------------------------------------------------------------- + homey.storage.extraDirs = [ + { path = "nextcloud"; } + { path = "nextcloud/html"; user = "33"; group = "33"; } + { path = "nextcloud/db"; mode = "0700"; user = "999"; group = "999"; } + { path = "nextcloud/db-dump"; mode = "0700"; } + ]; + + # ----------------------------------------------------------------------- + # Backup — exclude raw DB dir (pg_dump file in db-dump/ is used instead) + # ----------------------------------------------------------------------- + homey.backup.extraPaths = [ "${dataDir}/nextcloud" ]; + homey.backup.extraExcludePaths = [ "${dataDir}/nextcloud/db" ]; + # ----------------------------------------------------------------------- # Uptime Kuma monitor for this service # ----------------------------------------------------------------------- diff --git a/modules/services/ntfy.nix b/modules/services/ntfy.nix index e2c3c35..3c938c1 100644 --- a/modules/services/ntfy.nix +++ b/modules/services/ntfy.nix @@ -62,7 +62,7 @@ let in { options.homey.ntfy = { - enable = lib.mkEnableOption "Ntfy push notification server"; + enable = lib.mkEnableOption "Ntfy push notification server" // { default = true; }; port = lib.mkOption { type = lib.types.port; @@ -105,36 +105,14 @@ in mode = "0444"; }; - # Create ntfy data directories on the external HD before ntfy starts. - # Runs as a separate root service (outside ntfy-sh's restricted namespace) - # so it can access /mnt/data without hitting ReadWritePaths restrictions. - systemd.services.ntfy-sh-mkdir = { - description = "Create Ntfy data directories on external HD"; - wantedBy = [ "ntfy-sh.service" ]; - before = [ "ntfy-sh.service" ]; - after = [ "mnt-data.mount" ]; - requires = [ "mnt-data.mount" ]; - - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStart = pkgs.writeShellScript "ntfy-mkdir" '' - set -euo pipefail - mkdir -p ${dataDir}/ntfy/attachments - chown -R ntfy-sh:ntfy-sh ${dataDir}/ntfy - chmod 0750 ${dataDir}/ntfy ${dataDir}/ntfy/attachments - ''; - }; - }; - # Ensure ntfy-sh starts after the HD is mounted and dirs are ready. # Widen ReadWritePaths so ntfy-sh can write to the external HD. # Inject the VAPID private key at runtime: ExecStartPre copies the # build-time base config to /run/ntfy-sh/server.yml and appends the key, # then we override ExecStart to use that runtime config file. systemd.services.ntfy-sh = { - after = lib.mkAfter [ "mnt-data.mount" "ntfy-sh-mkdir.service" ]; - requires = lib.mkAfter [ "mnt-data.mount" "ntfy-sh-mkdir.service" ]; + after = lib.mkAfter [ "mnt-data.mount" "systemd-tmpfiles-setup.service" ]; + requires = lib.mkAfter [ "mnt-data.mount" ]; serviceConfig = { ReadWritePaths = lib.mkAfter [ "${dataDir}/ntfy" ]; RuntimeDirectory = "ntfy-sh"; # creates /run/ntfy-sh, owned by ntfy-sh user @@ -198,6 +176,28 @@ in }; }; + # ----------------------------------------------------------------------- + # Caddy virtual host — no forward_auth; ntfy uses its own token auth + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "ntfy"; + port = cfg.port; + auth = false; + }]; + + # ----------------------------------------------------------------------- + # Storage directories (owned by the ntfy-sh system user) + # ----------------------------------------------------------------------- + homey.storage.extraDirs = [ + { path = "ntfy"; user = "ntfy-sh"; group = "ntfy-sh"; } + { path = "ntfy/attachments"; user = "ntfy-sh"; group = "ntfy-sh"; } + ]; + + # ----------------------------------------------------------------------- + # Backup + # ----------------------------------------------------------------------- + homey.backup.extraPaths = [ "${dataDir}/ntfy" ]; + # ----------------------------------------------------------------------- # Uptime Kuma monitor for this service # ----------------------------------------------------------------------- diff --git a/modules/services/openldap.nix b/modules/services/openldap.nix index 1d13dfb..fe04a2d 100644 --- a/modules/services/openldap.nix +++ b/modules/services/openldap.nix @@ -21,7 +21,7 @@ let in { options.homey.openldap = { - enable = lib.mkEnableOption "OpenLDAP identity provider"; + enable = lib.mkEnableOption "OpenLDAP identity provider" // { default = true; }; image = lib.mkOption { type = lib.types.str; @@ -114,6 +114,20 @@ in requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; + # ----------------------------------------------------------------------- + # Storage directories + # ----------------------------------------------------------------------- + homey.storage.extraDirs = [ + { path = "openldap"; } + { path = "openldap/etc-ldap-slapd.d"; } + { path = "openldap/var-lib-ldap"; } + ]; + + # ----------------------------------------------------------------------- + # Backup + # ----------------------------------------------------------------------- + homey.backup.extraPaths = [ "${dataDir}/openldap" ]; + # ----------------------------------------------------------------------- # Firewall — openldap port is NOT opened externally # ----------------------------------------------------------------------- diff --git a/modules/services/paperless.nix b/modules/services/paperless.nix index f9ba02f..a1b9cd4 100644 --- a/modules/services/paperless.nix +++ b/modules/services/paperless.nix @@ -28,7 +28,7 @@ let in { options.homey.paperless = { - enable = lib.mkEnableOption "Paperless-ngx document management"; + enable = lib.mkEnableOption "Paperless-ngx document management" // { default = true; }; image = lib.mkOption { type = lib.types.str; @@ -124,6 +124,32 @@ in requires = lib.mkAfter [ "mnt-data.mount" "podman-paperless-redis.service" "podman-homey-network.service" ]; }; + # ----------------------------------------------------------------------- + # Caddy virtual host — forward_auth; Remote-User passed to Paperless for SSO + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "paperless"; + port = cfg.port; + auth = true; + }]; + + # ----------------------------------------------------------------------- + # Storage directories (UID 1000 = USERMAP_UID in container) + # ----------------------------------------------------------------------- + homey.storage.extraDirs = [ + { path = "paperless"; } + { path = "paperless/data"; user = "1000"; group = "1000"; } + { path = "paperless/media"; user = "1000"; group = "1000"; } + { path = "paperless/consume"; user = "1000"; group = "1000"; } + { path = "paperless/export"; user = "1000"; group = "1000"; } + ]; + + # ----------------------------------------------------------------------- + # Backup — exclude consume dir (unprocessed drops, usually empty) + # ----------------------------------------------------------------------- + homey.backup.extraPaths = [ "${dataDir}/paperless" ]; + homey.backup.extraExcludePaths = [ "${dataDir}/paperless/consume" ]; + # ----------------------------------------------------------------------- # Uptime Kuma monitor # ----------------------------------------------------------------------- diff --git a/modules/services/phpldapadmin.nix b/modules/services/phpldapadmin.nix index 6188b63..61f1b90 100644 --- a/modules/services/phpldapadmin.nix +++ b/modules/services/phpldapadmin.nix @@ -16,7 +16,7 @@ let in { options.homey.phpldapadmin = { - enable = lib.mkEnableOption "phpLDAPadmin web interface"; + enable = lib.mkEnableOption "phpLDAPadmin web interface" // { default = true; }; image = lib.mkOption { type = lib.types.str; @@ -50,6 +50,17 @@ in wants = lib.mkAfter [ "podman-openldap.service" "podman-homey-network.service" ]; }; + # ----------------------------------------------------------------------- + # Caddy virtual host — forward_auth + reverse_proxy + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "ldapadmin"; + port = cfg.port; + auth = true; + }]; + + # phpLDAPadmin is stateless (no persistent volumes) — no storage or backup entries needed. + # ----------------------------------------------------------------------- # Uptime Kuma monitor for this service # ----------------------------------------------------------------------- diff --git a/modules/services/transmission.nix b/modules/services/transmission.nix index 218acb7..d51a6c7 100644 --- a/modules/services/transmission.nix +++ b/modules/services/transmission.nix @@ -18,7 +18,7 @@ let in { options.homey.transmission = { - enable = lib.mkEnableOption "Transmission torrent client"; + enable = lib.mkEnableOption "Transmission torrent client" // { default = true; }; image = lib.mkOption { type = lib.types.str; @@ -60,5 +60,27 @@ in after = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; + + # ----------------------------------------------------------------------- + # Caddy virtual host — forward_auth, admins only + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "torrent"; + port = cfg.port; + auth = true; + }]; + + # ----------------------------------------------------------------------- + # Storage directories + # ----------------------------------------------------------------------- + homey.storage.extraDirs = [ + { path = "transmission"; } + { path = "transmission/config"; } + ]; + + # ----------------------------------------------------------------------- + # Backup + # ----------------------------------------------------------------------- + homey.backup.extraPaths = [ "${dataDir}/transmission" ]; }; } diff --git a/modules/services/uptime-kuma.nix b/modules/services/uptime-kuma.nix index aae647d..b36644a 100644 --- a/modules/services/uptime-kuma.nix +++ b/modules/services/uptime-kuma.nix @@ -215,7 +215,7 @@ in }; options.homey.uptimeKuma = { - enable = lib.mkEnableOption "Uptime Kuma uptime monitoring"; + enable = lib.mkEnableOption "Uptime Kuma uptime monitoring" // { default = true; }; image = lib.mkOption { type = lib.types.str; @@ -285,6 +285,27 @@ in }; }; + # ----------------------------------------------------------------------- + # Caddy virtual host — forward_auth, admins only + # ----------------------------------------------------------------------- + homey.caddy.virtualHosts = [{ + subdomain = "uptime"; + port = cfg.port; + auth = true; + }]; + + # ----------------------------------------------------------------------- + # Storage directories + # ----------------------------------------------------------------------- + homey.storage.extraDirs = [ + { path = "uptime-kuma"; } + ]; + + # ----------------------------------------------------------------------- + # Backup + # ----------------------------------------------------------------------- + homey.backup.extraPaths = [ "${dataDir}/uptime-kuma" ]; + # ----------------------------------------------------------------------- # Uptime Kuma self-monitor # ----------------------------------------------------------------------- diff --git a/modules/storage.nix b/modules/storage.nix index d688af1..59ab83d 100644 --- a/modules/storage.nix +++ b/modules/storage.nix @@ -63,6 +63,22 @@ in default = "ext4"; description = "Filesystem type of the external drive."; }; + + extraDirs = lib.mkOption { + type = lib.types.listOf (lib.types.submodule { + options = { + path = lib.mkOption { + type = lib.types.str; + description = "Path relative to mountPoint (e.g. \"gitea/data\")."; + }; + mode = lib.mkOption { type = lib.types.str; default = "0750"; }; + user = lib.mkOption { type = lib.types.str; default = "root"; }; + group = lib.mkOption { type = lib.types.str; default = "root"; }; + }; + }); + default = []; + description = "Per-service directories to create under mountPoint. Each service module contributes its own entries."; + }; }; config = lib.mkIf (cfg.device != "") { @@ -79,46 +95,20 @@ in ]; }; - # Ensure the mount point directory exists + # Mount point root + shared infrastructure dirs (restic cache, shared media). + # Per-service dirs are contributed via homey.storage.extraDirs by each + # service module, so adding a new service only requires editing that module. systemd.tmpfiles.rules = [ - "d ${cfg.mountPoint} 0755 root root -" - - # Service subdirectories — created on boot so containers can start - # even before any data is restored into them. - "d ${cfg.mountPoint}/openldap 0750 root root -" - "d ${cfg.mountPoint}/openldap/etc-ldap-slapd.d 0750 root root -" - "d ${cfg.mountPoint}/openldap/var-lib-ldap 0750 root root -" - "d ${cfg.mountPoint}/authelia 0750 root root -" - "d ${cfg.mountPoint}/authelia/config 0750 root root -" - "d ${cfg.mountPoint}/gitea 0750 1000 1000 -" - "d ${cfg.mountPoint}/gitea/data 0750 1000 1000 -" - "d ${cfg.mountPoint}/nextcloud 0750 root root -" - # www-data in the Nextcloud container is UID 33; it needs rx on the - # directory and rw on all files it creates inside. - "d ${cfg.mountPoint}/nextcloud/html 0750 33 33 -" - # Postgres (uid 999) must own this directory — it creates files directly in it - "d ${cfg.mountPoint}/nextcloud/db 0700 999 999 -" - "d ${cfg.mountPoint}/jellyfin 0750 root root -" - "d ${cfg.mountPoint}/jellyfin/config 0750 root root -" - "d ${cfg.mountPoint}/media 0755 root root -" - "d ${cfg.mountPoint}/media/movies 0755 root root -" - "d ${cfg.mountPoint}/media/tvshows 0755 root root -" - "d ${cfg.mountPoint}/media/general 0755 root root -" - "d ${cfg.mountPoint}/media/complete 0755 root root -" - "d ${cfg.mountPoint}/transmission 0750 root root -" - "d ${cfg.mountPoint}/transmission/config 0750 root root -" - "d ${cfg.mountPoint}/uptime-kuma 0750 root root -" - "d ${cfg.mountPoint}/ntfy 0750 ntfy-sh ntfy-sh -" - "d ${cfg.mountPoint}/ntfy/attachments 0750 ntfy-sh ntfy-sh -" - "d ${cfg.mountPoint}/paperless 0750 root root -" - # Paperless runs as UID 1000 (configured via USERMAP_UID) - "d ${cfg.mountPoint}/paperless/data 0750 1000 1000 -" - "d ${cfg.mountPoint}/paperless/media 0750 1000 1000 -" - "d ${cfg.mountPoint}/paperless/consume 0750 1000 1000 -" - "d ${cfg.mountPoint}/paperless/export 0750 1000 1000 -" - "d ${cfg.mountPoint}/mealie 0750 root root -" - "d ${cfg.mountPoint}/mealie/data 0755 root root -" - "d ${cfg.mountPoint}/restic-cache 0700 root root -" - ]; + "d ${cfg.mountPoint} 0755 root root -" + # Shared media directories used by both Jellyfin and Transmission. + "d ${cfg.mountPoint}/media 0755 root root -" + "d ${cfg.mountPoint}/media/movies 0755 root root -" + "d ${cfg.mountPoint}/media/tvshows 0755 root root -" + "d ${cfg.mountPoint}/media/general 0755 root root -" + "d ${cfg.mountPoint}/media/complete 0755 root root -" + "d ${cfg.mountPoint}/restic-cache 0700 root root -" + ] ++ (map + (d: "d ${cfg.mountPoint}/${d.path} ${d.mode} ${d.user} ${d.group} -") + config.homey.storage.extraDirs); }; }