Files
Nix-Vibe/hosts/richmond-server/configuration.nix
T
petere eb08cd4282 Nix-Vibe public snapshot (squashed history)
Current state of main at 0240060 feat(hp-laptop): install TeleportFling from its flake. History intentionally
collapsed to a single commit; this repo mirrors only the latest state.
2026-09-19 13:53:39 +01:00

375 lines
11 KiB
Nix

# 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"
];
}