Personal Nix setup
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add basic backup module

+311
+4
lib/apps/default.nix
··· 3 3 type = "app"; 4 4 program = import ./genCerts.nix inputs; 5 5 }; 6 + restoreBackup = { 7 + type = "app"; 8 + program = import ./restoreBackup.nix inputs; 9 + }; 6 10 }
+116
lib/apps/restoreBackup.nix
··· 1 + # Restore a backup from R2. 2 + # 3 + # On the server (defaults auto-detected): 4 + # restoreBackup vaultwarden # list available backups 5 + # restoreBackup vaultwarden 2026-04-05 # restore specific date 6 + # 7 + # On a fresh machine (specify paths explicitly): 8 + # restoreBackup --config ./rclone.conf --env ./secrets.env vaultwarden 2026-04-05 9 + # 10 + # Default paths (managed by NixOS): 11 + # Config: /etc/rclone-backup.conf (modules/server/backup.nix) 12 + # Secrets: /run/secrets/rclone-backup.env (agenix, modules/server/backup.nix) 13 + 14 + { pkgs, ... }: 15 + 16 + let 17 + rclone = "${pkgs.rclone}/bin/rclone"; 18 + coreutils = pkgs.coreutils; 19 + 20 + defaultConf = "/etc/rclone-backup.conf"; 21 + defaultEnv = "/run/secrets/rclone-backup.env"; 22 + in 23 + toString (pkgs.writers.writeBash "restoreBackup" '' 24 + set -euo pipefail 25 + 26 + REMOTE="r2-encrypted" 27 + CONFIG="${defaultConf}" 28 + ENV_FILE="${defaultEnv}" 29 + SERVICE="" 30 + DATE="" 31 + 32 + usage() { 33 + echo "Usage: restoreBackup [--config <rclone.conf>] [--env <secrets.env>] <service> [date]" 34 + echo "" 35 + echo " --config Path to rclone config (default: ${defaultConf})" 36 + echo " --env Path to rclone secrets env file (default: ${defaultEnv})" 37 + echo " service Name of the backup to restore (e.g. vaultwarden, tangled)" 38 + echo " date Backup date (YYYY-MM-DD) for snapshot backups. Omit to list dates or download synced backups." 39 + echo "" 40 + echo "Snapshot backups (sqlite) are stored by date. Synced backups (directories) are downloaded directly." 41 + echo "" 42 + echo "Examples:" 43 + echo " restoreBackup vaultwarden # list available snapshot dates" 44 + echo " restoreBackup vaultwarden 2026-04-05 # restore specific snapshot" 45 + echo " restoreBackup tangled # download synced backup" 46 + echo " restoreBackup --config ./rclone.conf --env ./secrets.env tangled" 47 + exit 1 48 + } 49 + 50 + while [[ $# -gt 0 ]]; do 51 + case "$1" in 52 + --config) CONFIG="$2"; shift 2 ;; 53 + --env) ENV_FILE="$2"; shift 2 ;; 54 + -h|--help) usage ;; 55 + *) if [[ -z "$SERVICE" ]]; then SERVICE="$1"; else DATE="$1"; fi; shift ;; 56 + esac 57 + done 58 + 59 + if [[ -z "$SERVICE" ]]; then 60 + usage 61 + fi 62 + 63 + if [[ ! -f "$CONFIG" ]]; then 64 + echo "Error: rclone config not found at $CONFIG. Use --config to specify one." 65 + exit 1 66 + fi 67 + 68 + if [[ -f "$ENV_FILE" ]]; then 69 + set -a 70 + source "$ENV_FILE" 71 + set +a 72 + else 73 + echo "Warning: No env file at $ENV_FILE. Rclone credentials must be set in environment." 74 + fi 75 + 76 + RCLONE="${rclone} --config $CONFIG" 77 + 78 + if [[ -n "$DATE" ]]; then 79 + # Snapshot restore (sqlite backups with date folders) 80 + echo "Downloading $SERVICE snapshot from $DATE..." 81 + OUT="/var/tmp/restore-$SERVICE-$DATE" 82 + ${coreutils}/bin/rm -rf "$OUT" 83 + ${coreutils}/bin/mkdir -p "$OUT" 84 + $RCLONE copy "$REMOTE/$SERVICE/$DATE/" "$OUT/" 85 + else 86 + # Check if this is a snapshot backup (has date subdirectories) or a synced backup 87 + SUBDIRS=$($RCLONE lsd "$REMOTE/$SERVICE/" 2>/dev/null | ${coreutils}/bin/awk '{print $NF}') 88 + if echo "$SUBDIRS" | ${coreutils}/bin/grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then 89 + echo "Available $SERVICE snapshots:" 90 + echo "$SUBDIRS" | ${coreutils}/bin/sed 's/^/ /' 91 + exit 0 92 + fi 93 + 94 + # Synced backup — download directly 95 + echo "Downloading $SERVICE synced backup..." 96 + OUT="/var/tmp/restore-$SERVICE" 97 + ${coreutils}/bin/rm -rf "$OUT" 98 + ${coreutils}/bin/mkdir -p "$OUT" 99 + $RCLONE copy "$REMOTE/$SERVICE/" "$OUT/" 100 + fi 101 + 102 + echo "" 103 + echo "Downloaded to: $OUT" 104 + echo "" 105 + echo "Contents:" 106 + ${coreutils}/bin/ls -la "$OUT/" 107 + echo "" 108 + echo "To restore, stop the service and copy the files into place. For example:" 109 + echo "" 110 + echo " systemctl stop $SERVICE" 111 + for item in "$OUT"/*; do 112 + base=$(${coreutils}/bin/basename "$item") 113 + echo " cp -a $OUT/$base /var/lib/$SERVICE/" 114 + done 115 + echo " systemctl start $SERVICE" 116 + '')
+5
machines/ramune/configuration.nix
··· 54 54 caddy.enable = true; 55 55 vaultwarden.enable = true; 56 56 tangled.enable = true; 57 + backup = { 58 + enable = true; 59 + r2AccountId = "a261b92e6b94f88e79c9c863e19accd4"; 60 + bucket = "ramune-backup"; 61 + }; 57 62 }; 58 63 }; 59 64
+174
modules/server/backup.nix
··· 1 + { lib, config, pkgs, helpers, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.modules.server; 6 + backup = cfg.backup; 7 + 8 + rclone = "${pkgs.rclone}/bin/rclone"; 9 + coreutils = pkgs.coreutils; 10 + 11 + rcloneFlags = "--config ${backup.configPath}"; 12 + 13 + pathType = types.submodule ({ name, ... }: { 14 + options = { 15 + name = mkOption { 16 + default = name; 17 + description = "Name used as the backup subdirectory in the remote. Defaults to the attribute name."; 18 + type = types.str; 19 + }; 20 + 21 + path = mkOption { 22 + description = "Path to back up."; 23 + type = types.str; 24 + }; 25 + 26 + sqlite = mkOption { 27 + default = null; 28 + description = "If set, use sqlite3 .backup on this database file instead of copying the path directly."; 29 + type = types.nullOr types.str; 30 + }; 31 + 32 + extras = mkOption { 33 + default = []; 34 + description = "Additional files/directories within path to copy alongside a sqlite backup."; 35 + type = types.listOf types.str; 36 + }; 37 + }; 38 + }); 39 + 40 + mkSqliteBackup = path: let 41 + sqlite = "${pkgs.sqlite}/bin/sqlite3"; 42 + in '' 43 + echo "Backing up ${path.name}..." 44 + ${coreutils}/bin/mkdir -p "$TMP/${path.name}" 45 + ${sqlite} "${path.path}/${path.sqlite}" ".backup $TMP/${path.name}/${path.sqlite}" 46 + ${concatMapStringsSep "\n" (item: '' 47 + if [ -e "${path.path}/${item}" ]; then 48 + ${coreutils}/bin/cp -a "${path.path}/${item}" "$TMP/${path.name}/" 49 + fi 50 + '') path.extras} 51 + ${rclone} ${rcloneFlags} copy "$TMP/${path.name}" "r2-encrypted:${path.name}/$DATE/" 52 + ''; 53 + 54 + mkSqlitePrune = path: '' 55 + CUTOFF=$(${coreutils}/bin/date -d '-${toString backup.retention} days' +%Y-%m-%d) 56 + ${rclone} ${rcloneFlags} lsd "r2-encrypted:${path.name}/" 2>/dev/null | ${coreutils}/bin/awk '{print $NF}' | while read -r dir; do 57 + if [[ "$dir" < "$CUTOFF" ]]; then 58 + ${rclone} ${rcloneFlags} purge "r2-encrypted:${path.name}/$dir/" || true 59 + fi 60 + done 61 + ''; 62 + 63 + mkSyncBackup = path: '' 64 + echo "Syncing ${path.name}..." 65 + ${rclone} ${rcloneFlags} sync "${path.path}" "r2-encrypted:${path.name}/" 66 + ''; 67 + 68 + in helpers.linuxAttrs { 69 + options.modules.server.backup = { 70 + enable = mkOption { 71 + default = false; 72 + description = "Whether to enable automated backups to R2."; 73 + type = types.bool; 74 + }; 75 + 76 + r2AccountId = mkOption { 77 + description = "Cloudflare account ID for R2 endpoint."; 78 + type = types.str; 79 + }; 80 + 81 + bucket = mkOption { 82 + default = "backups"; 83 + description = "R2 bucket name."; 84 + type = types.str; 85 + }; 86 + 87 + schedule = mkOption { 88 + default = "*-*-* 04:00:00"; 89 + description = "Systemd OnCalendar schedule for backups."; 90 + type = types.str; 91 + }; 92 + 93 + retention = mkOption { 94 + default = 30; 95 + description = "Number of days to retain backups."; 96 + type = types.int; 97 + }; 98 + 99 + configPath = mkOption { 100 + default = "/etc/rclone-backup.conf"; 101 + description = "Path to the rclone configuration file."; 102 + type = types.str; 103 + }; 104 + 105 + paths = mkOption { 106 + default = {}; 107 + description = "Paths to back up. Other modules can append to this attrset."; 108 + type = types.attrsOf pathType; 109 + }; 110 + }; 111 + 112 + config = let 113 + sqlitePaths = filter (p: p.sqlite != null) (attrValues backup.paths); 114 + syncPaths = filter (p: p.sqlite == null) (attrValues backup.paths); 115 + in mkIf (cfg.enable && backup.enable) { 116 + environment.etc."rclone-backup.conf".text = '' 117 + [r2] 118 + type = s3 119 + provider = Cloudflare 120 + endpoint = https://${backup.r2AccountId}.r2.cloudflarestorage.com 121 + acl = private 122 + 123 + [r2-encrypted] 124 + type = crypt 125 + remote = r2:${backup.bucket} 126 + ''; 127 + 128 + age.secrets."rclone-backup.env" = { 129 + symlink = true; 130 + path = "/run/secrets/rclone-backup.env"; 131 + file = ./encrypt/rclone-backup.env.age; 132 + }; 133 + 134 + systemd.services.backup = { 135 + description = "Backup services to R2"; 136 + wants = [ "network-online.target" ]; 137 + after = [ "network-online.target" ]; 138 + serviceConfig = { 139 + Type = "oneshot"; 140 + EnvironmentFile = "/run/secrets/rclone-backup.env"; 141 + ExecStart = pkgs.writeShellScript "backup" '' 142 + set -uo pipefail 143 + FAILED=0 144 + DATE=$(${coreutils}/bin/date +%Y-%m-%d) 145 + TMP=$(${coreutils}/bin/mktemp -d) 146 + trap '${coreutils}/bin/rm -rf "$TMP"' EXIT 147 + 148 + ${concatMapStringsSep "\n" (p: '' 149 + (set -e; ${mkSqliteBackup p}) || FAILED=1 150 + '') sqlitePaths} 151 + 152 + ${concatMapStringsSep "\n" (p: '' 153 + (set -e; ${mkSqlitePrune p}) || true 154 + '') sqlitePaths} 155 + 156 + ${concatMapStringsSep "\n" (p: '' 157 + (set -e; ${mkSyncBackup p}) || FAILED=1 158 + '') syncPaths} 159 + 160 + exit "$FAILED" 161 + ''; 162 + }; 163 + }; 164 + 165 + systemd.timers.backup = { 166 + wantedBy = [ "timers.target" ]; 167 + timerConfig = { 168 + OnCalendar = backup.schedule; 169 + Persistent = true; 170 + RandomizedDelaySec = "1h"; 171 + }; 172 + }; 173 + }; 174 + }
+1
modules/server/default.nix
··· 21 21 ./podman.nix 22 22 ./macos.nix 23 23 ./tangled.nix 24 + ./backup.nix 24 25 ]; 25 26 }
modules/server/encrypt/rclone-backup.env.age

This is a binary file and will not be displayed.

+4
modules/server/tangled.nix
··· 24 24 }; 25 25 26 26 config = mkIf (cfg.enable && cfg.tangled.enable) { 27 + modules.server.backup.paths.tangled = { 28 + path = "${config.services.tangled.knot.stateDir}/repos"; 29 + }; 30 + 27 31 services = { 28 32 tangled.knot = { 29 33 enable = true;
+6
modules/server/vaultwarden.nix
··· 27 27 }; 28 28 29 29 config = mkIf (cfg.enable && cfg.vaultwarden.enable) { 30 + modules.server.backup.paths.vaultwarden = { 31 + path = "/var/lib/vaultwarden"; 32 + sqlite = "db.sqlite3"; 33 + extras = [ "attachments" "rsa_key.pem" "rsa_key.pub.pem" ]; 34 + }; 35 + 30 36 age.secrets."vaultwarden" = { 31 37 symlink = true; 32 38 path = "/run/secrets/vaultwarden.env";
+1
secrets.nix
··· 11 11 "./modules/server/encrypt/tangled-knot-ssh.age".publicKeys = keys; 12 12 "./modules/server/encrypt/gitconfig.age".publicKeys = keys; 13 13 "./modules/server/encrypt/vaultwarden.age".publicKeys = keys; 14 + "./modules/server/encrypt/rclone-backup.env.age".publicKeys = keys; 14 15 15 16 "./modules/router/encrypt/pppoe-options.age".publicKeys = keys; 16 17