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:
♥ Minnie ♥ 2025-10-06 20:59:26 +08:00
parent b0bfb37d3c
commit c05598d9e0
Signed by: jasmine
GPG key ID: 8563E358D4E8040E
5 changed files with 231 additions and 36 deletions

View file

@ -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
];
}

View 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
];
}

View 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;
};
};
}

View file

@ -9,6 +9,7 @@
./mpd
./murmur
./opengist
./snapper
./traefik
];
}

View 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;
};
};
};
}