feat(viridian): implement comprehensive 3-2-1 backup strategy
Add automated snapshot and backup system with three independent tiers: Snapper (hourly local snapshots): - Configure snapper for all srv-* subvolumes - Tiered retention: 24 hourly, 7 daily, 4 weekly, 12 monthly - Snapshots stored at /.snapshots on viridian drive - Provides fast operational rollback for user errors Borgbackup onsite (hourly local backups): - Independent staging snapshots at /.staging-onsite - Repository on data drive at /srv/borg-repo - Unencrypted (physical security assumed) - Matches snapper retention policy - Fast local disaster recovery Borgbackup offsite (daily remote backups): - Independent staging snapshots at /.staging-offsite - Encrypted backups to borgbase repository - Retention: 7 daily, 4 weekly, 12 monthly - Remote disaster recovery with prune policy Architecture decisions: - Separate staging directories prevent job conflicts - Staging snapshots decouple borg jobs from snapper - Consistent zstd,9 compression across both borg jobs - Special case handling for containers subvolume path
This commit is contained in:
parent
b0bfb37d3c
commit
c05598d9e0
5 changed files with 231 additions and 36 deletions
|
|
@ -1,38 +1,6 @@
|
||||||
{config, ...}: {
|
{...}: {
|
||||||
age.secrets.borgbackup = {
|
imports = [
|
||||||
rekeyFile = ./passphrase.age;
|
./offsite.nix
|
||||||
};
|
./onsite.nix
|
||||||
|
|
||||||
services.borgbackup.jobs."borgbase" = {
|
|
||||||
paths = [
|
|
||||||
# Websites
|
|
||||||
"/srv/lighttpd/sajenim.dev"
|
|
||||||
# Services
|
|
||||||
"/var/lib/crowdsec"
|
|
||||||
"/var/lib/forgejo"
|
|
||||||
"/var/lib/opengist"
|
|
||||||
"/var/lib/traefik"
|
|
||||||
"/srv/minecraft"
|
|
||||||
# Multimedia
|
|
||||||
"/srv/multimedia/containers/jellyfin"
|
|
||||||
"/srv/multimedia/containers/lidarr"
|
|
||||||
"/srv/multimedia/containers/prowlarr"
|
|
||||||
"/srv/multimedia/containers/qbittorrent"
|
|
||||||
"/srv/multimedia/containers/radarr"
|
|
||||||
"/srv/multimedia/containers/sonarr"
|
|
||||||
];
|
|
||||||
|
|
||||||
repo = "r7ag7x1w@r7ag7x1w.repo.borgbase.com:repo";
|
|
||||||
encryption = {
|
|
||||||
mode = "repokey-blake2";
|
|
||||||
passCommand = "cat ${config.age.secrets.borgbackup.path}";
|
|
||||||
};
|
|
||||||
environment.BORG_RSH = "ssh -i /etc/ssh/ssh_host_ed25519_key";
|
|
||||||
compression = "auto,lzma";
|
|
||||||
startAt = "daily";
|
|
||||||
};
|
|
||||||
|
|
||||||
programs.ssh.knownHostsFiles = [
|
|
||||||
./borgbase_hosts
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
68
nixos/viridian/services/borgbackup/offsite.nix
Normal file
68
nixos/viridian/services/borgbackup/offsite.nix
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
# Encrypted passphrase for offsite borgbackup repository
|
||||||
|
age.secrets.borgbackup = {
|
||||||
|
rekeyFile = ./passphrase.age;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.borgbackup.jobs."offsite" = {
|
||||||
|
# Create staging snapshots before backup (independent from onsite)
|
||||||
|
preHook = ''
|
||||||
|
# Create read-only staging snapshots for each service
|
||||||
|
for subvol in containers forgejo lighttpd minecraft opengist; do
|
||||||
|
# Map config names to actual subvolume paths
|
||||||
|
case "$subvol" in
|
||||||
|
containers) src="/srv/multimedia/containers" ;;
|
||||||
|
*) src="/srv/$subvol" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
${pkgs.btrfs-progs}/bin/btrfs subvolume snapshot -r \
|
||||||
|
"$src" "/.staging-offsite/$subvol"
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Backup all staging snapshots
|
||||||
|
paths = [
|
||||||
|
"/.staging-offsite/containers"
|
||||||
|
"/.staging-offsite/forgejo"
|
||||||
|
"/.staging-offsite/lighttpd"
|
||||||
|
"/.staging-offsite/minecraft"
|
||||||
|
"/.staging-offsite/opengist"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Remove staging snapshots after backup completes
|
||||||
|
postHook = ''
|
||||||
|
for subvol in containers forgejo lighttpd minecraft opengist; do
|
||||||
|
${pkgs.btrfs-progs}/bin/btrfs subvolume delete \
|
||||||
|
"/.staging-offsite/$subvol"
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Remote repository configuration
|
||||||
|
repo = "r7ag7x1w@r7ag7x1w.repo.borgbase.com:repo";
|
||||||
|
|
||||||
|
encryption = {
|
||||||
|
mode = "repokey-blake2";
|
||||||
|
passCommand = "cat ${config.age.secrets.borgbackup.path}";
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.BORG_RSH = "ssh -i /etc/ssh/ssh_host_ed25519_key";
|
||||||
|
compression = "zstd,9";
|
||||||
|
startAt = "daily";
|
||||||
|
|
||||||
|
# Retention policy for daily remote backups
|
||||||
|
prune.keep = {
|
||||||
|
daily = 7; # Keep 7 daily backups (1 week)
|
||||||
|
weekly = 4; # Keep 4 weekly backups (1 month)
|
||||||
|
monthly = 12; # Keep 12 monthly backups (1 year)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# SSH host keys for borgbase.com
|
||||||
|
programs.ssh.knownHostsFiles = [
|
||||||
|
./borgbase_hosts
|
||||||
|
];
|
||||||
|
}
|
||||||
68
nixos/viridian/services/borgbackup/onsite.nix
Normal file
68
nixos/viridian/services/borgbackup/onsite.nix
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
hostname = config.networking.hostName;
|
||||||
|
in {
|
||||||
|
# Mount the data drive borg-repo subvolume for local backups
|
||||||
|
fileSystems."/srv/borg-repo" = {
|
||||||
|
device = "/dev/disk/by-label/data";
|
||||||
|
fsType = "btrfs";
|
||||||
|
options = [
|
||||||
|
"subvol=borg-repo"
|
||||||
|
"compress=zstd"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
services.borgbackup.jobs."onsite" = {
|
||||||
|
# Create staging snapshots before backup (independent from offsite)
|
||||||
|
preHook = ''
|
||||||
|
# Create read-only staging snapshots for each service
|
||||||
|
for subvol in containers forgejo lighttpd minecraft opengist; do
|
||||||
|
# Map config names to actual subvolume paths
|
||||||
|
case "$subvol" in
|
||||||
|
containers) src="/srv/multimedia/containers" ;;
|
||||||
|
*) src="/srv/$subvol" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
${pkgs.btrfs-progs}/bin/btrfs subvolume snapshot -r \
|
||||||
|
"$src" "/.staging-onsite/$subvol"
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Backup all staging snapshots
|
||||||
|
paths = [
|
||||||
|
"/.staging-onsite/containers"
|
||||||
|
"/.staging-onsite/forgejo"
|
||||||
|
"/.staging-onsite/lighttpd"
|
||||||
|
"/.staging-onsite/minecraft"
|
||||||
|
"/.staging-onsite/opengist"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Remove staging snapshots after backup completes
|
||||||
|
postHook = ''
|
||||||
|
for subvol in containers forgejo lighttpd minecraft opengist; do
|
||||||
|
${pkgs.btrfs-progs}/bin/btrfs subvolume delete \
|
||||||
|
"/.staging-onsite/$subvol"
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Local repository configuration
|
||||||
|
repo = "/srv/borg-repo/${hostname}";
|
||||||
|
|
||||||
|
# No encryption for local backups (physical security assumed)
|
||||||
|
encryption.mode = "none";
|
||||||
|
|
||||||
|
compression = "zstd,9";
|
||||||
|
startAt = "hourly";
|
||||||
|
|
||||||
|
# Match snapper retention policy
|
||||||
|
prune.keep = {
|
||||||
|
hourly = 24;
|
||||||
|
daily = 7;
|
||||||
|
weekly = 4;
|
||||||
|
monthly = 12;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
./mpd
|
./mpd
|
||||||
./murmur
|
./murmur
|
||||||
./opengist
|
./opengist
|
||||||
|
./snapper
|
||||||
./traefik
|
./traefik
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
90
nixos/viridian/services/snapper/default.nix
Normal file
90
nixos/viridian/services/snapper/default.nix
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
{config, ...}: let
|
||||||
|
hostname = config.networking.hostName;
|
||||||
|
in {
|
||||||
|
# Mount snapshots subvolume at /.snapshots for snapshot storage
|
||||||
|
fileSystems."/.snapshots" = {
|
||||||
|
device = "/dev/disk/by-label/${hostname}";
|
||||||
|
fsType = "btrfs";
|
||||||
|
options = [
|
||||||
|
"subvol=snapshots"
|
||||||
|
"compress=zstd"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Configure snapper for automated snapshots
|
||||||
|
services.snapper = {
|
||||||
|
# Enable snapper globally
|
||||||
|
configs = {
|
||||||
|
# Minecraft server data
|
||||||
|
minecraft = {
|
||||||
|
SUBVOLUME = "/srv/minecraft";
|
||||||
|
ALLOW_USERS = ["sajenim"];
|
||||||
|
TIMELINE_CREATE = true;
|
||||||
|
TIMELINE_CLEANUP = true;
|
||||||
|
|
||||||
|
# Tiered retention: 24h + 7d + 4w + 12m = ~1 year of snapshots
|
||||||
|
TIMELINE_LIMIT_HOURLY = 24;
|
||||||
|
TIMELINE_LIMIT_DAILY = 7;
|
||||||
|
TIMELINE_LIMIT_WEEKLY = 4;
|
||||||
|
TIMELINE_LIMIT_MONTHLY = 12;
|
||||||
|
TIMELINE_LIMIT_YEARLY = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Container data (jellyfin, arr services, etc)
|
||||||
|
containers = {
|
||||||
|
SUBVOLUME = "/srv/multimedia/containers";
|
||||||
|
ALLOW_USERS = ["sajenim"];
|
||||||
|
TIMELINE_CREATE = true;
|
||||||
|
TIMELINE_CLEANUP = true;
|
||||||
|
|
||||||
|
TIMELINE_LIMIT_HOURLY = 24;
|
||||||
|
TIMELINE_LIMIT_DAILY = 7;
|
||||||
|
TIMELINE_LIMIT_WEEKLY = 4;
|
||||||
|
TIMELINE_LIMIT_MONTHLY = 12;
|
||||||
|
TIMELINE_LIMIT_YEARLY = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Forgejo git forge data
|
||||||
|
forgejo = {
|
||||||
|
SUBVOLUME = "/srv/forgejo";
|
||||||
|
ALLOW_USERS = ["sajenim"];
|
||||||
|
TIMELINE_CREATE = true;
|
||||||
|
TIMELINE_CLEANUP = true;
|
||||||
|
|
||||||
|
TIMELINE_LIMIT_HOURLY = 24;
|
||||||
|
TIMELINE_LIMIT_DAILY = 7;
|
||||||
|
TIMELINE_LIMIT_WEEKLY = 4;
|
||||||
|
TIMELINE_LIMIT_MONTHLY = 12;
|
||||||
|
TIMELINE_LIMIT_YEARLY = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
# OpenGist pastebin data
|
||||||
|
opengist = {
|
||||||
|
SUBVOLUME = "/srv/opengist";
|
||||||
|
ALLOW_USERS = ["sajenim"];
|
||||||
|
TIMELINE_CREATE = true;
|
||||||
|
TIMELINE_CLEANUP = true;
|
||||||
|
|
||||||
|
TIMELINE_LIMIT_HOURLY = 24;
|
||||||
|
TIMELINE_LIMIT_DAILY = 7;
|
||||||
|
TIMELINE_LIMIT_WEEKLY = 4;
|
||||||
|
TIMELINE_LIMIT_MONTHLY = 12;
|
||||||
|
TIMELINE_LIMIT_YEARLY = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Lighttpd website data
|
||||||
|
lighttpd = {
|
||||||
|
SUBVOLUME = "/srv/lighttpd";
|
||||||
|
ALLOW_USERS = ["sajenim"];
|
||||||
|
TIMELINE_CREATE = true;
|
||||||
|
TIMELINE_CLEANUP = true;
|
||||||
|
|
||||||
|
TIMELINE_LIMIT_HOURLY = 24;
|
||||||
|
TIMELINE_LIMIT_DAILY = 7;
|
||||||
|
TIMELINE_LIMIT_WEEKLY = 4;
|
||||||
|
TIMELINE_LIMIT_MONTHLY = 12;
|
||||||
|
TIMELINE_LIMIT_YEARLY = 0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue