Files
homey/PORTING.md
T
Aner Zakobar 2f0d0b5e4c Port to NixOS: replace Helm chart with flake-based NixOS config
Replaces the Helm/k3s setup with a declarative NixOS configuration targeting
a Raspberry Pi 4. Services run as podman containers under systemd, with data
on an external HD at /mnt/data. Key components:

- flake.nix: multi-host flake with pi-main (aarch64) and a placeholder for a
  second machine
- modules/common.nix: shared system config (nix, podman, sops, SSH)
- modules/storage.nix: external HD mount with per-service subdirs
- modules/caddy.nix: Caddy with cloudflare DNS-01 ACME + authelia forward_auth
- modules/cloudflared.nix: Cloudflare tunnel for remote access
- modules/backup.nix: restic daily backups with NC maintenance mode pre-hook
- modules/services/{openldap,authelia,gitea,nextcloud,phpldapadmin}.nix: core services
- modules/services/{jellyfin,transmission}.nix: media services (disabled by default)
- secrets/: sops-nix scaffold with .sops.yaml age key config
- hosts/pi-main/: hardware config + service selection for the Pi
- PORTING.md: step-by-step migration guide (SD card → data restore → verify)
2026-04-15 17:18:12 +03:00

401 lines
11 KiB
Markdown

# Porting Guide — Helm/k3s → NixOS
This document walks through setting up the new NixOS-based home server from
scratch on a Raspberry Pi 4, restoring data from old Longhorn volumes, and
verifying each service.
---
## Prerequisites (on your workstation)
- `nix` with flakes enabled (`~/.config/nix/nix.conf`: `experimental-features = nix-command flakes`)
- `sops` + `age` CLI tools (`nix-shell -p sops age`)
- An SSH key pair
---
## Phase 0 — Secrets
### 0.1 Generate your age key (workstation)
```bash
age-keygen -o ~/.config/sops/age/keys.txt
age-keygen -y ~/.config/sops/age/keys.txt # print public key
```
You will add the Pi's public key in step 2.2; for now add your workstation
public key so you can edit the secrets file offline.
Edit `secrets/.sops.yaml`, replace the placeholder with your workstation pubkey:
```yaml
- age:
- AGE-PUBLIC-KEY-YOUR-WORKSTATION # ← paste here
```
### 0.2 Fill in secrets.yaml
`secrets/secrets.yaml` is a **plaintext template** — do not commit it until
encrypted. Fill in all values:
| Key | Source |
|-----|--------|
| `openldap/admin_password` | From old k8s secret `openldap-admin` |
| `openldap/config_password` | From old k8s secret `openldap-config` |
| `openldap/ro_password` | From old k8s secret `openldap-ro` |
| `gitea/admin_password` | From old k8s secret `gitea-admin-pass` |
| `nextcloud/admin_password` | From old k8s secret `nextcloud-admin-pass` |
| `nextcloud/postgres_password` | From old k8s secret `nextcloud-postgres-pass` |
| `authelia/jwt_secret` | Generate: `openssl rand -hex 64` |
| `authelia/session_secret` | Generate: `openssl rand -hex 64` |
| `authelia/storage_encryption_key` | Generate: `openssl rand -hex 64` |
| `gitea/lfs_jwt_secret` | Generate: `openssl rand -base64 32 \| tr -d '='` |
| `gitea/oauth2_jwt_secret` | Generate: `openssl rand -base64 32 \| tr -d '='` |
| `gitea/internal_token` | Generate: `openssl rand -base64 75 \| tr -d '\n='` |
| `cloudflare/api_token` | Cloudflare dashboard → API Tokens → DNS:Edit |
| `cloudflare/tunnel_token` | Created in Phase 3 (Cloudflare setup) |
| `restic/password` | Generate: `openssl rand -base64 32` |
To get old k8s secrets (if the cluster is still running):
```bash
kubectl get secret openldap-admin -n homey -o jsonpath='{.data.password}' | base64 -d
kubectl get secret openldap-config -n homey -o jsonpath='{.data.password}' | base64 -d
kubectl get secret openldap-ro -n homey -o jsonpath='{.data.password}' | base64 -d
kubectl get secret gitea-admin-pass -n homey -o jsonpath='{.data.password}' | base64 -d
kubectl get secret nextcloud-admin-pass -n homey -o jsonpath='{.data.password}' | base64 -d
kubectl get secret nextcloud-postgres-pass -n homey -o jsonpath='{.data.password}' | base64 -d
```
### 0.3 Encrypt secrets.yaml (workstation, before committing)
```bash
sops --encrypt --in-place secrets/secrets.yaml
git add secrets/secrets.yaml secrets/.sops.yaml
```
---
## Phase 1 — Install NixOS on the Raspberry Pi 4
### 1.1 Flash the SD card
Download the NixOS aarch64 SD card image:
```
https://nixos.org/download#nixos-iso
→ "Raspberry Pi (aarch64) SD card image"
```
Flash with:
```bash
# macOS
sudo dd if=nixos-*-aarch64-linux.img of=/dev/rdiskN bs=4m status=progress
# Linux
sudo dd if=nixos-*-aarch64-linux.img of=/dev/sdX bs=4M status=progress conv=fsync
```
Label the partitions to match `hardware.nix`:
```bash
# After flashing, mount the root partition and relabel if needed:
sudo e2label /dev/sdX2 NIXOS_SD
sudo fatlabel /dev/sdX1 FIRMWARE
```
Boot the Pi from the SD card. You should get a serial console or HDMI output.
### 1.2 Initial network setup
On the Pi (serial or HDMI):
```bash
# Find your IP
ip addr
# Set a temporary password for nixos user to SSH in
passwd nixos
```
From your workstation:
```bash
ssh nixos@<pi-ip>
```
### 1.3 Copy the flake to the Pi
```bash
# From your workstation (repo root)
rsync -avz --exclude='.git' . nixos@<pi-ip>:/tmp/homey/
```
### 1.4 Generate the Pi's age key
```bash
# On the Pi
nix-shell -p age --run 'age-keygen -o /tmp/pi-age-key.txt'
age-keygen -y /tmp/pi-age-key.txt # print public key
```
Copy the public key back to your workstation. Add it to `secrets/.sops.yaml`:
```yaml
- age:
- AGE-PUBLIC-KEY-YOUR-WORKSTATION
- AGE-PUBLIC-KEY-PI-MAIN-REPLACE-ME # ← paste Pi's public key here
```
Re-encrypt secrets so the Pi can decrypt them:
```bash
# On workstation
sops updatekeys secrets/secrets.yaml
git add secrets/.sops.yaml secrets/secrets.yaml
git commit -m "Add Pi age key"
# Copy updated files to Pi
rsync -avz secrets/ nixos@<pi-ip>:/tmp/homey/secrets/
```
Place the Pi's private key where sops-nix expects it:
```bash
# On the Pi
sudo mkdir -p /var/lib/sops-nix
sudo cp /tmp/pi-age-key.txt /var/lib/sops-nix/key.txt
sudo chmod 600 /var/lib/sops-nix/key.txt
```
### 1.5 Configure host-specific settings
Edit `hosts/pi-main/default.nix` on the Pi (or on workstation first):
1. Set your SSH public key in `users.users.admin.openssh.authorizedKeys.keys`
2. Set `homey.storage.device` to your USB drive:
```bash
ls -la /dev/disk/by-id/ | grep -v part
```
3. Set `homey.backup.repository` to your backup destination
Edit `hosts/pi-main/hardware.nix` if the disk labels differ from defaults.
### 1.6 Install NixOS
```bash
# On the Pi
sudo nixos-install --flake /tmp/homey#pi-main --no-root-passwd
# Reboot
sudo reboot
```
After reboot, SSH in with your admin key:
```bash
ssh admin@<pi-ip>
```
---
## Phase 2 — Restore Data from Old Volumes
Mount the external HD (if not auto-mounted):
```bash
sudo mount /dev/disk/by-id/<your-drive-id> /mnt/data
```
Copy data from the old Longhorn volume backups into the new layout:
```bash
# Adjust source paths to wherever your Longhorn volume dumps are
BACKUP_SRC=/path/to/longhorn/backups
# OpenLDAP
sudo rsync -av $BACKUP_SRC/openldap/etc-ldap-slapd.d/ /mnt/data/openldap/etc-ldap-slapd.d/
sudo rsync -av $BACKUP_SRC/openldap/var-lib-ldap/ /mnt/data/openldap/var-lib-ldap/
# Gitea
sudo rsync -av $BACKUP_SRC/gitea/data/ /mnt/data/gitea/data/
# Nextcloud
sudo rsync -av $BACKUP_SRC/nextcloud/html/ /mnt/data/nextcloud/html/
# Restore postgres from pg_dump if available, otherwise restore the data dir:
sudo rsync -av $BACKUP_SRC/nextcloud/db/ /mnt/data/nextcloud/db/
```
Fix ownership (containers run as UID 1000 or root depending on image):
```bash
# openldap runs as root inside the container
sudo chown -R root:root /mnt/data/openldap/
# gitea runs as git (UID 1000)
sudo chown -R 1000:1000 /mnt/data/gitea/
# nextcloud runs as www-data (UID 33)
sudo chown -R 33:33 /mnt/data/nextcloud/html/
# postgres data owned by postgres (UID 999 in the postgres image)
sudo chown -R 999:999 /mnt/data/nextcloud/db/
```
---
## Phase 3 — Cloudflare Tunnel Setup
### 3.1 Create the tunnel in Cloudflare Zero Trust
1. Go to [https://one.dash.cloudflare.com](https://one.dash.cloudflare.com) → Networks → Tunnels
2. Click "Create a tunnel" → Cloudflared → Name it `pi-main`
3. Copy the tunnel token (long string starting with `eyJ...`)
4. Add it to `secrets/secrets.yaml` under `cloudflare/tunnel_token`
5. Re-encrypt: `sops secrets/secrets.yaml` (the file opens in `$EDITOR`)
### 3.2 Configure public hostnames in the Cloudflare dashboard
In the tunnel's "Public Hostnames" tab, add:
| Subdomain | Domain | Service |
|-----------|--------|---------|
| `auth` | `home.zakobar.com` | `https://localhost:443` |
| `git` | `home.zakobar.com` | `https://localhost:443` |
| `nextcloud` | `home.zakobar.com` | `https://localhost:443` |
| `ldapadmin` | `home.zakobar.com` | `https://localhost:443` |
| `jellyfin` | `home.zakobar.com` | `https://localhost:443` |
| `torrent` | `home.zakobar.com` | `https://localhost:443` |
For each entry, under "Additional settings" → TLS → **No TLS Verify: ON**
(because cloudflared connects to `localhost` but the cert is for the real hostname).
### 3.3 Update DNS in Cloudflare
Add a CNAME for `home.zakobar.com` pointing to your tunnel's UUID (Cloudflare
creates this automatically when you add hostnames). You do not need to add
`home.zakobar.com` to your domain's A records — Cloudflare handles it.
---
## Phase 4 — Rebuild and Verify
After restoring data and completing Cloudflare setup, apply the final config:
```bash
# On the Pi
sudo nixos-rebuild switch --flake /path/to/homey#pi-main
```
### Verification checklist
```bash
# All container services running?
systemctl list-units 'podman-*' --state=active
# OpenLDAP responding?
ldapsearch -x -H ldap://127.0.0.1:389 -b dc=home,dc=zakobar,dc=com -D "cn=admin,dc=home,dc=zakobar,dc=com" -W
# Authelia health?
curl -s http://localhost:9091/api/health | python3 -m json.tool
# Caddy serving TLS?
curl -I https://auth.home.zakobar.com
# Gitea login?
# Visit https://git.home.zakobar.com — should redirect to authelia if not logged in
# Nextcloud?
# Visit https://nextcloud.home.zakobar.com
# Cloudflare tunnel connected?
systemctl status cloudflared-tunnel-pi-main
```
---
## Phase 5 — Local DNS (optional but recommended)
To access services without going through Cloudflare on the LAN, add these
records to your router's DNS or Pi-hole:
```
192.168.1.100 home.zakobar.com
192.168.1.100 auth.home.zakobar.com
192.168.1.100 git.home.zakobar.com
192.168.1.100 nextcloud.home.zakobar.com
192.168.1.100 ldapadmin.home.zakobar.com
192.168.1.100 jellyfin.home.zakobar.com
192.168.1.100 torrent.home.zakobar.com
```
Replace `192.168.1.100` with your Pi's actual LAN IP.
---
## Day-to-day Operations
### Apply config changes
```bash
sudo nixos-rebuild switch --flake /path/to/homey#pi-main
```
### Edit secrets
```bash
sops secrets/secrets.yaml
# Save and exit — sops re-encrypts automatically
# Then copy to Pi and rebuild
```
### Browse service data on disk
```bash
ls /mnt/data/
ls /mnt/data/gitea/data/
# No special tools needed — plain filesystem
```
### Trigger a manual backup
```bash
sudo systemctl start restic-backups-homey.service
```
### List backup snapshots
```bash
sudo restic -r <your-repo-url> \
--password-file /run/secrets/restic_password \
snapshots
```
### Restore a single service from backup
```bash
sudo systemctl stop podman-gitea.service
sudo restic -r <repo> restore latest \
--target / \
--include /mnt/data/gitea
sudo systemctl start podman-gitea.service
```
---
## Adding a Second Machine (future)
1. Create `hosts/pi-secondary/default.nix` and `hardware.nix`
2. Enable the services you want on that machine
3. Services communicating cross-machine: reference the primary Pi's LAN IP or
hostname directly in environment variables (e.g. point gitea's LDAP config
at `192.168.1.100:389` rather than `127.0.0.1:389`).
4. Add the new host to `flake.nix`:
```nix
pi-secondary = mkHost {
system = "x86_64-linux";
hostPath = ./hosts/pi-secondary/default.nix;
};
```
5. Generate an age key on the new machine and add it to `.sops.yaml`.