Files
homey/PORTING.md
Aner Zakobar 0b73d493d8 Working NixOS port: all core services operational
- Fix Caddy cfProxy helper for cloudflared http:// vhosts (X-Forwarded-Proto)
- Fix Authelia LDAP bind (readonly user ACL + password sync)
- Add gitea-admin-setup oneshot service to survive rebuilds
- Update Authelia forward_auth with header_up X-Forwarded-Proto https
- Update TODO.org with completed tasks and LDAP config details
- Remove old Helm/k8s artifacts (Chart.yaml, templates/, values/, scripts)
- Add result to .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 14:46:21 +03:00

11 KiB

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)

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:

- 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):

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)

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:

# 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:

# 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):

# Find your IP
ip addr

# Set a temporary password for nixos user to SSH in
passwd nixos

From your workstation:

ssh nixos@<pi-ip>

1.3 Copy the flake to the Pi

# From your workstation (repo root)
rsync -avz --exclude='.git' . nixos@<pi-ip>:/tmp/homey/

1.4 Generate the Pi's age key

# 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:

- 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:

# 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:

# 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:
    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

# 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:

ssh admin@<pi-ip>

Phase 2 — Restore Data from Old Volumes

Mount the external HD (if not auto-mounted):

sudo mount /dev/disk/by-id/<your-drive-id> /mnt/data

Copy data from the old Longhorn volume backups into the new layout:

# 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):

# 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 → 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:

# On the Pi
sudo nixos-rebuild switch --flake /path/to/homey#pi-main

Verification checklist

# 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

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

sudo nixos-rebuild switch --flake /path/to/homey#pi-main

Edit secrets

sops secrets/secrets.yaml
# Save and exit — sops re-encrypts automatically
# Then copy to Pi and rebuild

Browse service data on disk

ls /mnt/data/
ls /mnt/data/gitea/data/
# No special tools needed — plain filesystem

Trigger a manual backup

sudo systemctl start restic-backups-homey.service

List backup snapshots

sudo restic -r <your-repo-url> \
  --password-file /run/secrets/restic_password \
  snapshots

Restore a single service from backup

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:
    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.