diff --git a/hosts/nixos/apollo/configuration.nix b/hosts/nixos/apollo/configuration.nix index 07e5a15..4693cab 100644 --- a/hosts/nixos/apollo/configuration.nix +++ b/hosts/nixos/apollo/configuration.nix @@ -15,6 +15,20 @@ immich.enable = true; actual.enable = true; # seafile.enable = true; + + # Backup server - exposes data for pull-based backups + backup-server = { + enable = true; + zfsSnapshots = { + enable = true; + # Keep snapshots for point-in-time recovery + frequent = 4; # 4 x 15min = 1 hour of frequent snapshots + hourly = 24; # 24 hours + daily = 7; # 1 week + weekly = 4; # 1 month + monthly = 12; # 1 year + }; + }; }; hardware = { diff --git a/hosts/nixos/artemis/configuration.nix b/hosts/nixos/artemis/configuration.nix index 73e9335..497b7cb 100644 --- a/hosts/nixos/artemis/configuration.nix +++ b/hosts/nixos/artemis/configuration.nix @@ -14,6 +14,20 @@ desktopEnvironment.gnome.enable = true; hardware.systemd-boot.enable = false; # Mobile devices use different bootloader programs.graphical.enable = false; + + services = { + # Backup client - pulls vaultwarden backup from apollo + backup-client = { + enable = true; + backups = { + apollo-vaultwarden = { + remoteHost = "apollo"; + localPath = "/var/backups/apollo-vaultwarden"; + services = [ "vaultwarden" ]; + }; + }; + }; + }; }; # mobile-nixos needs aliases (uses nettools instead of net-tools) diff --git a/hosts/nixos/pochita/configuration.nix b/hosts/nixos/pochita/configuration.nix index 4b2ec0c..c601c37 100644 --- a/hosts/nixos/pochita/configuration.nix +++ b/hosts/nixos/pochita/configuration.nix @@ -18,6 +18,18 @@ wanikani-bypass-lessons.enable = true; wanikani-fetch-data.enable = true; wanikani-stats.enable = true; + + # Backup client - pulls full backup from apollo + backup-client = { + enable = true; + backups = { + apollo-full = { + remoteHost = "apollo"; # Tailscale hostname + localPath = "/var/backups/apollo"; + fullBackup = true; + }; + }; + }; }; desktopEnvironment.plasma.enable = true; programs.graphical.enable = false; diff --git a/hosts/nixos/tartarus/configuration.nix b/hosts/nixos/tartarus/configuration.nix index 9e6222b..6b17b8e 100644 --- a/hosts/nixos/tartarus/configuration.nix +++ b/hosts/nixos/tartarus/configuration.nix @@ -10,6 +10,20 @@ emulation.aarch64.enable = true; hardware.sound.enable = true; programs.steam.enable = true; + + services = { + # Backup client - pulls vaultwarden backup from apollo + backup-client = { + enable = true; + backups = { + apollo-vaultwarden = { + remoteHost = "apollo"; + localPath = "/var/backups/apollo-vaultwarden"; + services = [ "vaultwarden" ]; + }; + }; + }; + }; }; networking.hostName = "tartarus"; diff --git a/hosts/nixos/wallfacer/configuration.nix b/hosts/nixos/wallfacer/configuration.nix index 39fb62a..81a1589 100644 --- a/hosts/nixos/wallfacer/configuration.nix +++ b/hosts/nixos/wallfacer/configuration.nix @@ -8,6 +8,18 @@ hydra.enable = true; atticd.enable = true; cloudflared.enable = true; + + # Backup client - pulls full backup from apollo + backup-client = { + enable = true; + backups = { + apollo-full = { + remoteHost = "apollo"; # Tailscale hostname + localPath = "/var/backups/apollo"; + fullBackup = true; + }; + }; + }; }; }; diff --git a/hosts/nixos/ymir/configuration.nix b/hosts/nixos/ymir/configuration.nix index 90a67b5..06d2592 100644 --- a/hosts/nixos/ymir/configuration.nix +++ b/hosts/nixos/ymir/configuration.nix @@ -25,6 +25,18 @@ }; services = { ollama.enable = true; + + # Backup client - pulls vaultwarden backup from apollo + backup-client = { + enable = true; + backups = { + apollo-vaultwarden = { + remoteHost = "apollo"; + localPath = "/var/backups/apollo-vaultwarden"; + services = [ "vaultwarden" ]; + }; + }; + }; }; i18n.enable = true; }; diff --git a/modules/nixos/options.nix b/modules/nixos/options.nix index 1c5ed84..4653f24 100644 --- a/modules/nixos/options.nix +++ b/modules/nixos/options.nix @@ -147,6 +147,100 @@ wanikani-bypass-lessons.enable = lib.mkEnableOption "wanikani-bypass-lessons"; wanikani-fetch-data.enable = lib.mkEnableOption "wanikani-fetch-data"; wanikani-stats.enable = lib.mkEnableOption "wanikani-stats"; + + # Backup server - exposes data for pull-based backups + backup-server = { + enable = lib.mkEnableOption "backup server (exposes data for pull-based backups)"; + + zfsSnapshots = { + enable = lib.mkEnableOption "automatic ZFS snapshots of /persist"; + + frequent = lib.mkOption { + type = lib.types.int; + default = 4; + description = "Number of frequent (15-min) snapshots to keep"; + }; + + hourly = lib.mkOption { + type = lib.types.int; + default = 24; + description = "Number of hourly snapshots to keep"; + }; + + daily = lib.mkOption { + type = lib.types.int; + default = 7; + description = "Number of daily snapshots to keep"; + }; + + weekly = lib.mkOption { + type = lib.types.int; + default = 4; + description = "Number of weekly snapshots to keep"; + }; + + monthly = lib.mkOption { + type = lib.types.int; + default = 12; + description = "Number of monthly snapshots to keep"; + }; + }; + }; + + # Backup client - pulls backups from remote servers + backup-client = { + enable = lib.mkEnableOption "backup client (pulls data from remote servers)"; + + backups = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options = { + remoteHost = lib.mkOption { + type = lib.types.str; + description = "Remote host to backup from (e.g., apollo.tail-scale.ts.net)"; + }; + + remoteUser = lib.mkOption { + type = lib.types.str; + default = "root"; + description = "Remote user for SSH connection"; + }; + + localPath = lib.mkOption { + type = lib.types.str; + description = "Local path where backups will be stored"; + }; + + fullBackup = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to backup the entire /persist directory"; + }; + + services = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of services to backup (e.g., ['vaultwarden', 'immich'])"; + example = [ + "vaultwarden" + "immich" + "forgejo" + ]; + }; + + schedule = lib.mkOption { + type = lib.types.str; + default = "daily"; + description = "Backup schedule (systemd timer format)"; + example = "daily"; + }; + }; + } + ); + default = { }; + description = "Backup configurations"; + }; + }; }; # Hardware diff --git a/modules/nixos/services/backup-client.nix b/modules/nixos/services/backup-client.nix new file mode 100644 index 0000000..5cc48a5 --- /dev/null +++ b/modules/nixos/services/backup-client.nix @@ -0,0 +1,95 @@ +# Backup client - pulls backups from remote servers via rsync +# Supports full backups and selective service backups over Tailscale +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.osbmModules.services.backup-client; + + # Create a backup job for each configured backup + makeBackupService = + name: backupCfg: + let + # Build rsync paths based on what we're backing up + sourcePaths = + if backupCfg.fullBackup then + [ "/persist" ] # Full backup of everything + else if backupCfg.services != [ ] then + # Selective service backups + map (service: "/persist/var/lib/${service}") backupCfg.services + ++ lib.optional (builtins.elem "vaultwarden" backupCfg.services) "/persist/backup/vaultwarden" + else + [ ]; # Empty list if nothing to backup + + # Rsync command for each source path + rsyncCommands = map (source: '' + echo "Backing up ${source} from ${backupCfg.remoteHost}..." + ${pkgs.rsync}/bin/rsync -avz --delete \ + -e "${pkgs.openssh}/bin/ssh -o StrictHostKeyChecking=accept-new" \ + ${backupCfg.remoteUser}@${backupCfg.remoteHost}:${source}/ \ + ${backupCfg.localPath}/${builtins.baseNameOf source}/ + '') sourcePaths; + in + { + description = "Backup ${name} from ${backupCfg.remoteHost}"; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + serviceConfig = { + Type = "oneshot"; + User = "root"; + }; + + script = '' + set -e + + # Create backup directory if it doesn't exist + mkdir -p ${backupCfg.localPath} + + # Run rsync for each source path + ${lib.concatStringsSep "\n" rsyncCommands} + + echo "Backup ${name} completed successfully at $(date)" + ''; + }; + + makeBackupTimer = name: backupCfg: { + description = "Timer for ${name} backup"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = backupCfg.schedule; + Persistent = true; + RandomizedDelaySec = "30m"; # Randomize to avoid all backups running at once + }; + }; +in +{ + config = lib.mkIf cfg.enable { + # Create systemd services for each backup + systemd.services = lib.mapAttrs' ( + name: backupCfg: lib.nameValuePair "backup-${name}" (makeBackupService name backupCfg) + ) cfg.backups; + + # Create systemd timers for each backup + systemd.timers = lib.mapAttrs' ( + name: backupCfg: lib.nameValuePair "backup-${name}" (makeBackupTimer name backupCfg) + ) cfg.backups; + + # Ensure rsync and openssh are available + environment.systemPackages = with pkgs; [ + rsync + openssh + ]; + + # Ensure Tailscale is enabled for secure connections + assertions = [ + { + assertion = config.services.tailscale.enable; + message = "backup-client requires Tailscale to be enabled for secure connections"; + } + ]; + }; +} diff --git a/modules/nixos/services/backup-server.nix b/modules/nixos/services/backup-server.nix new file mode 100644 index 0000000..f0ad322 --- /dev/null +++ b/modules/nixos/services/backup-server.nix @@ -0,0 +1,33 @@ +# Backup server - exposes data for backup clients to pull +# Enables ZFS snapshots for local point-in-time recovery +{ + config, + lib, + ... +}: +let + cfg = config.osbmModules.services.backup-server; +in +{ + config = lib.mkIf cfg.enable { + # Enable ZFS auto-snapshots if requested + services.zfs.autoSnapshot = lib.mkIf cfg.zfsSnapshots.enable { + enable = true; + inherit (cfg.zfsSnapshots) + frequent + hourly + daily + weekly + monthly + ; + }; + + # Ensure SSH is enabled for backup access + assertions = [ + { + assertion = config.services.openssh.enable; + message = "backup-server requires openssh to be enabled"; + } + ]; + }; +} diff --git a/modules/nixos/services/default.nix b/modules/nixos/services/default.nix index ea9cb1f..ff7e038 100644 --- a/modules/nixos/services/default.nix +++ b/modules/nixos/services/default.nix @@ -3,6 +3,8 @@ ./actual.nix ./anubis.nix ./atticd.nix + ./backup-client.nix + ./backup-server.nix ./cloudflared.nix ./ollama.nix ./openssh.nix