Docker (Auto)Backup mit Restic & Time Machine

RESTIC Backup Script – Funktionsübersicht

1. Ziel

Mein Backup Script ist ein stabiler Split-Backup-Prozess für Docker-Umgebungen, der:

  • Stateful Container (z. B. Nextcloud, Nginx Proxy Manager, AdGuard) sichert
  • Stateless Container ignoriert oder nur optional einbezieht
  • Datenbanken (Nextcloud, Paperless) zuverlässig dumpen
  • Restic für snapshots nutzt
  • Time Machine automatisiert triggert
  • Vollständiges Logging für Nachvollziehbarkeit bietet

2. Kernfunktionen

a) Logging

  • Alle Aktionen werden mit Zeitstempel in einer Logdatei (/Volumes/Daten/Backup/logs/backup_YYYY-MM-DD.log) protokolliert
  • Konsistente Statusmeldungen (log "Message")
  • Standardausgabe + Fehler werden direkt in Logfile umgeleitet

b) Docker-Prüfung und Container-Detection

  • Prüft, ob Docker läuft (docker info)
  • Erkennt Stateful- und Stateless-Container anhand definierter Image-Listen
  • Automatisches Stoppen/Starten der Stateful Container für konsistente Backups

c) Datenbank-Backups

  • Nextcloud: dynamische Erkennung des DB-Containers, mysqldump/mariadb-dump mit hartkodiertem Nutzer (nextcloud)
  • Paperless: hartkodierte DB-Zugangsdaten (paperless/paperless)
  • Dumps landen temporär im Backup-Verzeichnis ($TMP_DIR)

d) Volume-Backup

  • Exportiert Nextcloud-Volume als TAR-Archiv via temporärem Docker-Container (alpine)
  • Paperless-Medien und andere Mount-Punkte werden zusätzlich gesichert

e) Restic Backup

  • Führt drei getrennte Backup-Tags durch:
    1. core → temporäre DB Dumps + Stateful Container-Mounts
    2. media → Nextcloud TAR + Paperless Media
  • Nutzt Restic-Cache für User/Root-Kompatibilität

f) Container Management

  • Stoppt Stateful Container vor kritischen Backups
  • Startet Container wieder nach Backup

g) Cleanup

  • Löscht temporäre Backup-Dateien ($TMP_DIR)
  • Bereinigt Restic-Cache und setzt Berechtigungen
  • Führt Restic forget/prune für Retention durch

h) Time Machine Integration

  • Automatisches Starten des Time Machine Backups (tmutil startbackup)

3. Stärken des Scripts

  1. Robustheit
    • Alles hartkodiert und minimal abstrahiert → kein Risiko von Heuristik-Fehlern
    • Stop/Start der Container garantiert konsistente Dumps
  2. Transparenz & Nachvollziehbarkeit
    • Vollständiges Logging mit Zeitstempel
    • Alle Schritte klar erkennbar und nachvollziehbar
  3. Split-Backup Architektur
    • Trennung von core (DBs + Stateful) und media (Nextcloud, Paperless)
    • Verbessert Backup-Performance und Wiederherstellbarkeit
  4. Docker- & Restic-kompatibel
    • Nutzt vorhandene Docker-Volumes und Container
    • Restic-Snapshots mit Tagging, Retention und Prune
  5. Einfaches temporäres Management
    • Temporäre Dump- und Export-Verzeichnisse sauber isoliert
    • Keine Abhängigkeit von externen Tools außer Docker, Restic, tmutil
  6. Stabile Time Machine Integration
    • Optionaler, automatisierter Start von TM-Backups

4. Fazit

Es ist nicht das eleganteste Script, aber es ist produktionsreif, stabil und zuverlässig.
Perfekt für produktive Docker-Umgebungen, bei denen Backup-Konsistenz wichtiger ist als Refactoring oder “Optimierung um jeden Preis”.

Install:

brew install restic

Repo initialisieren:

restic -r /Volumes/Daten/Backup/restic-repo init

mein Backup script welches NUR als root ausgeführt wird und im Verzeichnis /Library/Scripts/ als backup_restic_v19.sh liegt.

#!/usr/bin/env bash
set -euo pipefail

#############################################
# CONFIG
#############################################

RESTIC_REPO="/Volumes/Daten/Backup/restic-repo"
export RESTIC_PASSWORD="**********"

# eigener Cache (wichtig für root vs user)
export RESTIC_CACHE_DIR="/Volumes/Daten/Backup/restic-cache"
mkdir -p "$RESTIC_CACHE_DIR"

NEXTCLOUD_VOLUME="nextcloud_33_nextcloud_data"
PAPERLESS_CONTAINER="paper_2_20_13-docs-1"

PAPERLESS_DB_USER="paperless"
PAPERLESS_DB_PASSWORD="paperless"
PAPERLESS_DB_NAME="paperless"
MYSQL_PASSWORD="***************"

TMP_DIR="/Volumes/Daten/Backup/tmp/db-backup"

# wichtig für Docker write access
mkdir -p "$TMP_DIR"
chmod 777 "$TMP_DIR"

LOG_PREFIX="[RESTIC-V19]"

#############################################
# LOGGING
#############################################

LOGFILE="/Volumes/Daten/Backup/logs/backup_$(date +%F).log"
mkdir -p "$(dirname "$LOGFILE")"

exec > >(tee -a "$LOGFILE") 2>&1

log() {
    echo "$(date '+[%Y-%m-%d %H:%M:%S]') ${LOG_PREFIX} $1"
}

#############################################
# CHECK DOCKER
#############################################

log "Checking Docker availability"
docker info >/dev/null 2>&1 || { echo "Docker not running"; exit 1; }

#############################################
# DETECT CONTAINERS
#############################################

STATEFUL_CONTAINERS=""
STATELESS_CONTAINERS=""
EXTRA_PATHS=""

STATEFUL_IMAGES=(
  "jc21/nginx-proxy-manager"
  "adguard/adguardhome"
)

STATELESS_IMAGES=(
  "oznu/cloudflare-ddns"
  "containous/whoami"
)

log "Detecting containers"

while read -r name image; do
    for s in "${STATEFUL_IMAGES[@]}"; do
        [[ "$image" == *"$s"* ]] && STATEFUL_CONTAINERS+="$name"$'\n'
    done
    for s in "${STATELESS_IMAGES[@]}"; do
        [[ "$image" == *"$s"* ]] && STATELESS_CONTAINERS+="$name"$'\n'
    done
done < <(docker ps --filter "label=backup.include=true" --format '{{.Names}} {{.Image}}')

STATEFUL_CONTAINERS=$(printf "%s" "$STATEFUL_CONTAINERS" | sort -u | sed '/^$/d')

#############################################
# PAPERLESS PATHS
#############################################

log "Detecting Paperless mounts"

PAPERLESS_PATHS=$(docker inspect "$PAPERLESS_CONTAINER" \
    | grep '"Source"' \
    | awk -F'"' '{print $4}' \
    | sort -u || true)

#############################################
# NEXTCLOUD DB DETECTION
#############################################

log "Detecting Nextcloud DB container"

NC_DB_CONTAINER=$(docker ps --format '{{.Names}}' | while read -r c; do
    if docker inspect "$c" 2>/dev/null | grep -qi "mariadb\|mysql"; then
        if docker exec "$c" ps aux 2>/dev/null | grep -q "mysql\|mariadbd"; then
            echo "$c"; break
        fi
    fi
done || true)

if [[ -z "$NC_DB_CONTAINER" ]]; then
    log "ERROR: Nextcloud DB container not found"
    exit 1
fi

#############################################
# NEXTCLOUD DB DUMP
#############################################

log "Dumping Nextcloud DB"

docker exec "$NC_DB_CONTAINER" sh -c "
    if command -v mysqldump >/dev/null 2>&1; then
        mysqldump -u nextcloud -p'${MYSQL_PASSWORD}' nextcloud
    else
        mariadb-dump -u nextcloud -p'${MYSQL_PASSWORD}' nextcloud
    fi
" > "$TMP_DIR/nextcloud.sql"

#############################################
# PAPERLESS DB DETECTION
#############################################

log "Detecting Paperless DB container"

PAPERLESS_DB=$(docker ps --format '{{.Names}}' | grep -iE 'paper.*db|db.*paper|mariadb.*paper' | head -n1 || true)

if [[ -z "$PAPERLESS_DB" ]]; then
    log "ERROR: Paperless DB container not found"
    exit 1
fi

#############################################
# PAPERLESS DB DUMP (FIXED!)
#############################################

log "Dumping Paperless DB"

docker exec "$PAPERLESS_DB" sh -c "
    if command -v mariadb-dump >/dev/null 2>&1; then
        DUMP=mariadb-dump
    else
        DUMP=mysqldump
    fi

    \$DUMP \
        --host=127.0.0.1 \
        --user='$PAPERLESS_DB_USER' \
        --password='$PAPERLESS_DB_PASSWORD' \
        --single-transaction \
        --quick \
        --lock-tables=false \
        $PAPERLESS_DB_NAME
" > "$TMP_DIR/paperless.sql"

#############################################
# NEXTCLOUD ARCHIVE
#############################################

log "Exporting Nextcloud volume"

docker run --rm \
    -v "${NEXTCLOUD_VOLUME}:/from" \
    -v "$TMP_DIR:/to" \
    alpine sh -c "tar cf /to/nextcloud_data.tar -C /from ."

#############################################
# STOP STATEFUL CONTAINERS
#############################################

log "Stopping stateful containers"

echo "$STATEFUL_CONTAINERS" | while read -r c; do
    [[ -n "$c" ]] && docker stop "$c" >/dev/null 2>&1 || true
done

#############################################
# EXTRA PATHS
#############################################

log "Collecting mounts"

EXTRA_PATHS=$(echo "$STATEFUL_CONTAINERS" | while read -r c; do
    docker inspect "$c" 2>/dev/null | grep '"Source"' | awk -F'"' '{print $4}'
done | sort -u)

#############################################
# CORE BACKUP
#############################################

log "Running CORE backup"

restic -r "$RESTIC_REPO" backup \
    "$TMP_DIR" \
    $PAPERLESS_PATHS \
    $EXTRA_PATHS \
    --tag core

#############################################
# MEDIA BACKUP
#############################################

log "Running MEDIA backup"

restic -r "$RESTIC_REPO" backup \
    "$TMP_DIR/nextcloud_data.tar" \
    /Volumes/Daten/docker/data/paperless-ngx/media \
    --tag media

#############################################
# START CONTAINERS
#############################################

log "Starting containers"

echo "$STATEFUL_CONTAINERS" | while read -r c; do
    [[ -n "$c" ]] && docker start "$c" >/dev/null 2>&1 || true
done

#############################################
# CLEAN TMP
#############################################

rm -rf "$TMP_DIR"

#############################################
# RESTIC CLEANUP
#############################################

log "Running forget/prune"

restic -r "$RESTIC_REPO" forget \
    --keep-daily 7 \
    --keep-weekly 4 \
    --keep-monthly 6 \
    --prune

#############################################
# CACHE PERMISSION FIX
#############################################

mkdir -p /Volumes/Daten/Backup/restic-cache
chown -R credel:staff /Volumes/Daten/Backup/restic-cache || true
chmod 755 /Volumes/Daten/Backup/restic-cache || true

#############################################
# TIME MACHINE
#############################################

log "Triggering Time Machine"

tmutil startbackup --auto >/dev/null 2>&1 || true

log "===== V19 SPLIT BACKUP DONE ====="

Test-Recovery (Validierung meiner Backups)

1. Snapshot auswählen

restic -r /Volumes/Daten/Backup/restic-repo snapshots

ID merken (z. B. de8e6ede)


2. Restore in Test-Verzeichnis

restic -r /Volumes/Daten/Backup/restic-repo restore de8e6ede \
–target /Volumes/Daten/RESTORE_TEST/

3. Daten prüfen (Quick Check)

find /Volumes/Daten/RESTORE_TEST -type f | grep -E „pdf|jpg|png“ | head

Ziel:

  • echte Dateien vorhanden ✔
  • Verzeichnisstruktur korrekt ✔

4. DB Dumps prüfen

ls -lh /Volumes/Daten/RESTORE_TEST/Volumes/Daten/Backup/tmp/db-backup/

Optional tiefer:

head -n 20 nextcloud.sql
head -n 20 paperless.sql

Ziel:

  • SQL Dumps vorhanden ✔
  • keine leeren Dateien ✔

5. (Optional) Stichproben öffnen

  • PDFs öffnen
  • Bilder ansehen

Ziel:

  • Daten wirklich lesbar ✔

Was du damit validierst

  • Restic Repository ist konsistent
  • Snapshots sind vollständig
  • Daten sind physisch wiederherstellbar
  • DB Dumps sind nutzbar

⚠️ Wichtige Regel

Nie direkt ins Livesystem restoren
→ immer in:

/RESTORE_TEST/

Kurzform (Merksatz)

Snapshot → Restore → Dateien prüfen → DB prüfen → fertig

#############################

Notfall-Runbook (Ultra-kompakt)

Scenario: SSD/Host komplett defekt – Recovery aus Time Machine + Restic


0. Voraussetzungen (neues System)

  • macOS oder Linux neu installiert
  • Docker installiert
  • Zugriff auf Time Machine Disk

1. Time Machine einbinden

GUI (empfohlen)

  • Time Machine Disk mounten
  • Finder → Time Machine öffnen

Ziel:

/Users/credel/
/Volumes/Daten/

Restore nach:

/Users/<user>/
/Volumes/Daten/

2. Restore von Restic Repository (KRITISCH)

Aus Time Machine wiederherstellen:

/Volumes/Daten/Backup/restic-repo

Ziel:

~/restic-repo

3. Restic prüfen

restic -r ~/restic-repo snapshots

Wenn OK → Repository intakt


4. Ziel-Ordner für Restore vorbereiten

mkdir -p /Volumes/Daten/RESTORE_RECOVERY

5. Snapshot Restore

restic -r ~/restic-repo restore de8e6ede \
–target /Volumes/Daten/RESTORE_RECOVERY/

6. Docker Daten zurückspielen

Nextcloud / Paperless / NPM / Adguard Daten:

aus:

/Volumes/Daten/RESTORE_RECOVERY/

nach:

/Volumes/Daten/docker/

7. Docker Stack starten

docker compose up -d

oder einzeln:

docker start <container>

8. Paperless (kritischer Check)

Prüfen:

  • DB läuft
  • Media vorhanden
  • Web UI erreichbar

9. Nextcloud (optional rebuild)

Falls nötig:

  • Container neu deployen
  • /data wieder mounten
  • Login prüfen

10. Validierung (Minimal)

docker ps
restic snapshots
ls /Volumes/Daten/docker/

⚠️ KRITISCHE ABHÄNGIGKEITEN

✔ Restic Repo = ESSENTIAL
✔ Paperless DB + media = CRITICAL
✔ Docker Compose Files = MUST HAVE
✔ Time Machine = LAST RESORT BACKUP LAYER


MENTAL MODEL

Time Machine

Restic Repo Restore

Snapshot Restore

Docker Stack Rebuild

Paperless Validated

END STATE

Wenn alles funktioniert:

  • Paperless läuft wie vorher
  • Container stack ist wiederhergestellt
  • Daten konsistent
  • System vollständig rekonstruierbar

#############################

Datenintegrität über Zeit:

Das ist der einzige Punkt, den man im Auge behalten sollte. Minimal-Empfehlung ohne Script anzufassen. Ab und zu manuell:

restic -r /Volumes/Daten/Backup/restic-repo check --read-data-subset=5%

example output:

credel@CREDELS-3540 Scripts % restic -r /Volumes/Daten/Backup/restic-repo check –read-data-subset=5%
using temporary cache in /var/folders/tq/vkhnvll57s742h0qhrk6lnvc0000gn/T/restic-check-cache-857334196
create exclusive lock for repository
enter password for repository:
repository 92697ba5 opened (version 2, compression level auto)
created new cache in /var/folders/tq/vkhnvll57s742h0qhrk6lnvc0000gn/T/restic-check-cache-857334196
load indexes
[0:00] 100.00% 3 / 3 index files loaded
check all packs
check snapshots, trees and blobs
[0:00] 100.00% 12 / 12 snapshots
read 5.0% of data packs
[0:03] 100.00% 50 / 50 packs
no errors were found

Backups Runs automatisieren:

#############################

Restic Browser

Download (offiziell)

Restic Browser Releases öffnen

Dort findest du:

  • fertige macOS-Binaries (.dmg oder .app)
  • Versionen für Windows und Linux
  • immer die aktuellste Release-Version

Laut Projektbeschreibung werden die vorgebauten Programme genau dort bereitgestellt


Was du konkret machen musst (macOS)

  1. Release-Seite öffnen (Link oben)
  2. Neueste Version wählen (z. B. v0.3.x)
  3. Datei laden:
    • meist .dmg oder .tar.gz
  4. App nach /Applications ziehen
  5. Starten

⚠️ Wichtige Voraussetzung

Der Browser ist nur ein Frontend – du brauchst weiterhin das CLI:

  • restic muss installiert sein (z. B. via Homebrew)
  • und im PATH liegen

Das Tool ruft intern das restic-Binary auf

Beispiel:

brew install restic

Was du danach bekommst

Der Restic Browser kann:

  • Snapshots anzeigen
  • durch Ordner browsen
  • einzelne Dateien wiederherstellen
  • Daten als ZIP exportieren

Aber:

  • ❌ keine Backups konfigurieren
  • ❌ kein Scheduling

also genau mein Use Case: Restore + Browsing

Am Rande:

Was hier dokumentiert ist hat einige Tage an Arbeit gekostet. Ohne ChatGPT hätte ich das sowieso nie hinbekommen. Dennoch war es nicht ganz trivial, da mich ChatGPT oft irre geführt hat und immer wieder neu angepasste Backup scrips kaputt optimiert hat, was z.T. echt nervig war.