{ config, lib, pkgs, homeyConfig, ... }: # Pi-main host configuration. # This file declares which services run on this machine and any # host-specific overrides. Hardware config lives in hardware.nix. { imports = [ ./hardware.nix ]; # linux_rpi4 is the Raspberry Pi Foundation's kernel, sourced from nixpkgs # and pre-built in cache.nixos.org. Avoids a multi-hour native compilation. boot.kernelPackages = pkgs.linuxKernel.packages.linux_rpi4; # ------------------------------------------------------------------------- # Identity # ------------------------------------------------------------------------- networking.hostName = "pi-main"; # ------------------------------------------------------------------------- # WiFi — static IP, always connect to home network # ------------------------------------------------------------------------- networking.wireless = { enable = true; # secretsFile is read by wpa_supplicant at runtime; values are literal # (not env vars). The key name after "ext:" must match a line in the file # formatted as: key_name=the-actual-password secretsFile = config.sops.secrets."wifi/psk".path; networks."Zakobar".pskRaw = "ext:wifi_psk"; }; # Static IP on wlan0 networking.interfaces.wlan0.ipv4.addresses = [{ address = "192.168.1.100"; prefixLength = 24; }]; networking.defaultGateway = "192.168.1.1"; networking.nameservers = [ "1.1.1.1" "8.8.8.8" ]; # Disable DHCP on wlan0 — we're using a static address networking.useDHCP = false; networking.interfaces.wlan0.useDHCP = false; # The secret file must contain exactly one line: wifi_psk= # Add it with: sops secrets/secrets.yaml → wifi/psk: "wifi_psk=YourPassword" sops.secrets."wifi/psk" = { owner = "root"; mode = "0400"; }; # ------------------------------------------------------------------------- # Admin user # ------------------------------------------------------------------------- users.users.admin = { isNormalUser = true; extraGroups = [ "wheel" "podman" ]; # Paste your SSH public key here openssh.authorizedKeys.keys = [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDfzDDO5juINctECmWlsYtGghEiX/RnTJ1cazLvOWSrPfsTyEd+B1+Ig8kFefNryjkpApfRXqj5KtLPNlpLfdVBrOIfhIveEp2MGqhgOGZFNVxQyXnZgii8Zdh4cqZ2O3pZpMsaAQBaJ9nH6dK0dJjicWT5f6TqwrVcInywRc5SuyizoSxoFmg7ch2rnlVi0j5XMVqdh8XLzHXZ7yWCzXy7+hWl/d7pwpyuzoK8dBw2EU9TauhgRDruom5Q9vWJTLStALC9pAIb0v9UFj9y+1zwx7pXsXp5F1g73EYrE4QR+QQ6z2LebuK280W0t+VA/fSCEB13DnkmofgqZQxX5MSCmrxZ5lTFp1FjW6yJo7As9FheF/GECowYkMRIx4IiQsjjHjZqlLRpLas11yAp6tGoZnw59hFo6Lu0Kva39jGVVmioYHtAeE5rD5w+v5kseJR4jlQ8aKB5yOjYUQOIz2AHQyoidgaeR2jPWqZUeRQbACI+/p3CHO45r3hrjATtGloBg0xF95Qws7Be3mjHVhbBLOoob8MdZ8nYAGnhlWrZphlkvXsHC6OUkuDJW00tmMjWXRlFwhFJ+nqUQCgLVjxVHQJ5rq9GeXBUuNXAeCm5BKBsdq+9qqVlt7D9iGyfr0lcZ7peKz/96KwPCWpG2En1Ur0/cVcbWnXEfG/xWO10tQ== cardno:24_758_470" ]; }; security.sudo.wheelNeedsPassword = false; # convenience on a home server # ------------------------------------------------------------------------- # External HD # ------------------------------------------------------------------------- homey.storage = { # Replace with the actual by-id path of your USB drive. # Find it: ls -la /dev/disk/by-id/ | grep -v part device = "/dev/disk/by-label/homey-data"; mountPoint = "/mnt/data"; fsType = "ext4"; }; # ------------------------------------------------------------------------- # Services enabled on this host # ------------------------------------------------------------------------- # Auth stack (run these together — authelia depends on openldap) homey.openldap.enable = true; homey.authelia.enable = true; # Productivity homey.gitea.enable = true; homey.nextcloud.enable = true; homey.phpldapadmin.enable = true; # Media (enable when ready) homey.jellyfin.enable = false; homey.transmission.enable = false; # Documents and recipes homey.paperless.enable = true; homey.mealie.enable = true; # Reverse proxy + Cloudflare homey.caddy.enable = true; homey.cloudflared.enable = true; # Nix binary cache homey.attic.enable = true; nix.settings = { substituters = lib.mkAfter [ "https://attic.zakobar.com/main" ]; trusted-public-keys = lib.mkAfter [ "main:9SZt/6plBU7jjQzz90J7O011I13hmJvOMYouxNqExNQ=" ]; }; # CI/CD homey.giteaRunner.enable = true; # Eurovision voting app homey.eurovote.enable = true; # Monitoring stack homey.uptimeKuma.enable = true; homey.ntfy.enable = true; # Generate with: ssh admin@192.168.1.100 'sudo ntfy webpush keys' # Add private key to sops: ntfy/web_push_private_key homey.ntfy.webPushPublicKey = "BE2qZVa3JEF741WTPtLevyhfP0I8bV0sD2a9-_y9NoyC40sgLpQi7bcoZesBwZEpRz8oiTVuoUFnHbckAsBQI5U"; homey.ntfy.webPushEmail = "aner@zakobar.com"; homey.monitoring.enable = true; # Backups homey.backup.enable = true; # Where to send restic backups — set to your backup destination: # "sftp:user@nas.local:/backups/homey" # "b2:your-bucket-name:homey" # "rclone:remote:homey" homey.backup.repository = "s3:https://s3.us-east-005.backblazeb2.com/zakobar-home-backup"; # ------------------------------------------------------------------------- # Reliability hardening # ------------------------------------------------------------------------- # Hardware watchdog — auto-reboot if the system hangs (e.g. blocked USB I/O). # bcm2835_wdt exposes /dev/watchdog; systemd pets it every runtimeTime/2. # If systemd itself stops responding, the hardware resets the Pi after 20s. boot.kernelModules = [ "bcm2835_wdt" ]; systemd.watchdog = { runtimeTime = "300s"; # 5 min — generous window for boot I/O storm on USB drive rebootTime = "360s"; }; # Disable WiFi power save — the brcmfmac driver on RPi4 lets the chip sleep, # causing it to miss packets and drop the connection under low traffic. # Run once when the wlan0 interface appears (and on every re-plug/reconnect). systemd.services.wifi-disable-power-save = { description = "Disable WiFi power management on wlan0"; wantedBy = [ "multi-user.target" ]; after = [ "sys-subsystem-net-devices-wlan0.device" ]; bindsTo = [ "sys-subsystem-net-devices-wlan0.device" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = "${pkgs.iw}/bin/iw dev wlan0 set power_save off"; }; }; # Network watchdog — if the LAN gateway becomes unreachable, restart # wpa_supplicant to force a fresh association. If the link is still # dead 30 s later, reboot so the hardware watchdog doesn't have to. # Runs every 2 min starting 5 min after boot. systemd.services.network-watchdog = { description = "Network connectivity watchdog"; after = [ "network-online.target" ]; serviceConfig = { Type = "oneshot"; ExecStart = pkgs.writeShellScript "network-watchdog" '' gateway="192.168.1.1" if ! ${pkgs.iputils}/bin/ping -c 3 -W 10 "$gateway" > /dev/null 2>&1; then echo "Gateway $gateway unreachable — restarting wpa_supplicant" systemctl restart wpa_supplicant.service sleep 30 if ! ${pkgs.iputils}/bin/ping -c 3 -W 10 "$gateway" > /dev/null 2>&1; then echo "Still unreachable after wpa_supplicant restart — rebooting" systemctl reboot fi fi ''; }; }; systemd.timers.network-watchdog = { description = "Periodic network connectivity check"; wantedBy = [ "timers.target" ]; timerConfig = { OnBootSec = "5min"; OnUnitActiveSec = "2min"; Persistent = true; }; }; # Compressed in-RAM swap via zstd. Pages evicted from RAM are compressed # (~3:1 ratio) and stored in a 25% RAM region (~2 GB) rather than written # to disk. Gives the OOM killer breathing room under PHP upload spikes. # CPU overhead is negligible during normal operation. zramSwap = { enable = true; algorithm = "zstd"; memoryPercent = 25; }; # hdparm -B udev rule removed: USB-SATA bridges often don't support APM # commands and hdparm can hang indefinitely, causing boot-time crashes. environment.systemPackages = [ pkgs.hdparm pkgs.tmux ]; systemd.services.nextcloud-generate-previews = { description = "Generate missing Nextcloud preview thumbnails"; after = [ "podman-nextcloud.service" ]; requires = [ "podman-nextcloud.service" ]; serviceConfig = { Type = "oneshot"; ExecStart = "${pkgs.podman}/bin/podman exec -u www-data nextcloud php occ preview:generate-all"; }; }; # ------------------------------------------------------------------------- # Local DNS overrides (optional — makes LAN clients hit the Pi directly # instead of going through Cloudflare for *.zakobar.com) # ------------------------------------------------------------------------- # If you run Pi-hole or Adguard, add these records there instead. # networking.extraHosts = '' # 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 # ''; }