2026-05-10 13:44:27 +03:00
2026-05-10 11:30:43 +03:00
2026-05-10 11:30:43 +03:00
2026-05-10 13:44:27 +03:00
2026-05-10 11:30:43 +03:00
2026-05-10 11:30:43 +03:00
2026-04-25 21:47:42 +03:00
2026-04-25 21:49:42 +03:00
2026-04-26 00:09:52 +03:00
2026-04-25 21:49:42 +03:00
2026-05-10 11:30:43 +03:00
2026-05-10 13:44:27 +03:00
2026-05-10 11:30:43 +03:00

Homey

A home environment for everyone!

NixOS Deployment (active branch: nixos-port)

Prerequisites

Before building, make sure the following are set in the repo:

  • hosts/pi-main/default.nix — SSH public key, static IP, WiFi SSID
  • secrets/secrets.yaml — all secrets populated and sops-encrypted
  • WiFi password secret formatted as wifi_psk=YourPassword (see below)

Adding / updating secrets

sops secrets/secrets.yaml

Opens your editor with the decrypted file. Save and quit to re-encrypt.

The WiFi password entry must use the wifi_psk= prefix so wpa_supplicant can look up the value by name:

wifi/psk: "wifi_psk=YourActualWifiPassword"

Phase 1 — Bootstrap image (flash this first)

The full pi-main config requires sops secrets, which require an age key on the Pi — but the age key doesn't exist until after first boot. To break the chicken-and-egg problem, flash a minimal bootstrap image first.

Before building, fill in the WiFi password in flake.nix in the pi-main-bootstrap config (search for WIFI_PASSWORD_HERE):

networks."Zakobar".psk = "your-actual-wifi-password";

Build the bootstrap SD image (requires aarch64-linux build capability — either boot.binfmt.emulatedSystems = ["aarch64-linux"] on your workstation, or an aarch64 remote builder):

nix build .#nixosConfigurations.pi-main-bootstrap.config.system.build.sdImage \
  --system aarch64-linux

Find your SD card device, then flash (double-check /dev/sdX!):

lsblk

zstdcat result/sd-image/nixos-sd-image-*.img.zst | \
  sudo dd of=/dev/sdX bs=4M status=progress conv=fsync

The Pi will boot at 192.168.1.100, connect to Zakobar WiFi, and accept SSH connections with your key. No services run yet.

Phase 2 — Generate age key and re-encrypt secrets

# SSH into the Pi
ssh admin@192.168.1.100

# Generate the age key
sudo age-keygen -o /var/lib/sops-nix/key.txt

# Print the public key — copy it
sudo age-keygen -y /var/lib/sops-nix/key.txt

Back on your workstation, add the public key to secrets/.sops.yaml alongside the existing PGP key:

keys:
  - &pi_main age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
creation_rules:
  - path_regex: secrets/secrets.yaml$
    key_groups:
      - pgp:
          - 076AA297579A0064
        age:
          - *pi_main

Then re-encrypt so the Pi can decrypt its own secrets:

sops updatekeys secrets/secrets.yaml

Phase 3 — Deploy the full config

nixos-rebuild switch \
  --flake .#pi-main \
  --target-host admin@192.168.1.100 \
  --build-host admin@192.168.1.100 \
  --use-remote-sudo

The Pi builds its own config natively (no cross-compilation). sops-nix will now decrypt all secrets and start all services.

You can also use the command:

homey-deploy-rpi-main

Ongoing deploys from workstation

All future config changes follow the same pattern:

  1. Edit files on workstation
  2. Run:
homey-deploy-rpi-main

NixOS activates the new config on the Pi immediately, with an automatic rollback if activation fails.

Post-deploy setup

Some services require manual one-time configuration after the first deploy.

Ntfy — push notifications

Ntfy's admin user is created automatically from sops on first start. You still need to create a phone token and subscribe to the alerts topic.

  1. Visit https://ntfy.zakobar.com and log in with the admin password (ntfy/admin_password in secrets/secrets.yaml).
  2. Go to Account → Access Tokens → Create token — give it a name (e.g. "phone") and copy the token value.
  3. In the Ntfy mobile app:

    • Server: https://ntfy.zakobar.com
    • Access token: the token you just created
  4. Subscribe to the alerts topic in the app.

Uptime Kuma — notifications (two-deploy process)

Uptime Kuma monitors are created automatically by the sync script on first deploy, but notification channels must be configured in the UI before they can be attached to monitors. This requires two deploys:

Deploy 1 — services are up, monitors exist, but no notifications assigned yet.

Then, in the Uptime Kuma UI (https://uptime.zakobar.com):

  1. Go to Settings → Notifications → Add Notification.
  2. Choose ntfy as the type and fill in:

    • Server URL: https://ntfy.zakobar.com
    • Topic: alerts
    • Token: use the admin token (or create a dedicated one in ntfy)
  3. Save — you do not need to manually assign it to any monitor.

Deploy 2 — run homey-deploy-rpi-main again. The sync script will detect the newly configured notification channel and attach it to every monitor automatically.

Any notifications added to Uptime Kuma in the future will also be picked up on the next deploy.

Backing up

Backups use restic and run automatically via systemd on a daily schedule.

Strategy — two tiers

  1. Primary (automatic): Daily backup to an S3-compatible bucket (Backblaze B2, Wasabi, AWS S3, etc.). Restic deduplicates and encrypts before upload. Retention: 7 daily, 4 weekly, 6 monthly snapshots.
  2. Offload (manual): Run scripts/offload-backup.sh --target /path/to/disk to clone snapshots from the S3 repo onto a local disk (USB plugged into the Pi, or a disk on your workstation). Uses restic copy so deduplication is preserved on the target.

What is backed up

All service data under /mnt/data/:

  • openldap/ — LDAP database and config
  • authelia/ — Authelia config and state
  • gitea/ — Gitea repositories and data
  • nextcloud/ — Nextcloud files + a pg_dump of the database
  • jellyfin/ — Jellyfin metadata (media files are excluded — re-downloadable)
  • transmission/ — Torrent client config

Nextcloud is placed into maintenance mode and postgres is pg_dump'd before each backup to ensure a consistent snapshot.

First-time setup — initialize the repository

Restic requires a one-time init before the first backup can run. The automated job will fail with "repository does not exist" until this is done.

Run on the Pi after the first deploy:

# Note: use single quotes around the remote script to prevent local shell expansion
ssh admin@192.168.1.100 'sudo bash -c '"'"'
  export AWS_ACCESS_KEY_ID=$(cat /run/secrets/restic/s3_access_key_id)
  export AWS_SECRET_ACCESS_KEY=$(cat /run/secrets/restic/s3_secret_access_key)
  export RESTIC_PASSWORD=$(cat /run/secrets/restic/password)
  restic -r s3:https://s3.us-east-005.backblazeb2.com/zakobar-home-backup init
'"'"''

You only need to do this once. After init succeeds, the daily timer will run normally. To trigger a backup immediately without waiting for 03:00:

ssh admin@192.168.1.100 "sudo systemctl start restic-backups-homey.service"

Configuration

Repository URL and credentials are set per-host:

# hosts/pi-main/default.nix
homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/your-bucket";

S3 credentials live in secrets/secrets.yaml as restic/s3_access_key_id and restic/s3_secret_access_key.

Restore

# List snapshots
restic -r s3:https://... snapshots

# Restore latest snapshot to /mnt/data
restic -r s3:https://... restore latest --target /mnt/data

# Restore a single service
restic -r s3:https://... restore latest --target /mnt/data --include /mnt/data/gitea

Disaster Recovery

Full recovery from total host failure (dead Pi, dead SD card), assuming this git repo and your workstation PGP key (076AA297579A0064) survive.

Step 1 — Flash and boot a new Pi

Follow Phase 1 above to build and flash a fresh bootstrap image, then SSH in.

Step 2 — Regenerate the age key and re-encrypt secrets

The old Pi's age key is gone with the dead machine. Your workstation PGP key is the fallback and can still decrypt secrets/secrets.yaml.

On the Pi:

sudo age-keygen -o /var/lib/sops-nix/key.txt
sudo age-keygen -y /var/lib/sops-nix/key.txt   # copy this public key

On the workstation — replace the old age key in secrets/.sops.yaml with the new public key, then re-encrypt:

sops updatekeys secrets/secrets.yaml
git add secrets/.sops.yaml secrets/secrets.yaml
git commit -m "replace Pi age key after host failure"

Step 3 — Deploy the full NixOS config

nixos-rebuild switch \
  --flake .#pi-main \
  --target-host admin@192.168.1.100 \
  --build-host admin@192.168.1.100 \
  --use-remote-sudo

This brings up the OS and mounts /mnt/data. Services will fail to start until data is restored — that is expected.

Step 4 — Restore data from restic

Credentials are in secrets/secrets.yaml (restic/password, restic/s3_access_key_id, restic/s3_secret_access_key).

ssh admin@192.168.1.100

export RESTIC_REPOSITORY="s3:https://s3.us-east-005.backblazeb2.com/zakobar-home-backup"
export RESTIC_PASSWORD="..."          # restic/password from secrets
export AWS_ACCESS_KEY_ID="..."        # restic/s3_access_key_id
export AWS_SECRET_ACCESS_KEY="..."    # restic/s3_secret_access_key

restic snapshots                      # verify repo is reachable
sudo restic restore latest --target /mnt/data

If restoring from a USB offload disk instead of S3:

sudo restic -r /mnt/usb/homey-backup restore latest --target /mnt/data

Step 5 — Restore the Nextcloud database

The raw Postgres data dir is excluded from restic; only the pg_dump SQL file is backed up. After the data restore you will have /mnt/data/nextcloud/db-dump/nextcloud.sql but an empty database. Import it:

sudo systemctl start podman-nextcloud-postgres
# Wait ~10 s for Postgres to be ready, then:
podman exec -i nextcloud-postgres \
  psql -U postgres nextcloud_db \
  < /mnt/data/nextcloud/db-dump/nextcloud.sql

Step 6 — Start services and verify

sudo systemctl start podman-openldap podman-authelia podman-gitea podman-nextcloud

Manual checks after restart:

  • Gitea: Admin → Authentication Sources — verify the LDAP source is present. It lives in Gitea's database (restored from restic) so it should survive automatically. Confirm by logging in with an LDAP user.
  • Nextcloud: Admin → LDAP/AD Integration — confirm the LDAP app is still configured. If not, re-enter the settings from the LDAP Configuration section of this file.

Key risks

Risk Consequence
External HD also fails Restore all data from restic — Nextcloud files may be large
Workstation PGP key lost Cannot decrypt secrets/secrets.yaml — passwords must be reset manually per service
USB offload not yet implemented scripts/offload-backup.sh does not exist yet; S3 is the only working backup tier

Running commands in containers

All services run as podman containers. Use podman exec to run commands inside them.

General pattern

Containers are started by systemd as root, so they live in root's podman context. All podman commands must be run with sudo.

# List running containers
sudo podman ps

# Run a command in a container
sudo podman exec <container-name> <command>

# Run as a specific user
sudo podman exec -u <user> <container-name> <command>

# Interactive shell
sudo podman exec -it <container-name> sh

Container names match the service: openldap, authelia, gitea, nextcloud, nextcloud-postgres, jellyfin, transmission.

Nextcloud — running occ commands

occ must run as www-data inside the nextcloud container.

# General form
sudo podman exec -u www-data nextcloud php occ <command>

# Examples
sudo podman exec -u www-data nextcloud php occ status
sudo podman exec -u www-data nextcloud php occ maintenance:mode --off
sudo podman exec -u www-data nextcloud php occ preview:generate-all -vvv
sudo podman exec -u www-data nextcloud php occ ldap:promote-group "admins"

Running without -u www-data will create files owned by root inside the container, which breaks Nextcloud's file access.

S
Description
No description provided
Readme 514 KiB
Languages
Nix 97.7%
Shell 2.3%