feat(hetzner): add Stalwart, restic backups, and virtualcam
This commit is contained in:
@@ -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"];
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user