# This is the NixOS configuration for richmond-server. # It is a server and does not require a GUI. { config , pkgs , lib , inputs , # Re-add inputs here ... # specialArgs from flake.nix }: { imports = [ # Import your common modules here ../../modules/core/common.nix ../../modules/core/management.nix # ../../modules/some-common-module.nix (import ../../modules/storage/disko.nix { inherit inputs lib config; diskoConfigPath = ./disko-config.nix; }) ./hardware-configuration.nix # Import hardware configuration ../../modules/core/podman.nix ../../modules/services/ntfy.nix ]; sops.secrets = { "richmond-server/tailscale-authkey" = { mode = "0600"; owner = "root"; }; "richmond-server/pihole-password" = { }; "richmond-server/castopod-api-password" = { key = "richmond-server/castopod-api-password"; }; "richmond-server/mcf-notices-env" = { }; "richmond-server/castopod-env" = { }; "richmond-server/castopod-api-env" = { }; "users/petere-password" = { neededForUsers = true; }; }; # Set your hostname networking.hostName = "richmond-server"; # Set explicit nameservers for the host to ensure it can reach registries # regardless of local container or Tailscale DNS state. networking.nameservers = [ "1.1.1.1" "8.8.8.8" ]; # Allow rootless containers to bind to privileged ports boot.kernel.sysctl = { "net.ipv4.ip_unprivileged_port_start" = 53; }; # Enable SSH services.openssh.enable = true; # Backup Server Configuration # Borg Backup removed - x1carbon migrated to Backrest on homeserver-1 # Richmond-server now serves as backup target for Backrest (homeserver-1) only # Create a dedicated system user for Backrest backups from homeserver-1 users.users.restic-homeserver1 = { isSystemUser = true; group = "restic-homeserver1"; shell = pkgs.zsh; openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICwGZFEr6OMm7SIPrYlt6wuuesvlmBIezqQVDxhXOHjD restic@homeserver-1" ]; }; # Admin user (shared definition in modules/core/users.nix) my.users.petere = { hashedPasswordFile = config.sops.secrets."users/petere-password".path; }; users.groups = { restic-homeserver1 = { }; }; # Disable systemd-resolved to prevent it from binding to port 53, # allowing Pi-hole to take over DNS duties. services.resolved.enable = false; # The 'services.resolved.extraConfig' option is deprecated and now causes a build failure. # The 'services.resolved.enable = false;' line above is sufficient to free up port 53 for Pi-hole. # services.resolved.extraConfig = '' # DNSStubListener=no # ''; # Enable Tailscale (globally enabled in modules/core/settings.nix) services.tailscale.authKeyFile = config.sops.secrets."richmond-server/tailscale-authkey".path; # NOTE: no "--ssh" here. Tailscale SSH would intercept port 22 on the tailnet # and enforce the tailnet ACL, blocking the restic-homeserver1 user that # Backrest (homeserver-1) uses for backups. Regular OpenSSH handles SSH instead. services.tailscale.extraUpFlags = [ "--accept-dns=false" ]; # Glances system monitor - exposed to the tailnet so the Homepage # dashboard on homeserver-1 can display real-time stats for this machine. services.glances = { enable = true; port = 61208; extraArgs = [ "--webserver" ]; }; # VLAN configuration networking.vlans = { management = { id = 5; interface = "enp1s0"; }; office = { id = 10; interface = "enp1s0"; }; tech = { id = 20; interface = "enp1s0"; }; advice = { id = 30; interface = "enp1s0"; }; words = { id = 40; interface = "enp1s0"; }; general = { id = 50; interface = "enp1s0"; }; printers = { id = 60; interface = "enp1s0"; }; }; networking.interfaces = { management.useDHCP = true; office.useDHCP = true; tech.useDHCP = true; advice.useDHCP = true; words.useDHCP = true; general.useDHCP = true; printers.useDHCP = true; }; # podman configuration is now in ../../modules/core/podman.nix # --- Pi-hole Container Configuration --- virtualisation.oci-containers.containers.pihole = { image = "docker.io/pihole/pihole:2026.07.2"; autoStart = true; ports = [ "53:53/tcp" "53:53/udp" "80:80/tcp" # Web UI on port 80 ]; volumes = [ "/var/lib/pihole/etc-pihole:/etc/pihole" "/var/lib/pihole/etc-dnsmasq.d:/etc/dnsmasq.d" ]; environment = { TZ = "Europe/London"; DNSMASQ_LISTENING = "all"; PIHOLE_INTERFACE = "all"; }; environmentFiles = [ "/run/pihole-env" ]; extraOptions = [ "--cap-add=NET_ADMIN" # Needed for DHCP features ]; }; # --- MCFNotices Container Configuration --- virtualisation.oci-containers.containers.mcf-notices = { image = "docker.io/pedley/slideshow-builder:2.6.0"; autoStart = true; volumes = [ # Persistent runtime config (notices-data) — app stores settings.db at # /data/settings.db inside the container. "/home/petere/mcf_data:/data" ]; environmentFiles = [ config.sops.secrets."richmond-server/mcf-notices-env".path ]; extraOptions = [ "--no-healthcheck" ]; }; # Improve container service resilience and ensure network is ready before starting. systemd.services.podman-pihole = { after = [ "network-online.target" "pihole-data-dirs.service" ]; wants = [ "network-online.target" ]; preStart = '' ${pkgs.podman}/bin/podman rm -f pihole || true ''; unitConfig = { StartLimitIntervalSec = 0; }; serviceConfig = { Restart = lib.mkForce "always"; RestartSec = "10s"; }; }; # Rendered by sops-nix from the pihole-password secret; the Pi-hole container # reads this file for FTLCONF_webserver_api_password. sops.templates."pihole-env" = { content = '' FTLCONF_webserver_api_password=${config.sops.placeholder."richmond-server/pihole-password"} ''; path = "/run/pihole-env"; mode = "0600"; }; services.mysql = { enable = true; package = pkgs.mariadb; settings = { mysqld = { bind-address = "0.0.0.0"; # Allow container access character-set-server = "utf8mb4"; collation-server = "utf8mb4_unicode_ci"; }; client = { default-character-set = "utf8mb4"; }; }; }; # --- Castopod Container Configuration --- virtualisation.oci-containers.containers.castopod = { image = "castopod/castopod:1.15.5"; autoStart = true; volumes = [ "/var/lib/castopod/media:/var/www/html/public/media" "/var/lib/castopod/writable:/var/www/html/writable" "/var/lib/castopod/plugins:/var/www/html/plugins" "/var/lib/castopod/plugins/mcf/episode-filter:/var/www/html/plugins/mcf/episode-filter" "/var/lib/castopod/public/plugins/mcf/episode-filter/assets:/var/www/html/public/plugins/mcf/episode-filter/assets" ]; environmentFiles = [ config.sops.secrets."richmond-server/castopod-env".path "/run/castopod-api-env" ]; extraOptions = [ "--network=host" ]; # We use a "configuration injection" trick here via the /run/castopod-api-env file. }; # Improve Castopod container resilience and ensure directories exist systemd.services.podman-castopod = { after = [ "network-online.target" "castopod-data-dirs.service" ]; wants = [ "network-online.target" ]; unitConfig = { StartLimitIntervalSec = 0; }; serviceConfig = { Restart = lib.mkForce "always"; RestartSec = "10s"; }; }; # NOTE: Castopod is an experiment. This env file is a "configuration injection" # used to pass restapi settings; the original preStart wrote a single line with # literal \n sequences. This template writes real newlines instead — review # Castopod's behaviour on the next pass. sops.templates."castopod-api-env" = { content = '' CP_DATABASE_PREFIX=cp_" restapi.enabled=true restapi.basicAuth=true restapi.basicAuthUsername=pedley restapi.basicAuthPassword=${config.sops.placeholder."richmond-server/castopod-api-password"} dummy=" ''; path = "/run/castopod-api-env"; mode = "0600"; }; # Create persistent data directories for Castopod. systemd.services.castopod-data-dirs = { description = "Create data directories for Castopod container"; wantedBy = [ "multi-user.target" ]; before = [ "podman-castopod.service" ]; serviceConfig.Type = "oneshot"; script = '' mkdir -p /var/lib/castopod/media mkdir -p /var/lib/castopod/writable mkdir -p /var/lib/castopod/plugins mkdir -p /var/lib/castopod/public/plugins/mcf/episode-filter/assets # Ensure the container's www-data user (UID 33) can write to these chown -R 33:33 /var/lib/castopod/media /var/lib/castopod/writable /var/lib/castopod/plugins /var/lib/castopod/public chmod -R 775 /var/lib/castopod/media /var/lib/castopod/writable /var/lib/castopod/plugins /var/lib/castopod/public ''; }; # --- ntfy Configuration --- services.ntfy-container = { enable = true; port = 8085; }; # Open firewall ports for Pi-hole and ntfy globally. networking.firewall.enable = true; networking.firewall.allowedTCPPorts = [ 53 80 8085 ]; networking.firewall.allowedUDPPorts = [ 53 ]; # Restrict Castopod (8080) to only be accessible via Tailscale networking.firewall.interfaces.tailscale0.allowedTCPPorts = [ 8080 61208 ]; # Create persistent data directories for Pi-hole. systemd.services.pihole-data-dirs = { description = "Create data directories for Pi-hole container"; wantedBy = [ "multi-user.target" ]; before = [ "podman-pihole.service" ]; serviceConfig.Type = "oneshot"; script = '' mkdir -p /var/lib/pihole/etc-pihole mkdir -p /var/lib/pihole/etc-dnsmasq.d ''; }; # Create the persistent data directory for the MCFNotices container. systemd.services.mcf-notices-data-dirs = { description = "Create data directory for MCFNotices container"; wantedBy = [ "multi-user.target" ]; before = [ "podman-mcf-notices.service" ]; serviceConfig.Type = "oneshot"; script = '' mkdir -p /home/petere/mcf_data chown petere:users /home/petere/mcf_data chmod 775 /home/petere/mcf_data ''; }; # Ensure the base directory for backups exists and has correct permissions systemd.tmpfiles.rules = [ # Borg backup directories removed (migrated to Backrest) "d /home/backup/restic/immich-backup 0700 restic-homeserver1 restic-homeserver1 -" ]; nix.settings.trusted-users = [ "root" "petere" ]; }