375 lines
11 KiB
Nix
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"
|
|
];
|
|
}
|