# 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@ ``` ### 1.3 Copy the flake to the Pi ```bash # From your workstation (repo root) rsync -avz --exclude='.git' . nixos@:/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@:/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@ ``` --- ## Phase 2 — Restore Data from Old Volumes Mount the external HD (if not auto-mounted): ```bash sudo mount /dev/disk/by-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` | `zakobar.com` | `https://localhost:443` | | `git` | `zakobar.com` | `https://localhost:443` | | `nextcloud` | `zakobar.com` | `https://localhost:443` | | `ldapadmin` | `zakobar.com` | `https://localhost:443` | | `jellyfin` | `zakobar.com` | `https://localhost:443` | | `torrent` | `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 `zakobar.com` pointing to your tunnel's UUID (Cloudflare creates this automatically when you add hostnames). You do not need to add `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=zakobar,dc=com -D "cn=admin,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.zakobar.com # Gitea login? # Visit https://git.zakobar.com — should redirect to authelia if not logged in # Nextcloud? # Visit https://nextcloud.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 zakobar.com 192.168.1.100 auth.zakobar.com 192.168.1.100 git.zakobar.com 192.168.1.100 nextcloud.zakobar.com 192.168.1.100 ldapadmin.zakobar.com 192.168.1.100 jellyfin.zakobar.com 192.168.1.100 torrent.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 \ --password-file /run/secrets/restic_password \ snapshots ``` ### Restore a single service from backup ```bash sudo systemctl stop podman-gitea.service sudo restic -r 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`.