Reorganize services/ into category subfolders

Group service modules by category (media, network, vpn, identity,
dev, desktop) to make the growing services/ dir easier to navigate.
containers.nix stays at the top level since it's a shared backend,
not a single-category service.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 06:08:43 +02:00
co-authored by Claude Sonnet 5
parent 4679afa505
commit d7a66f3e3b
27 changed files with 41 additions and 38 deletions
+13
View File
@@ -0,0 +1,13 @@
{ ... }:
# mDNS/DNS-SD (advertise the host + shares on the LAN).
{
services.avahi = {
enable = true;
nssmdns4 = true;
publish = {
enable = true;
userServices = true;
};
};
}
+9
View File
@@ -0,0 +1,9 @@
{ ... }:
# Caddy reverse proxy — base enable + open the web ports.
# Each host adds its own `services.caddy.virtualHosts.<name>` (LAN names on
# jupiter, public domains with automatic HTTPS on the neptun).
{
services.caddy.enable = true;
networking.firewall.allowedTCPPorts = [ 80 443 ];
}
+51
View File
@@ -0,0 +1,51 @@
{ pkgs, lib, ... }:
# Pi-hole via the official container (the native nixpkgs pihole-ftl module
# segfaults on aarch64 / Pi 3B+). Host networking so it can serve DHCP and reach
# the host's unbound at 127.0.0.1:5335. Config via FTLCONF_* env vars (pihole v6)
# — these override pihole.toml on every start, so it stays effectively
# declarative. The web admin password is added from sops in the host config.
{
virtualisation.podman = {
enable = true;
dockerCompat = true;
};
virtualisation.oci-containers = {
backend = "podman";
containers.pihole = {
image = "pihole/pihole:2026.07.2"; # validated in mercury-vm
autoStart = true;
extraOptions = [
"--network=host" # DHCP broadcast + host unbound on 127.0.0.1
"--cap-add=NET_ADMIN" # DHCP
"--cap-add=NET_RAW" # DNS engine (dnsmasq) — REQUIRED
"--cap-add=SYS_NICE"
"--cap-add=CHOWN" # entrypoint chowns /etc/pihole
];
volumes = [ "/var/lib/pihole:/etc/pihole" ]; # persist config/state
environment = {
TZ = "Europe/Berlin";
FTLCONF_dns_upstreams = "127.0.0.1#5335"; # host unbound (recursive)
FTLCONF_dns_listeningMode = "all"; # serve the LAN
FTLCONF_dns_domain = "sol";
FTLCONF_dhcp_active = "true";
FTLCONF_dhcp_start = "10.0.0.50";
FTLCONF_dhcp_end = "10.0.0.200";
FTLCONF_dhcp_router = "10.0.0.1";
FTLCONF_dhcp_leaseTime = "1h";
# Arrays (format validated in the VM): static lease + local DNS records.
FTLCONF_dhcp_hosts = "00:e0:4c:3c:a3:1f,10.0.0.20,jupiter";
FTLCONF_dns_hosts = "10.0.0.10 mercury.sol;10.0.0.20 jupiter.sol";
# Wildcard: any <service>.jupiter.sol -> jupiter (caddy routes by hostname).
FTLCONF_misc_dnsmasq_lines = "address=/jupiter.sol/10.0.0.20";
};
};
};
# Bind-mount source must exist (podman won't create it).
systemd.tmpfiles.rules = [ "d /var/lib/pihole 0755 root root -" ];
networking.firewall.allowedTCPPorts = [ 53 80 ];
networking.firewall.allowedUDPPorts = [ 53 67 ];
}
+53
View File
@@ -0,0 +1,53 @@
{ pkgs, ... }:
# Samba file server for the /mnt/data share + password provisioning.
# Import on any host that should serve the NAS.
{
services.samba = {
enable = true;
openFirewall = true;
settings = {
global = {
"workgroup" = "WORKGROUP";
"server string" = "jupiter";
"security" = "user";
};
data = {
"path" = "/mnt/data";
"browseable" = "yes";
"read only" = "no";
"guest ok" = "no";
"valid users" = "darman";
};
};
};
# Samba keeps its own NTLM password DB, separate from the system password;
# `services.samba` never sets it, so logins fail until provisioned. Runs
# AFTER samba-smbd so its state dir exists — an activation script runs too
# early and smbpasswd fails to init the passdb. Reads a single-line
# password from the first file that exists:
# Real host: /run/secrets/samba_password (sops-nix, see secrets.nix)
# VM test: /etc/samba/smb-password (plaintext, see vm.nix)
# smbpasswd prompts new + confirm, so the value is fed twice.
systemd.services.samba-smbpasswd = {
description = "Provision Samba password for darman";
after = [ "samba-smbd.service" ];
requires = [ "samba-smbd.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
for f in /run/secrets/samba_password /etc/samba/smb-password; do
if [ -f "$f" ]; then
pw=$(head -n1 "$f")
printf '%s\n%s\n' "$pw" "$pw" | ${pkgs.samba}/bin/smbpasswd -a -s darman
exit 0
fi
done
echo "no samba password source found" >&2
'';
};
}
+36
View File
@@ -0,0 +1,36 @@
{ ... }:
# Local recursive DNS resolver (privacy + DNSSEC). Your adblock DNS
# (pihole/AdGuard) forwards to this instead of a public upstream.
# Listens on 127.0.0.1:5335 — point the adblock engine's upstream there:
# AdGuard: dns.upstream_dns = [ "127.0.0.1:5335" ];
# pihole: upstream = "127.0.0.1#5335";
{
services.unbound = {
enable = true;
# Do NOT point resolv.conf at unbound: it listens on :5335, not :53, so
# that leaves the host with no resolver until pihole binds :53 — a
# boot-time deadlock. Host resolves via networking.nameservers instead.
resolveLocalQueries = false;
# NixOS manages the DNSSEC root trust anchor (unbound-anchor).
settings.server = {
interface = [ "127.0.0.1" ];
port = 5335;
access-control = [ "127.0.0.0/8 allow" ];
do-ip6 = "no"; # flip to yes if you resolve over IPv6
prefer-ip6 = "no";
# Privacy / hardening (standard pi-hole+unbound guide).
hide-identity = true;
hide-version = true;
harden-glue = true;
harden-dnssec-stripped = true;
use-caps-for-id = false;
qname-minimisation = true;
edns-buffer-size = 1232;
prefetch = true;
};
};
}