Backup server: incremental mirror, client subset, configurable compression

- local_backup rsynced every repository into an empty folder each night (full transfer); keep a persistent mirror (backup_server_mirror) and rsync --delete into it
- backup_server_clients: clients handled by the server (app names must be unique, e.g. two 'webserver' apps collided)
- backup_server_local_skip: apps that are pruned remotely but not mirrored
- backup_server_compression: borg repos are encrypted already, allow 'none'

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lhahnandClaude Opus 5.5 committed 2026-10-14 20:44:47 +02:00
1 parent 1d99257ead
commit c0155e3396
3 files changed
+27 -18

No files matched your search

+6
View File
@@ -23,6 +23,12 @@ backup_cron_owner: "{{ backup_owner }}"
backup_server_remote_prune_time: 6 backup_server_remote_prune_time: 6
backup_server_remote_backup_time: 8 backup_server_remote_backup_time: 8
backup_server_local_prune_time: 10 backup_server_local_prune_time: 10
#server: clients to prune/mirror (defaults to backup_clients), apps not mirrored locally, local compression
backup_server_clients: "{{ backup_clients }}"
backup_server_local_skip: []
backup_server_compression: lzma
#local mirror of the remote repositories (defaults to <backup_root>/storage/remote-backups)
#backup_server_mirror: /mnt/nas/backup/mirror
backup_script: backup_script:
+1 -1
View File
@@ -75,7 +75,7 @@
group: "{{ backup_group }}" group: "{{ backup_group }}"
mode: "0750" mode: "0750"
loop: loop:
- "{{ backup_remotes_folder }}" - "{{ backup_server_mirror | default(backup_remotes_folder) }}"
- "{{ backup_location }}/{{ backup_app }}" - "{{ backup_location }}/{{ backup_app }}"
- name: setup borg local repository - name: setup borg local repository
@@ -1,18 +1,22 @@
{% set server_clients = backup_server_clients | default(backup_clients) %}
#!/bin/bash #!/bin/bash
BORGUSER="{{ backup_owner }}"; BORGUSER="{{ backup_owner }}";
TARGETFOLDER="{{ backup_remotes_folder }}"; TARGETFOLDER="{{ backup_remotes_folder }}";
LOCALREPOLOCATION="{{ backup_location }}/{{ backup_app }}"; LOCALREPOLOCATION="{{ backup_location }}/{{ backup_app }}";
APPLIST="{{ backup_clients | json_query('*.app') | join(' ') }}"; APPLIST="{{ server_clients | json_query('*.app') | join(' ') }}";
# repos that are only pruned remotely, not mirrored locally
LOCALAPPLIST="{{ server_clients | json_query('*.app') | difference(backup_server_local_skip) | join(' ') }}";
MIRRORFOLDER="{{ backup_server_mirror | default(backup_remotes_folder) }}";
declare -A BORG_CLIENT_MAP declare -A BORG_CLIENT_MAP
{% for client in backup_clients %} {% for client in server_clients %}
BORG_CLIENT_MAP[{{ backup_clients[client].app }}]="{{ client }}"; BORG_CLIENT_MAP[{{ server_clients[client].app }}]="{{ client }}";
{% endfor %} {% endfor %}
declare -A BORG_USER_MAP declare -A BORG_USER_MAP
{% for client in backup_clients %} {% for client in server_clients %}
BORG_USER_MAP[{{ backup_clients[client].app }}]="{{ backup_clients[client].owner }}"; BORG_USER_MAP[{{ server_clients[client].app }}]="{{ server_clients[client].owner }}";
{% endfor %} {% endfor %}
@@ -36,8 +40,8 @@ COMMAND:
APPS: A single app or comma-separated (sub-)set of below listed apps to be considered. APPS: A single app or comma-separated (sub-)set of below listed apps to be considered.
{% for client in backup_clients %} {% for client in server_clients %}
- {{ backup_clients[client].app }} - {{ server_clients[client].app }}
{% endfor %} {% endfor %}
"; ";
} }
@@ -177,25 +181,24 @@ local_prune () {
local_backup () { local_backup () {
ARCHIVENAME="{{ backup_app }}-$(date '+%s')"; ARCHIVENAME="{{ backup_app }}-$(date '+%s')";
mkdir -p $TARGETFOLDER/$ARCHIVENAME; mkdir -p $MIRRORFOLDER;
chown $BORGUSER: $TARGETFOLDER/$ARCHIVENAME; chown $BORGUSER: $MIRRORFOLDER;
for APP in $APPLIST; # keep a persistent mirror so rsync only transfers what changed
for APP in $LOCALAPPLIST;
do do
sudo -H -u $BORGUSER bash -c ' sudo -H -u $BORGUSER bash -c '
TARGETFOLDER='$TARGETFOLDER'; MIRRORFOLDER='$MIRRORFOLDER';
ARCHIVENAME='$ARCHIVENAME';
REPOLOCATION='${BORG_CLIENT_MAP[$APP]}':'$APP'; REPOLOCATION='${BORG_CLIENT_MAP[$APP]}':'$APP';
rsync -azr $REPOLOCATION $TARGETFOLDER/$ARCHIVENAME/;'; rsync -ar --delete $REPOLOCATION $MIRRORFOLDER/;';
done done
# borg repositories are encrypted already, compression rarely helps
sudo -H -u $BORGUSER bash -c ' sudo -H -u $BORGUSER bash -c '
TARGETFOLDER='$TARGETFOLDER'; MIRRORFOLDER='$MIRRORFOLDER';
REPOLOCATION='$LOCALREPOLOCATION'; REPOLOCATION='$LOCALREPOLOCATION';
ARCHIVENAME='$ARCHIVENAME'; ARCHIVENAME='$ARCHIVENAME';
export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key); export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key);
borg create -C lzma $REPOLOCATION::$ARCHIVENAME $TARGETFOLDER/$ARCHIVENAME;'; borg create -C {{ backup_server_compression }} $REPOLOCATION::$ARCHIVENAME $MIRRORFOLDER;';
rm -rf $TARGETFOLDER/$ARCHIVENAME;
} }
local_delete () { local_delete () {