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 = {
|
||||
rekeyFile = ./passphrase.age;
|
||||
};
|
||||
|
||||
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
|
||||
{...}: {
|
||||
imports = [
|
||||
./offsite.nix
|
||||
./onsite.nix
|
||||
];
|
||||
}
|
||||
|
|
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
|
||||
./murmur
|
||||
./opengist
|
||||
./snapper
|
||||
./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