Skip to content

Backup & Disaster Recovery ​

Prinzip ​

Drei voneinander unabhängige Schichten:

  1. Stack-Definitionen liegen in Git (matev/server-stacks auf Forgejo + Mirror auf einem zweiten Forgejo oder GitHub-Privat).
  2. Datenbanken werden vor dem Volume-Backup per mysqldump / pg_dump in einen dumps/-Ordner pro Stack geschrieben.
  3. Volumes (alle *_data/, *_storage/, cms_content/, cms_media/, plus die Dump-Ordner) werden mit Restic auf eine NAS oder einen Backup-Server gepushed.

Recovery aus dem Vollausfall:

git clone server-stacks  →  restic restore Volumes  →  docker compose up -d

Zeit bis "wieder oben": ca. 30-60 Min wenn die Restic-Quelle erreichbar ist.

Was wo liegt ​

Zwei Verzeichnis-Bäume zu sichern:

home/bengel/stacks/                ← Compose + persistente Daten-Volumes
└── proxy/, mgmt/, git/, apps/

/var/www/                          ← Source-Code + storage/content/media
└── admin/, cms/, b2b/, staging/, docs/, storybook/, ...
Stack / PfadWas sichern
~/stacks/proxy/Docker-Volumes caddy_data, caddy_config (oder als Bind-Mount auf ~/stacks/proxy/caddy_data/)
~/stacks/mgmt/portainer_data/, matomo_data/, matomo_db_data/, dumps/
~/stacks/git/postgres_forgejo/, forgejo_data/, forgejo_config/, woodpecker_data/, dumps/
~/stacks/apps/apps_db_data/, meili_data/, opensearch_data/, ollama_data/, penpot_postgres/, penpot_assets/, dumps/
/var/www/admin/storage/Filament-Uploads + Logs
/var/www/cms/content/ + media/ + site/accounts/Kirby-Inhalt (kritisch — der CMS-Content!)
/var/www/b2b/storage/Livewire-Uploads
/var/www/<service>/.envKonfig-Secrets pro Service (oder separat im PW-Manager halten)

apps_redis_data/ wird nicht gesichert (rein flüchtige Cache/Queue-Daten). ollama_data/ ist optional — die Modelle lassen sich neu pullen, aber Wiederherstellung ist schneller mit Backup.

Datenbank-Dumps (vor Volume-Backup) ​

~/stacks/db-backup.sh (gemeinsames Script für alle Stacks):

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

STAMP=$(date +%Y-%m-%d_%H%M)
KEEP_DAYS=14

dump() {                                # dump <stack> <type> <container> <db> <user> <pass>
    local stack=$1 type=$2 container=$3 db=$4 user=$5 pass=$6
    local out=~/stacks/$stack/dumps/${db}-${STAMP}.sql.gz
    mkdir -p "$(dirname "$out")"
    case "$type" in
        mariadb) docker exec "$container" \
            mariadb-dump --single-transaction --quick --lock-tables=false \
            -u "$user" -p"$pass" "$db" | gzip > "$out" ;;
        mysql)   docker exec "$container" \
            mysqldump --single-transaction --quick --lock-tables=false \
            -u "$user" -p"$pass" "$db" | gzip > "$out" ;;
        pg)      docker exec "$container" \
            pg_dump -U "$user" -d "$db" | gzip > "$out" ;;
    esac
    # alte Dumps wegräumen
    find ~/stacks/$stack/dumps -name "${db}-*.sql.gz" -mtime +$KEEP_DAYS -delete
}

# mgmt
dump mgmt mariadb matomo-db    matomo       matomo "$MATOMO_DB_PASSWORD"

# git
dump git  pg      forgejo-db   forgejo      forgejo "$FORGEJO_DB_PASSWORD"

# apps
dump apps mariadb apps-db      matev_admin  matev   "$APPS_DB_PASSWORD"
dump apps mariadb apps-db      matev_b2b    matev   "$APPS_DB_PASSWORD"
dump apps pg      penpot-postgres penpot    penpot  "$PENPOT_DB_PASSWORD"

# taiga
dump taiga pg     taiga-db        taiga     taiga   "$TAIGA_DB_PASSWORD"

Cron (root):

30 2 * * * /home/bengel/stacks/db-backup.sh >> /var/log/matev-db-backup.log 2>&1

Volume-Backup mit Restic ​

Restic ist deduplizierend, verschlüsselt, inkrementell — perfekt für tägliche Snapshots.

Setup (einmalig) ​

bash
# Restic installieren
apt install -y restic

# Repo initialisieren (auf NAS via SFTP)
export RESTIC_REPOSITORY="sftp:backup@nas.matev.eu:/volume1/backup/matev"
export RESTIC_PASSWORD_FILE=/root/.restic-password
echo "<langes-zufaelliges-passwort>" > /root/.restic-password
chmod 600 /root/.restic-password

restic init

Alternativ: B2 / S3 / Hetzner-Storage-Box als Repo.

Tägliches Backup-Script ​

/usr/local/sbin/matev-backup.sh:

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

export RESTIC_REPOSITORY="sftp:backup@nas.matev.eu:/volume1/backup/matev"
export RESTIC_PASSWORD_FILE=/root/.restic-password

# Snapshot beider Hierarchien — stacks/ (Compose+Daten) und /var/www/ (Code+Storage)
restic backup /home/bengel/stacks /var/www \
    --tag daily \
    --exclude='*/apps_redis_data/*' \
    --exclude='*/woodpecker_data/build_*' \
    --exclude='*/node_modules/*' \
    --exclude='*/vendor/*' \
    --exclude='*.tmp'

# Retention: 7 daily, 4 weekly, 6 monthly, 2 yearly
restic forget --prune \
    --keep-daily 7 \
    --keep-weekly 4 \
    --keep-monthly 6 \
    --keep-yearly 2

# Integrität prüfen (1× pro Woche, hier täglich Stichprobe)
restic check --read-data-subset=2%

Cron (root, nach dem DB-Dump):

0 3 * * * /usr/local/sbin/matev-backup.sh >> /var/log/matev-backup.log 2>&1

Caddy-Volumes ​

caddy_data + caddy_config sind benannte Docker-Volumes (kein Bind-Mount im stacks/-Verzeichnis). Entweder:

  • Variante A (empfohlen): die Volumes in proxy/docker-compose.yml auf Bind-Mounts umstellen — ./caddy_data:/data, ./caddy_config:/config — dann sind sie automatisch im Restic-Snapshot drin.
  • Variante B: Restic-Backup zusätzlich auf /var/lib/docker/volumes/ ansetzen (umständlicher, läuft als root, fragiler).

Recovery-Test ​

Mindestens monatlich auf einer anderen Maschine durchspielen — sonst ist das Backup nicht beweisbar:

bash
# Auf einem Test-Host:
restic snapshots
restic restore latest --target /tmp/restore-test
ls /tmp/restore-test/home/bengel/stacks/apps/    # sollte alle Volumes zeigen

Disaster-Recovery-Pfad ​

Server brennt ab. Neuer Server steht. Wiederherstellung:

bash
# 1) Basis-Setup (Docker, Compose, gateway-Netz)
curl -fsSL https://get.docker.com | sh
docker network create gateway

# 2) Stack-Definitionen aus Git holen
mkdir -p /home/bengel/stacks
cd /home/bengel/stacks
git clone https://git.matev.eu/matev/server-stacks.git .
# Wenn Forgejo gerade auch hops ist: vom GitHub-Mirror

# 3) Stacks + Webroots aus Restic restoren
export RESTIC_REPOSITORY="sftp:backup@nas.matev.eu:/volume1/backup/matev"
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic restore latest --target / --include /home/bengel/stacks --include /var/www
chown -R bengel:bengel /home/bengel/stacks
chown -R bengel:www-data /var/www
find /var/www -type d -exec chmod 2775 {} \;

# 4) .env-Dateien wiederherstellen
# Die liegen NICHT in Git, sondern werden separat in einem Passwort-Manager
# (1Password / Bitwarden) gehalten — händisch in jeden Stack reinkopieren:
#   ~/stacks/mgmt/.env, ~/stacks/git/.env, ~/stacks/apps/.env
#   /var/www/admin/.env, /var/www/b2b/.env, /var/www/cms/.env

# 5) Hochfahren in der richtigen Reihenfolge
cd /home/bengel/stacks/proxy && docker compose up -d
cd /home/bengel/stacks/mgmt  && docker compose up -d
cd /home/bengel/stacks/git   && docker compose up -d
cd /home/bengel/stacks/apps  && docker compose up -d

# 6) Caddy macht beim ersten Request automatisch neue Zertifikate
# (sofern DNS schon umgestellt ist und die alten caddy_data nicht restored wurden)

# 7) Smoke-Tests
curl -I https://admin.matev.eu
curl -I https://cms.matev.eu
curl -I https://staging.matev.eu

Was NICHT durchs Backup abgedeckt ist ​

  • .env-Dateien: bewusst nicht im Git, also auch nicht im Restic — separat im Passwort-Manager halten.
  • OAuth-Client-Secrets in Forgejo/Taiga/Penpot: stehen meistens nur in deren DB, sind aber im DB-Dump enthalten — außer sie wurden über UI nach dem Restore neu generiert.
  • Build-Caches in /var/lib/docker/: kommen beim nächsten Build neu, kein Backup nötig.
  • Let's-Encrypt-Account-Key: liegt in caddy_data — wenn man Variante A oben nimmt, ist er drin.

Vergleich Git-based Compose vs. Portainer-managed Stacks ​

Das hier ist auch der Grund, warum wir Stacks nicht in Portainer anlegen, sondern als Git-Repo halten:

Git-based Compose (✅ unsere Wahl)Portainer-managed
Stack-Definition wo?~/stacks/<name>/docker-compose.yml (in Git)portainer.db
Disaster-Recoverygit clone + Restic-Restore + up -dPortainer muss vor allem anderen wieder laufen, dann erst die Stacks
Versionierunggit log zeigt jede Änderungnur Portainer-UI-Audit
Review vor ÄnderungPull-Requestwer im UI klickt, ändert prod
Backup-AufwandVolumes + DumpsVolumes + Dumps + portainer.db
Anzahl SPOFsRestic + GitRestic + Git + Portainer-DB

Portainer bleibt also nur Cockpit: Logs ansehen, Container neu starten, Shell aufmachen, Resource-Verbrauch sehen. Stacks erstellt/editiert man dort nicht.