Everything changed - major rewrite

This commit is contained in:
Aner Zakobar
2026-06-07 00:59:22 +03:00
parent 08e8b5edbe
commit 261cf892dd
20 changed files with 673 additions and 139 deletions
+153 -61
View File
@@ -1,9 +1,9 @@
# AGENTS.md
Self-hosted home server configuration for a Raspberry Pi 4 (8 GB), managed
entirely through NixOS. Services run as podman containers under systemd.
Remote access is via Cloudflare Tunnel; local access goes through Caddy
with Let's Encrypt TLS (DNS-01, Cloudflare API).
entirely through NixOS. Services run as podman containers or native NixOS
services under systemd. Remote access is via Cloudflare Tunnel; local access
goes through Caddy with Let's Encrypt TLS (DNS-01, Cloudflare API).
The original Kubernetes/Helm setup is preserved on the `main` branch.
This branch (`nixos-port`) is the active NixOS port.
@@ -20,14 +20,21 @@ modules/
caddy.nix # Caddy reverse proxy (DNS-01 ACME, forward_auth)
cloudflared.nix # Cloudflare Tunnel for remote access
backup.nix # Restic daily backups (S3 primary + manual offload)
monitoring.nix # Prometheus + Grafana (native NixOS services)
services/
openldap.nix # OpenLDAP — central identity provider
authelia.nix # Authelia — SSO gateway
authelia.nix # Authelia — SSO gateway + accessControlRules option
gitea.nix # Gitea — Git server
gitea-runner.nix # Gitea Actions runner
nextcloud.nix # Nextcloud + PostgreSQL
phpldapadmin.nix # phpLDAPadmin — LDAP web UI
jellyfin.nix # Jellyfin — media server (disabled by default)
transmission.nix # Transmission — torrent client (disabled by default)
jellyfin.nix # Jellyfin — media server (disabled)
transmission.nix # Transmission — torrent client (disabled)
uptime-kuma.nix # Uptime Kuma + homey.monitoring.monitors option
ntfy.nix # Ntfy — push notification server (native NixOS)
mealie.nix # Mealie — recipe manager
paperless.nix # Paperless-ngx — document management
eurovote.nix # Eurovision Vote — Django voting app
hosts/
pi-main/
default.nix # Service selection + host-specific overrides
@@ -42,14 +49,20 @@ PORTING.md # Step-by-step migration guide from the old Helm s
All services live under `zakobar.com`.
| Service | URL | Auth |
|---------|-----|------|
| Authelia | `auth.zakobar.com` | Public (it is the auth portal) |
| Gitea | `git.zakobar.com` | Gitea-native (LDAP) |
| Nextcloud | `nextcloud.zakobar.com` | Nextcloud-native |
| phpLDAPadmin | `ldapadmin.zakobar.com` | Authelia two_factor, admins only |
| Jellyfin | `jellyfin.zakobar.com` | Jellyfin-native |
| Transmission | `torrent.zakobar.com` | Authelia two_factor, admins only |
| Service | URL | Auth | Runtime |
|---------|-----|------|---------|
| Authelia | `auth.zakobar.com` | Public (it is the auth portal) | container |
| Gitea | `git.zakobar.com` | Gitea-native (LDAP) | container |
| Nextcloud | `nextcloud.zakobar.com` | Nextcloud-native | container |
| Mealie | `mealie.zakobar.com` | Mealie-native (LDAP) | container |
| Paperless | `paperless.zakobar.com` | Authelia one_factor (SSO) | container |
| phpLDAPadmin | `ldapadmin.zakobar.com` | Authelia two_factor, admins only | container |
| Uptime Kuma | `uptime.zakobar.com` | Authelia two_factor, admins only | container |
| Grafana | `grafana.zakobar.com` | Authelia two_factor, admins only | NixOS |
| Ntfy | `ntfy.zakobar.com` | Bypass (ntfy token/password auth) | NixOS |
| Eurovision Vote | `eurovision-vote.zakobar.com` | Authelia one_factor (`/admin` two_factor) | NixOS |
| Jellyfin | `jellyfin.zakobar.com` | Jellyfin-native | container (disabled) |
| Transmission | `torrent.zakobar.com` | Authelia two_factor, admins only | container (disabled) |
## Networking
@@ -63,7 +76,16 @@ All containers join a private podman network named **`homey`**, created by the
- **Defence in depth** — even if the firewall were misconfigured, services are
not bound to `0.0.0.0`.
Internal ports (all mapped to `127.0.0.1` on the host):
Native NixOS services (not containers) listen on `127.0.0.1` directly:
| Service | Host port |
|---------|-----------|
| ntfy | 2586 |
| Eurovision Vote | 8007 |
| Prometheus | 9090 |
| Grafana | 3002 |
Container host-port mappings (all bound to `127.0.0.1`):
| Container | Host port | Container port |
|-----------|-----------|----------------|
@@ -73,6 +95,10 @@ Internal ports (all mapped to `127.0.0.1` on the host):
| nextcloud | 8080 | 80 |
| nextcloud-postgres | 5432 | 5432 |
| phpldapadmin | 8081 | 80 |
| uptime-kuma | 3001 | 3001 |
| mealie | 9093 | 9000 |
| paperless | 8083 | 8000 |
| paperless-redis | (internal only) | 6379 |
| jellyfin | 8096 | 8096 |
| transmission | 9092 | 9091 |
@@ -87,22 +113,38 @@ All persistent data lives on the external HD at `/mnt/data/`:
```
/mnt/data/
openldap/
etc-ldap-slapd.d/ → /etc/ldap/slapd.d in container
var-lib-ldap/ → /var/lib/ldap in container
authelia/config/ → /config
gitea/data/ → /data
etc-ldap-slapd.d/ → /etc/ldap/slapd.d in container
var-lib-ldap/ → /var/lib/ldap in container
authelia/config/ → /config
gitea/data/ → /data
nextcloud/
html/ → /var/www/html
db/ → /var/lib/postgresql/data
db-dump/ → pg_dump output (pre-backup)
jellyfin/config/ → /config
media/movies|tvshows|... → shared media (read-only to jellyfin)
transmission/config/ → /config
restic-cache/ → restic local cache
html/ → /var/www/html
db/ → /var/lib/postgresql/data
db-dump/ → pg_dump output (pre-backup)
jellyfin/config/ → /config
media/movies|tvshows|... → shared media (read-only to jellyfin)
transmission/config/ → /config
uptime-kuma/ → /app/data
mealie/data/ → /app/data
paperless/
data/ → /usr/src/paperless/data (DB, index)
media/ → /usr/src/paperless/media (document files)
consume/ → /usr/src/paperless/consume (drop folder)
export/ → /usr/src/paperless/export
ntfy/
auth.db → ntfy user/token database (host path)
cache.db → ntfy message cache (host path)
attachments/ → file attachments (host path)
restic-cache/ → restic local cache
```
Grafana and Prometheus use system state dirs (`/var/lib/grafana`,
`/var/lib/prometheus2`) and are not backed up — dashboards are provisioned by
Nix and metrics are ephemeral.
The drive device path is set per-host in `hosts/<name>/default.nix` via
`homey.storage.device`. Use a `/dev/disk/by-id/` path for stability.
`homey.storage.device`. Use a `/dev/disk/by-label/` or `/dev/disk/by-id/`
path for stability.
## Build / Validate Commands
@@ -160,7 +202,8 @@ restic password, Cloudflare tokens) can be generated fresh.
### Nix
1. **Module pattern** — every service is an opt-in module with an `enable` option:
1. **Module pattern** — every service is an opt-in module with an `enable` option
(defaulting to `false` for optional services):
```nix
options.homey.myservice.enable = lib.mkEnableOption "My service";
config = lib.mkIf config.homey.myservice.enable { ... };
@@ -168,7 +211,7 @@ restic password, Cloudflare tokens) can be generated fresh.
2. **`homeyConfig` specialArgs** — top-level site config (domain, org name,
timezone) is passed via `specialArgs` in `flake.nix` and accessed as
`homeyConfig` in every module. Do not read domain/org from hardcoded strings.
`homeyConfig` in every module. Do not hardcode domain/org strings.
3. **No secrets in the Nix store** — secrets are always read from sops-managed
files at runtime, never embedded in the built config. Use
@@ -176,7 +219,7 @@ restic password, Cloudflare tokens) can be generated fresh.
4. **Secret injection pattern** — because `oci-containers` `environmentFiles`
is limited, use a `systemd ExecStartPre` script to write an ephemeral env
file at `/run/<service>-secrets.env` and reference it via `EnvironmentFile`.
file at `/run/<service>-secrets.env` and reference it via `environmentFiles`.
Clean it up in `postStop`.
5. **`--network=homey`** — all containers join the private `homey` podman
@@ -187,16 +230,47 @@ restic password, Cloudflare tokens) can be generated fresh.
explicitly. The external HD mount unit is `mnt-data.mount`; containers that
need storage must depend on it.
### Module Contribution Options
Several cross-cutting concerns are wired up via list options that any service
module can append to, rather than editing central files:
| Option | Declared in | Purpose |
|--------|-------------|---------|
| `homey.caddy.virtualHosts` | `caddy.nix` | Add a reverse-proxy vhost |
| `homey.storage.extraDirs` | `storage.nix` | Create tmpfiles dirs on the HD |
| `homey.backup.extraPaths` | `backup.nix` | Include a path in restic backups |
| `homey.monitoring.monitors` | `uptime-kuma.nix` | Add an Uptime Kuma HTTP monitor |
| `homey.authelia.accessControlRules` | `authelia.nix` | Add Authelia access-control rules |
Each service module declares its own entries. No central file edits needed.
**`homey.authelia.accessControlRules`** — each rule has:
- `priority` (int) — lower = earlier in the list. Authelia stops at the first
match, so more-specific rules (e.g. `subject: group:admins`) must precede
their catch-all counterparts. Assigned priority ranges by category:
- `0` — auth bypass (Authelia itself)
- `1019` — blanket bypasses (e.g. ntfy)
- `2049` — admin-only two_factor + deny pairs
- `5064` — open one_factor services
- `6579` — per-path rules (resources + subject combinations)
- `domain` (list of strings)
- `policy` — `bypass` | `one_factor` | `two_factor` | `deny`
- `subject` (optional list) — e.g. `[ "group:admins" ]`
- `resources` (optional list) — URL path regexes
### Adding a New Service
1. Create `modules/services/<name>.nix` following the existing module pattern.
2. Add `homey.<name>.enable = false` as the default option.
3. Import the new module in `flake.nix` (in the `modules` list inside `mkHost`).
4. Enable it in `hosts/pi-main/default.nix`.
5. Add a Caddy virtual host block in `modules/caddy.nix`.
6. Add the service data directory to `modules/storage.nix` `tmpfiles.rules`.
7. Add the data path to the `paths` list in `modules/backup.nix`.
8. Add any new secrets to `secrets/secrets.yaml` (plaintext) and document them.
2. Import it in `flake.nix` (in the `modules` list inside `mkHost`).
3. Enable it in `hosts/pi-main/default.nix`.
4. Inside the module's `config = lib.mkIf cfg.enable { ... }` block:
- **Caddy**: add `homey.caddy.virtualHosts = [{ subdomain = "…"; port = …; auth = true/false; }]`
- **Storage**: add `homey.storage.extraDirs = [{ path = "…"; }]` for each HD directory
- **Backup**: add `homey.backup.extraPaths = [ "${dataDir}/…" ]`
- **Authelia**: add `homey.authelia.accessControlRules = [{ priority = …; domain = […]; policy = "…"; }]`
- **Monitoring**: add `homey.monitoring.monitors = [{ name = "…"; url = "…"; interval = 60; }]`
5. Add any new secrets to `secrets/secrets.yaml` and document them.
### Updating or Regenerating Secrets
@@ -240,45 +314,48 @@ production-ready:
DNS plugin uses `lib.fakeHash` as a placeholder. After the first `nix build`,
replace it with the hash Nix reports in the error message.
- [ ] **`hosts/pi-main/default.nix` — fill in real values**:
- SSH public key in `users.users.admin.openssh.authorizedKeys.keys`
- External HD device path in `homey.storage.device`
- Backup repository URL in `homey.backup.repository` — must be an S3-compatible
URL, e.g. `"s3:https://s3.us-west-002.backblazeb2.com/your-bucket-name"`
- [ ] **`monitoring.nix` — Grafana dashboard hash**: The Node Exporter Full
dashboard `fetchurl` hash is a placeholder. Run:
```bash
nix store prefetch-file --hash-type sha256 \
https://grafana.com/api/dashboards/1860/revisions/37/download
```
and replace the hash in `modules/monitoring.nix`.
- [ ] **`secrets/secrets.yaml` — populate and encrypt**: Fill in all secret
values (old passwords from k8s + freshly generated ones, including
`restic/s3_access_key_id` and `restic/s3_secret_access_key`), then run
`sops --encrypt --in-place secrets/secrets.yaml` before committing.
- [x] **`secrets/.sops.yaml` — PGP key**: The encryption subkey
`076AA297579A0064` is already in `.sops.yaml`.
values, then run `sops --encrypt --in-place secrets/secrets.yaml` before
committing. Secrets needed:
- From old k8s deployment: openldap passwords, gitea/nextcloud passwords
- Fresh: authelia JWT/session/encryption keys, gitea JWT tokens
- New services: `uptime-kuma/admin_password`, `ntfy/admin_password`,
`grafana/secret_key`, `ntfy/web_push_private_key`
- Backup: `restic/s3_access_key_id`, `restic/s3_secret_access_key`
- WiFi: `wifi/psk`
- [ ] **Cloudflare Tunnel**: Create the tunnel in the Zero Trust dashboard,
copy the tunnel token into secrets, and configure public hostnames. See
`modules/cloudflared.nix` and Phase 3 of `PORTING.md` for details.
copy the tunnel token into secrets, and configure public hostnames for all
enabled services. See `modules/cloudflared.nix` for details.
- [ ] **Cloudflare Tunnel — add new services**: After the initial tunnel is set
up, add public hostnames for: `uptime`, `ntfy`, `grafana`, `mealie`,
`paperless`, `eurovision-vote`.
- [ ] **Second machine**: When ready, add `hosts/pi-secondary/` and uncomment
the `pi-secondary` entry in `flake.nix`. Services communicating cross-machine
should reference the primary Pi's LAN IP instead of `127.0.0.1`.
- [ ] **Jellyfin and Transmission**: Both modules are written and importable
but disabled. Enable in `hosts/pi-main/default.nix` when ready:
- [ ] **Jellyfin and Transmission**: Both modules exist but are disabled.
Enable in `hosts/pi-main/default.nix` when ready:
```nix
homey.jellyfin.enable = true;
homey.transmission.enable = true;
```
- [ ] **Backup — S3 credentials**: Add `restic/s3_access_key_id` and
`restic/s3_secret_access_key` to secrets, and set `homey.backup.repository`
to your S3-compatible bucket URL in `hosts/pi-main/default.nix`.
- [ ] **Backup — offload script**: Write `scripts/offload-backup.sh` for
manually copying snapshots to a local disk (USB attached to Pi, or a disk
on your workstation). Uses `restic copy` to clone from the S3 repo into a
local restic repo on the target path. See `TODO.org` for design notes.
manually copying snapshots to a local disk. Uses `restic copy` to clone from
the S3 repo into a local restic repo. See `TODO.org` for design notes.
### Post- Pi first boot
### Post-Pi first boot
These items require the Pi to be built, flashed, and booted at least once.
@@ -293,7 +370,6 @@ These items require the Pi to be built, flashed, and booted at least once.
- [ ] **Gitea LDAP auth**: After first start, configure LDAP authentication
in Gitea's admin panel (Admin → Authentication Sources → Add LDAP source).
The old Helm chart had this commented out; it must be done manually once.
Relevant settings:
- Host: `127.0.0.1`, Port: `389`, Security: Unencrypted
- Bind DN: `cn=readonly,dc=zakobar,dc=com`
@@ -302,3 +378,19 @@ These items require the Pi to be built, flashed, and booted at least once.
- [ ] **Nextcloud LDAP app**: After restoring the Nextcloud volume, verify
the LDAP Users and Contacts app is still configured correctly
(Admin → LDAP/AD Integration).
- [ ] **Ntfy VAPID keys**: Generate Web Push keys on the Pi:
```bash
sudo ntfy webpush keys
```
Set `homey.ntfy.webPushPublicKey` in `default.nix` and add the private key
to sops as `ntfy/web_push_private_key`.
- [ ] **Uptime Kuma monitors**: On first boot, `uptime-kuma-sync` will
automatically create all monitors declared via `homey.monitoring.monitors`.
Verify they appear correctly in the UI at `https://uptime.zakobar.com`.
- [ ] **Paperless admin token (iOS Shortcut)**: After first start, generate a
dedicated API token in the Paperless web UI (Profile → API Auth Token) for
the iOS Shortcut upload flow. The `/api/documents/post_document/` path
bypasses Authelia — the token is the only auth.