Everything changed - major rewrite
This commit is contained in:
@@ -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)
|
||||
- `10–19` — blanket bypasses (e.g. ntfy)
|
||||
- `20–49` — admin-only two_factor + deny pairs
|
||||
- `50–64` — open one_factor services
|
||||
- `65–79` — 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.
|
||||
|
||||
Reference in New Issue
Block a user