toastie89 3 months ago
commit
504924e297

+ 37 - 0
.env.template

@@ -0,0 +1,37 @@
+# Backup schedule
+#    minute ( 1-59
+#    | hour 1-23
+#    | | day of month (1-31)
+#    | | | month (1-12)
+#    | | | | weekday 0=Sun, ...,  6=Sat, 7=Sun
+#    | | | | |
+CRON=0 1 * * * /opt/scripts/do-backup.sh
+
+# Timezone, import for cron to start at the right time
+TZ=Europe/Berlin
+
+# Prometheus URL where to sent metric to,
+# no metrics will be sent if empty
+PROM_URL=http://192.168.10.10:9090/push/metrics/job/borg/host/examplehost
+
+# Hostname used in the archive name 
+BORG_HOSTNAME=examplehost
+
+# Passphrase to encrypt the keyfile
+BORG_PASSPHRASE=ReplaceWithYourSecretPassphrase
+
+# Path within the container to store backups
+BORG_REPO=/mnt/target/borg
+
+# Path within the container to backup
+BORG_SOURCE_PATH=/mnt/source/data
+
+# Volume on the host to mount as backup source 
+# This volume is mounted to /mnt/source within the container
+VOLUME_SOURCE=/srv
+
+# Volume on the host to mount as backup target 
+# This volume is mounted to /mnt/target within the container
+VOLUME_TARGET=/srv/backup
+
+BORG_RETENTION="--keep-hourly 2 --keep-daily 14 --keep-weekly 6 --keep-monthly 12 --keep-yearly 10"

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+.env
+data/

+ 49 - 0
README.md

@@ -0,0 +1,49 @@
+# Borg Backup
+
+Container image to create cron scheduled backups using [borg backup](https://www.borgbackup.org/) based on Alpine Linux.
+
+## Why to use Borg Backup
+  - Space efficient storage due to deduplication and compression
+  - Quick backup runs including pruning of old backups on disk
+  - Encryption allows storing in insecure offsite locations
+  - Fuse-mount of backups ease restore
+- For remote backups, you may take a look in restic
+
+## Security considerations:
+  - This container will run with root priveliges in order to access all data for backup
+  - The backup source volume is mounted read-only to avoid alering data by mistake
+  
+## Prepare for backup restore
+Following files MUST be stored along with the backup to enable encryption of backup data
+  - `.env`-file which contains the Passphrase
+  - Keyfiles, stored in ./data/.config/borg/keys/
+
+## Monitoring
+  - Status and statistics are sent to Prometheus using a simple bash script and curl
+
+## Build
+  - Alpine and borg version are hard-coded in docker compose so we don't mess up backups due to version upgrades
+  - Run `docker compose build` to build the container image from `./build/Dockerfile`
+
+## Installation & Setup
+  - Configuration: `cp .env.template .env ` and adapt `.env` (parameters are explained in the template file)
+  - Init the backup archive: `docker exec --rm -it borg bash -c "borg init --encryption repokey-blake2"`
+  - Start the container: `docker-compose up -d`
+
+## Progam flow
+  - `/scripts/entry.sh` is called during container startup
+    and installs the cronjob defined in `.env` variable $CRON
+  - crond starts `/scripts/do-backup.sh` which
+    - notifies prometheus about the status and stats
+	- executes borg backup
+	- prunes and compacts old backups in 
+
+## Backup restore
+  1. Stop the backup container: `docker compose down`
+  2. Run an interactive shell: `docker compose -f docker-compose.yml -f docker-compose.restore.yml run borg bash`
+  3. Fuse-mount the backup: `borg mount $BORG_REPO <mount_point>`
+  4. Restore your files
+  5. Finally unmount and exit: `borg umount <mount_point> && exit`.
+  
+# Failure handling
+  - In case Borg fails to create/acquire a lock: `borg break-lock /mnt/repository`

+ 8 - 0
build/Dockerfile

@@ -0,0 +1,8 @@
+ARG ALPINE_VERSION=latest
+ARG BORG_VERSION=
+FROM alpine:${ALPINE_VERSION}
+RUN apk add --upgrade --no-cache borgbackup${BORG_VER} tzdata curl py3-llfuse bash
+ENV PATH="$PATH:/opt/scripts"
+CMD ["entry.sh"]
+
+

+ 16 - 0
docker-compose.restore.yml

@@ -0,0 +1,16 @@
+version: '3'
+services:
+  borg:
+    container_name: borg-restore
+
+    cap_add:
+      - SYS_ADMIN
+
+    security_opt:
+      - apparmor:unconfined
+      - label:disable
+
+    devices:
+      - /dev/fuse:/dev/fuse
+
+    command: /bin/sh

+ 27 - 0
docker-compose.yml

@@ -0,0 +1,27 @@
+version: '3'
+services:
+  borg:
+    image: toastie89/borg 
+    build:
+      context: ./build
+      args:
+        ALPINE_VER: 3.19.1
+        BORG_VER: =1.2.7-r0
+
+    container_name: borg 
+    hostname: borg 
+    volumes:
+      - ${VOLUME_SOURCE}:/mnt/source:ro         # backup source
+      - ${VOLUME_TARGET}:/mnt/target:rw         # backup target
+      - ./data/.config/borg:/root/.config/borg  # config and keyfiles
+      - ./data/.cache/borg:/root/.cache/borg    # checksums used for deduplication
+      - ./scripts:/opt/scripts                  # scripts run by cron and helper 
+    environment:
+      - TZ=${TZ}
+      - PROM_URL=${PROM_URL}
+      - BORG_HOSTNAME=${BORG_HOSTNAME}
+      - BORG_PASSPHRASE=${BORG_PASSPHRASE}
+      - BORG_REPO=${BORG_REPO}
+      - BORG_SOURCE_PATH=${BORG_SOURCE_PATH}
+      - BORG_RETENTION=${BORG_RETENTION}
+      - CRON=${CRON}

+ 31 - 0
scripts/do-backup.sh

@@ -0,0 +1,31 @@
+BORG_GENERAL_OPTIONS="--show-rc --show-version --info"
+
+# Report start of backup to prom
+notify-prom-status.sh start
+
+# Run backup
+borg $BORG_GENERAL_OPTIONS \
+  create \
+    --stats \
+    --one-file-system \
+    --exclude-if-present .backupignore \
+    --keep-exclude-tags \
+    $BORG_REPO::$BORG_HOSTNAME-{now} \
+    $BORG_SOURCE_PATH 
+
+# Report backup exit code to prom
+notify-prom-status.sh $?
+
+# Prune old backups
+borg $BORG_GENERAL_OPTIONS \
+  prune \
+    $BORG_RETENTION \
+    $BORG_REPO
+  
+# Free space by compacting segments
+borg $BORG_GENERAL_OPTIONS \
+  compact \
+    $BORG_REPO
+
+# Report backup statistics to prom
+notify-prom-stats.sh

+ 8 - 0
scripts/entry.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+
+trap "printf 222222222222; killall  -2 borg; killall  -2 crond; exit" 2 
+trap "printf 151515151515; killall -15 borg; killall -15 crond; exit" 15 
+
+echo "$CRON" | crontab -
+crond -f
+

+ 5 - 0
scripts/get-last-delta.sh

@@ -0,0 +1,5 @@
+#!/bin/bash
+    
+borg diff --sort --content-only \
+$BORG_REPO::`borg list --last 1 --short $BORG_REPO` \
+`borg list --last 2 --short $BORG_REPO | head -n 1`

+ 28 - 0
scripts/notify-prom-stats.sh

@@ -0,0 +1,28 @@
+#!/bin/bash
+
+[ -z "$PROM_URL" ] && exit
+
+json=`borg info --last 1 --json`
+
+
+cat <<EOF | curl --data-binary @- $PROM_URL
+# TYPE borg_run_duration_seconds gauge
+# HELP borg_run_duration_seconds duration between start and end
+`echo -n 'borg_run_duration_seconds ' && \
+echo $json | grep -o '"duration": [0-9]*' | grep -o '[0-9]*'`
+
+# TYPE borg_run_size_deduplicated_bytes gauge
+# HELP borg_run_size_deduplicated_bytes deduplicated size of the last run
+`echo -n 'borg_run_size_deduplicated_bytes ' && \
+echo $json | grep -o '"compressed_size": [0-9]*' | grep -o '[0-9]*'`
+
+# TYPE borg_run_size_original_bytes gauge
+# HELP borg_run_size_original_bytes original size of the last run
+`echo -n 'borg_run_size_original_bytes ' && \
+echo $json | grep -o '"original_size": [0-9]*' | grep -o '[0-9]*'`
+
+# TYPE borg_cache_chunks_size_total_compressed_bytes gauge
+# HELP borg_cache_chunks_size_total_compressed_bytes total size of all chunks
+`echo -n 'borg_cache_chunks_size_total_compressed_bytes ' && \
+echo $json | grep -o '"unique_csize": [0-9]*' | grep -o '[0-9]*'`
+EOF

+ 18 - 0
scripts/notify-prom-status.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+
+[ -z "$PROM_URL" ] && exit
+
+case $1 in
+  start)    state='1';  status='started' ;;
+  stop|0)   state='0';  status='stopped' ;;
+  info|1)   state='0';  status='stopped with info' ;;
+  fail|2|*) state='-1'; status='failed' ;;
+esac
+
+cat <<EOF | curl --data-binary @- $PROM_URL
+# TYPE borg_hook_state gauge
+# HELP borg_hook_state 1=started 0=stopped -1=failed
+borg_hook_state $state
+# TYPE borg_hook_state_timestamp gauge
+borg_hook_state_timestamp{label="$status"} `date +%s`
+EOF