feat(hetzner): add Stalwart, restic backups, and virtualcam

This commit is contained in:
2026-08-11 15:36:40 +02:00
parent 62c70dab19
commit f806506d9a
13 changed files with 586 additions and 63 deletions
+78 -27
View File
@@ -1,41 +1,92 @@
{pkgs, ...}: let
backupScript = pkgs.writeShellScript "weekly-backup" ''
BACKUP_DIR="/home/admin/backups"
SRC="/home/admin"
DATE=$(date +%Y-%m-%dT%H-%M-%S)
FILENAME="weekly-backup-$DATE.tar.gz"
{
pkgs,
lib,
...
}: let
# sops-encrypted secrets (single file holds all service secrets).
secretsFile = ../../secrets/secrets.yaml;
# Root-only runtime files restic reads from (0600 root).
runtimeDir = "/var/lib/restic";
passwordFile = "/var/lib/restic/.password";
environmentFile = "/var/lib/restic/environment";
# Backblaze B2 backend, per restic docs: b2:bucketname.
repo = "b2:hetzner-severijnse";
mkdir -p "$BACKUP_DIR"
# Backup everything under /home/admin EXCEPT:
# - The backups dir itself (infinite loop)
# - DMS mail data (GBs of email, backed up separately)
# - NixOS-managed service data (at their own paths below)
tar czf "$BACKUP_DIR/$FILENAME" \
--exclude="$BACKUP_DIR" \
--exclude="/home/admin/backups" \
--exclude="/home/admin/dms/mail-data" \
--exclude="/home/admin/dms/mail-state" \
"$SRC"
# Materialize the restic password and B2 credentials from sops into
# root-only files, so secrets are never world-readable in the Nix store.
writeSecrets = pkgs.writeShellScript "restic-write-secrets" ''
set -euo pipefail
mkdir -p ${runtimeDir}
${pkgs.sops}/bin/sops \
--decrypt --extract '["restic_password"]' \
--input-type yaml --output-type yaml ${secretsFile} \
| tr -d '\n' > "${passwordFile}"
chmod 0600 "${passwordFile}"
# Prune backups older than 14 days
find "$BACKUP_DIR" -name "weekly-backup-*" -mtime +14 -delete
: > "${environmentFile}"
chmod 0600 "${environmentFile}"
${pkgs.sops}/bin/sops --decrypt --input-type yaml --output-type yaml ${secretsFile} |
${pkgs.gnused}/bin/sed -nE \
's/^b2_key_id: (.*)/B2_ACCOUNT_ID=\1/p; s/^b2_application_key: (.*)/B2_ACCOUNT_KEY=\1/p' \
>> "${environmentFile}"
'';
in {
systemd.services.weekly-backup = {
description = "Weekly backup of home directory";
path = with pkgs; [coreutils gnutar findutils];
systemd.services.restic-password = {
description = "Materialize restic repository password and B2 credentials from sops";
wantedBy = ["multi-user.target"];
# The age key lives in /etc/age/keys.txt; the service must know where it is
# and needs a HOME for age to report its user config directory.
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
serviceConfig = {
Type = "oneshot";
ExecStart = "${backupScript}";
User = "root";
Environment = ["HOME=/root"];
ExecStart = "${writeSecrets}";
};
};
systemd.timers.weekly-backup = {
wantedBy = ["timers.target"];
# B2 credentials are supplied via environmentFile (B2_ACCOUNT_ID / B2_ACCOUNT_KEY),
# matching the official module example which combines `repository` and `environmentFile`.
services.restic.backups.localbackup = {
repository = "b2:hetzner-severijnse";
environmentFile = environmentFile;
passwordFile = passwordFile;
initialize = true;
paths = [
"/home/admin"
"/var/lib/postgresql"
"/var/lib/gitea"
"/var/lib/caddy"
"/var/lib/virtualcam"
"/var/lib/coredns"
"/etc/nixos"
];
exclude = [
"/home/admin/backups"
"/home/admin/dms/mail-logs"
"/home/admin/.opencode"
"/home/admin/.local"
"/home/admin/.npm"
"/home/admin/.config"
"*.log"
"*.log.*"
"**/.cache"
];
timerConfig = {
OnCalendar = "Mon *-*-* 03:00:00";
Persistent = true;
RandomizedDelaySec = "15m";
};
pruneOpts = [
"--keep-daily 7"
"--keep-weekly 4"
"--keep-monthly 6"
];
runCheck = true;
};
}
# The backup must never run before the secrets exist.
systemd.services."restic-backups-localbackup" = {
requires = ["restic-password.service"];
after = ["restic-password.service"];
};
}