L lionbackup CLOUD
REGISTRIEREN LOGIN

Managed Database, eigenes Backup: unabhängig vom Cloud-Provider sichern

Veröffentlicht am 27. August 2026 · Anleitung · ← Blog

Eine Managed PostgreSQL oder MariaDB nimmt Ihnen Betrieb, Updates und Datensicherungen ab — aber diese Datensicherungen liegen beim selben Anbieter, im selben Konto, im selben Schadensradius wie die Datenbank selbst. Dieser Artikel zeigt, warum Workload und Backup getrennt gehören, und liefert fertige Docker- und Kubernetes-Beispiele: SQL-Dump, maximal komprimiert, verschlüsselt abgelegt bei einem souveränen europäischen Anbieter.

Warum das Backup nicht neben der Datenbank liegen darf

Managed Database as a Service ist eine gute Idee: Der Provider betreibt, patcht und überwacht die Datenbank, und automatische Datensicherungen gehören meist dazu. Was dabei leicht untergeht: Die Verantwortung für Ihre Daten bleibt bei Ihnen. Jedes Shared-Responsibility-Modell sagt genau das — der Provider sichert die Plattform, Sie sichern Ihre Inhalte.

Die Datensicherungen des Providers sind Betriebswerkzeug, keine Vorsorge für den Ernstfall. Sie teilen sich mit der Datenbank denselben Schadensradius:

Die Antwort ist eine doppelte Trennung. Logisch: Das Backup liegt bei einem anderen Anbieter, erreichbar nur über eigene Zugangsdaten, die mit Ihrem Cloud-Konto nichts zu tun haben. Verantwortungstechnisch: Für den Betrieb der Workload ist Ihr Cloud-Provider zuständig — für die letzte Kopie Ihrer Daten sind Sie es, und diese Kopie untersteht ausschließlich Ihnen. Bei lionbackup kommt dazu, dass die Kopie unzerstörbar abgelegt werden kann: Ein Write-Token darf nur schreiben, niemals lesen oder löschen, und unveränderbare Projekte schützen abgeschlossene Sicherungen selbst vor dem eigenen Konto. Gespeichert wird ausschließlich in Europa, bei einem souveränen Anbieter nach europäischem Recht.

Das Ergebnis: Selbst ein Komplettausfall Ihres Cloud-Providers ist dann ein sehr schlechter Tag — aber nicht das Ende Ihres Unternehmens. Der Dump lässt sich am nächsten Morgen bei einem anderen Provider oder auf einer eigenen Maschine einspielen.

Warum ein SQL-Dump — und warum die Kompression bei lionbackup aus bleibt

Für die getrennte Kopie ist ein SQL-Dump das richtige Format: Er ist providerneutral (jedes psql bzw. mariadb spielt ihn wieder ein), er enthält genau die eigenen Daten statt eines ganzen Blockgeräts, und er ist als Text hervorragend komprimierbar — oft auf ein Zehntel und kleiner. Das hält die gespeicherte Menge und damit die Kosten klein.

Komprimiert wird dabei genau einmal, direkt hinter dem Dump: mit pigz -9 (nutzt alle CPU-Kerne; wo es fehlt, übernimmt gzip -9 — gleiches Format, ein Kern). Im lionbackup-Client wird die Kompression dann abgeschaltet: compression_level: 0. Ein bereits maximal gepackter Dump wird durch eine zweite Kompressionsrunde nicht kleiner — sie würde nur CPU-Zeit kosten. Verschlüsselt wird unabhängig davon immer, auf Ihrer Seite, mit Ihrem Schlüssel.

compression_level: 0 braucht Client 0.1.16 oder neuer. Ältere Versionen behandeln 0 als „nicht gesetzt“ und komprimieren mit der Standardstufe weiter — es geht nichts kaputt, es kostet nur unnötig CPU.

Einmalige Vorbereitung

Drei Dinge braucht jedes der folgenden Beispiele — einmal eingerichtet, gelten sie für alle:

1. Projekt und Write-Token. Legen Sie im Portal ein Projekt an und erzeugen Sie darin ein Write-Token. Es kann ausschließlich sichern — weder lesen noch löschen. Selbst wer den Backup-Host vollständig übernimmt, kommt damit nicht an Ihre Sicherungen heran.

2. Schlüsselpaar. Der Client verschlüsselt vor dem Upload mit einem öffentlichen Schlüssel; der private bleibt bei Ihnen und wird nur zum Wiederherstellen gebraucht:

sudo mkdir -p /opt/lionbackup/conf /opt/lionbackup/keys /opt/lionbackup/dump
sudo chown 65532:65532 /opt/lionbackup/keys
cd /opt/lionbackup

docker run --rm -v /opt/lionbackup/keys:/keys \
  git.lionbackup.cloud/lionbackup/lionbackup-client:0.1.16 \
  --generate-key --key-name /keys/backup
Die Datei backup.key ist der einzige Weg zu Ihren Daten. Bewahren Sie eine Kopie an einem getrennten, sicheren Ort auf (Passwort-Manager, Tresor) — nicht auf dem Server und nicht beim Cloud-Provider. Ohne sie kann niemand die Sicherungen entschlüsseln, auch wir nicht.

3. Konfiguration und Compose-Datei. Die Konfiguration sichert den Dump-Ordner und schaltet die Kompression ab:

sudo tee /opt/lionbackup/conf/lionbackup.yaml >/dev/null <<'EOF'
lionbackup:
  project: <Projekt-UUID aus dem Portal>
  storagezone: <Storage-Zone des Projekts aus dem Portal>
  token: <Write-Token aus dem Portal>

  # Der Dump-Ordner aus den Skripten unten.
  files:
    - /dump

  # Der Dump ist schon maximal komprimiert (pigz -9) — nicht doppelt
  # komprimieren. 0 = aus (ab Client 0.1.16); verschluesselt wird immer.
  compression_level: 0

  encryption_keyfile: /keys/backup.pub
  tmp_directory: /tmp
  chunksize: 64
  log_level: INFO
EOF
# Der Client laeuft im Container als Benutzer 65532 und akzeptiert eine
# Token-Config nur, wenn ausschliesslich ihr Eigentuemer sie lesen kann.
sudo chown 65532:65532 /opt/lionbackup/conf/lionbackup.yaml
sudo chmod 600 /opt/lionbackup/conf/lionbackup.yaml
sudo tee /opt/lionbackup/docker-compose.yml >/dev/null <<'EOF'
services:
  backup:
    image: git.lionbackup.cloud/lionbackup/lionbackup-client:0.1.16
    command: ["--config", "/conf/lionbackup.yaml"]
    restart: "no"
    # Das Image ist ein scratch-Image ohne Shell und ohne Paketmanager.
    read_only: true
    volumes:
      - ./dump:/dump:ro
      - ./conf:/conf:ro
      - ./keys:/keys:ro
      - lionbackup-tmp:/tmp

  # Fuer den S3-Abschnitt weiter unten (eigene Konfiguration, eigener Ordner).
  backup-s3:
    image: git.lionbackup.cloud/lionbackup/lionbackup-client:0.1.16
    command: ["--config", "/conf/lionbackup-s3.yaml"]
    restart: "no"
    read_only: true
    volumes:
      - ./bucket:/bucket:ro
      - ./conf:/conf:ro
      - ./keys:/keys:ro
      - lionbackup-tmp:/tmp

volumes:
  lionbackup-tmp:
EOF

PostgreSQL 18 sichern

Mit Docker

Das komplette Skript — anpassen müssen Sie nur die vier Werte am Anfang (oder Sie setzen sie als Umgebungsvariablen):

#!/bin/sh
# PostgreSQL → lionbackup: Dump erzeugen, maximal komprimieren, verschluesselt ablegen.
DB_HOST="${DB_HOST:-db.example.com}"   # Endpunkt Ihrer Managed Database
DB_PORT="${DB_PORT:-5432}"
DB_USER="${DB_USER:-app}"
DB_NAME="${DB_NAME:-app}"
: "${DB_PASSWORD:?DB_PASSWORD als Umgebungsvariable setzen}"

set -eu
cd /opt/lionbackup

# pigz komprimiert auf allen Kernen; wo es fehlt, uebernimmt gzip (gleiches Format).
if command -v pigz >/dev/null 2>&1; then PACK="pigz -9"; else PACK="gzip -9"; fi

# --format=plain: ein Klartext-Dump, den jedes psql wieder einspielt — bei jedem
# Provider. --no-owner/--no-privileges laesst Rollen des alten Providers weg,
# die es am Ziel nicht gibt.
docker run --rm -e PGPASSWORD="$DB_PASSWORD" \
  docker.io/library/postgres:18 \
  pg_dump --host "$DB_HOST" --port "$DB_PORT" --username "$DB_USER" \
          --format=plain --no-owner --no-privileges "$DB_NAME" \
  | $PACK > "dump/$DB_NAME.sql.gz"

docker compose run --rm backup

Als Datei unter /opt/lionbackup/backup-postgres.sh ablegen, chmod +x, und per Cron oder systemd-Timer täglich laufen lassen — ein fertiges Timer-Beispiel steht in diesem Artikel.

Mit Kubernetes

Drei Objekte: ein Secret mit den Datenbank-Zugangsdaten, ein Secret mit Konfiguration und öffentlichem Schlüssel, ein CronJob. Zuerst die Zugangsdaten — nur die Werte anpassen:

apiVersion: v1
kind: Secret
metadata:
  name: lionbackup-db
stringData:
  DB_HOST: db.example.com   # Endpunkt Ihrer Managed Database
  DB_PORT: "5432"
  DB_USER: app
  DB_NAME: app
  DB_PASSWORD: bitte-setzen

Die Client-Konfiguration ist dieselbe wie oben — nur der Schlüsselpfad zeigt auf /conf (lionbackup.yaml mit encryption_keyfile: /conf/backup.pub):

kubectl create secret generic lionbackup-conf \
  --from-file=lionbackup.yaml --from-file=backup.pub

Ein Detail mit Gewicht: Kubernetes hängt Secret-Dateien als root:root und gruppen-/welt-lesbar ein — der Client verweigert eine so lesbare Token-Konfiguration grundsätzlich (Geheimnis-Schutz), und lesen könnte der Client-Benutzer 65532 eine 0400-Root-Datei auch nicht. Der Dump-Container legt Konfiguration und Schlüssel deshalb zu Beginn mit install -m 0600 für den Client-Benutzer in ein gemeinsames Volume — das erledigen die ersten zwei Zeilen seines Skripts:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: lionbackup-postgres
spec:
  schedule: "0 1 * * *"        # taeglich 01:00
  timeZone: Europe/Berlin
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 1
      template:
        spec:
          restartPolicy: Never
          initContainers:
            - name: dump
              image: docker.io/library/postgres:18
              command: ["/bin/sh", "-ec"]
              args:
                - |
                  # Config + Schluessel fuer den Client-Benutzer bereitstellen
                  # (Secret-Mounts sind root:root und gruppen-lesbar — s. o.):
                  install -m 0600 -o 65532 -g 65532 /secret/lionbackup.yaml /conf/lionbackup.yaml
                  install -m 0644 -o 65532 -g 65532 /secret/backup.pub /conf/backup.pub
                  if command -v pigz >/dev/null 2>&1; then PACK="pigz -9"; else PACK="gzip -9"; fi
                  pg_dump --host "$DB_HOST" --port "$DB_PORT" --username "$DB_USER" \
                          --format=plain --no-owner --no-privileges "$DB_NAME" \
                    | $PACK > "/dump/$DB_NAME.sql.gz"
              env:
                - name: PGPASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: lionbackup-db
                      key: DB_PASSWORD
              envFrom:
                - secretRef:
                    name: lionbackup-db
              volumeMounts:
                - name: dump
                  mountPath: /dump
                - name: conf
                  mountPath: /conf
                - name: conf-secret
                  mountPath: /secret
                  readOnly: true
          containers:
            - name: lionbackup
              image: git.lionbackup.cloud/lionbackup/lionbackup-client:0.1.16
              args: ["--config", "/conf/lionbackup.yaml"]
              volumeMounts:
                - name: dump
                  mountPath: /dump
                  readOnly: true
                - name: conf
                  mountPath: /conf
                  readOnly: true
                - name: tmp
                  mountPath: /tmp
          volumes:
            - name: dump
              emptyDir: {}
            - name: tmp
              emptyDir: {}
            - name: conf
              emptyDir: {}
            - name: conf-secret
              secret:
                secretName: lionbackup-conf

MariaDB sichern

Dasselbe Muster mit den MariaDB-Werkzeugen — gezeigt mit der aktuellen LTS-Reihe 11.8. --single-transaction erzeugt einen konsistenten Stand, ohne Tabellen zu sperren; --routines --triggers --events nimmt gespeicherte Logik mit, die sonst still fehlen würde.

Mit Docker

#!/bin/sh
# MariaDB → lionbackup: Dump erzeugen, maximal komprimieren, verschluesselt ablegen.
DB_HOST="${DB_HOST:-db.example.com}"   # Endpunkt Ihrer Managed Database
DB_PORT="${DB_PORT:-3306}"
DB_USER="${DB_USER:-app}"
DB_NAME="${DB_NAME:-app}"
: "${DB_PASSWORD:?DB_PASSWORD als Umgebungsvariable setzen}"

set -eu
cd /opt/lionbackup

if command -v pigz >/dev/null 2>&1; then PACK="pigz -9"; else PACK="gzip -9"; fi

# MYSQL_PWD statt --password: so taucht das Passwort in keiner Prozessliste auf.
docker run --rm -e MYSQL_PWD="$DB_PASSWORD" \
  docker.io/library/mariadb:11.8 \
  mariadb-dump --host "$DB_HOST" --port "$DB_PORT" --user "$DB_USER" \
               --single-transaction --routines --triggers --events "$DB_NAME" \
  | $PACK > "dump/$DB_NAME.sql.gz"

docker compose run --rm backup

Mit Kubernetes

Secret lionbackup-db wie beim PostgreSQL-Beispiel anlegen (mit DB_PORT: "3306") — der CronJob unterscheidet sich nur im Dump-Container:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: lionbackup-mariadb
spec:
  schedule: "0 1 * * *"        # taeglich 01:00
  timeZone: Europe/Berlin
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 1
      template:
        spec:
          restartPolicy: Never
          initContainers:
            - name: dump
              image: docker.io/library/mariadb:11.8
              command: ["/bin/sh", "-ec"]
              args:
                - |
                  # Config + Schluessel fuer den Client-Benutzer bereitstellen
                  # (Secret-Mounts sind root:root und gruppen-lesbar — s. o.):
                  install -m 0600 -o 65532 -g 65532 /secret/lionbackup.yaml /conf/lionbackup.yaml
                  install -m 0644 -o 65532 -g 65532 /secret/backup.pub /conf/backup.pub
                  if command -v pigz >/dev/null 2>&1; then PACK="pigz -9"; else PACK="gzip -9"; fi
                  mariadb-dump --host "$DB_HOST" --port "$DB_PORT" --user "$DB_USER" \
                               --single-transaction --routines --triggers --events "$DB_NAME" \
                    | $PACK > "/dump/$DB_NAME.sql.gz"
              env:
                - name: MYSQL_PWD
                  valueFrom:
                    secretKeyRef:
                      name: lionbackup-db
                      key: DB_PASSWORD
              envFrom:
                - secretRef:
                    name: lionbackup-db
              volumeMounts:
                - name: dump
                  mountPath: /dump
                - name: conf
                  mountPath: /conf
                - name: conf-secret
                  mountPath: /secret
                  readOnly: true
          containers:
            - name: lionbackup
              image: git.lionbackup.cloud/lionbackup/lionbackup-client:0.1.16
              args: ["--config", "/conf/lionbackup.yaml"]
              volumeMounts:
                - name: dump
                  mountPath: /dump
                  readOnly: true
                - name: conf
                  mountPath: /conf
                  readOnly: true
                - name: tmp
                  mountPath: /tmp
          volumes:
            - name: dump
              emptyDir: {}
            - name: tmp
              emptyDir: {}
            - name: conf
              emptyDir: {}
            - name: conf-secret
              secret:
                secretName: lionbackup-conf

Einen S3-Bucket sichern

Neben der Datenbank liegt bei vielen der zweite kritische Datenbestand im S3-kompatiblen Object Storage — Uploads, Dokumente, Exporte. Auch er verdient eine Kopie außerhalb des Providers. Das Muster: rclone lädt den kompletten Bucket in einen lokalen Ordner, dann sichert der Client diesen Ordner.

Ein Unterschied zur Datenbank: Der Bucket enthält gemischte Inhalte, die nicht vorkomprimiert sind — hier bleibt die Kompression im Client deshalb eingeschaltet. Die eigene Konfiguration dafür:

sudo tee /opt/lionbackup/conf/lionbackup-s3.yaml >/dev/null <<'EOF'
lionbackup:
  project: <Projekt-UUID aus dem Portal>
  storagezone: <Storage-Zone des Projekts aus dem Portal>
  token: <Write-Token aus dem Portal>

  files:
    - /bucket

  # Gemischte, unkomprimierte Inhalte: hier lohnt die Kompression im Client.
  compression_method: ZSTD
  compression_level: 5

  encryption_keyfile: /keys/backup.pub
  tmp_directory: /tmp
  chunksize: 64
  log_level: INFO
EOF
sudo chown 65532:65532 /opt/lionbackup/conf/lionbackup-s3.yaml
sudo chmod 600 /opt/lionbackup/conf/lionbackup-s3.yaml

Mit Docker

#!/bin/sh
# S3-Bucket → lionbackup: kompletten Bucket herunterladen, verschluesselt ablegen.
S3_ENDPOINT="${S3_ENDPOINT:-https://s3.example.com}"
S3_BUCKET="${S3_BUCKET:-mein-bucket}"
S3_ACCESS_KEY="${S3_ACCESS_KEY:-mein-access-key}"
: "${S3_SECRET_KEY:?S3_SECRET_KEY als Umgebungsvariable setzen}"

set -eu
cd /opt/lionbackup
mkdir -p bucket

# sync spiegelt den Bucket 1:1 nach ./bucket. Jede Sicherung bei lionbackup
# ist danach ein eigener, abgeschlossener Stand — auch wenn spaeter Objekte
# aus dem Bucket geloescht werden, bleibt der gesicherte Stand vollstaendig.
docker run --rm \
  -e RCLONE_CONFIG_SRC_TYPE=s3 \
  -e RCLONE_CONFIG_SRC_PROVIDER=Other \
  -e RCLONE_CONFIG_SRC_ENDPOINT="$S3_ENDPOINT" \
  -e RCLONE_CONFIG_SRC_ACCESS_KEY_ID="$S3_ACCESS_KEY" \
  -e RCLONE_CONFIG_SRC_SECRET_ACCESS_KEY="$S3_SECRET_KEY" \
  -v /opt/lionbackup/bucket:/bucket \
  docker.io/rclone/rclone:1.71 \
  sync "src:$S3_BUCKET" /bucket --transfers 8

docker compose run --rm backup-s3

Mit Kubernetes

apiVersion: v1
kind: Secret
metadata:
  name: lionbackup-s3
stringData:
  RCLONE_CONFIG_SRC_TYPE: s3
  RCLONE_CONFIG_SRC_PROVIDER: Other
  RCLONE_CONFIG_SRC_ENDPOINT: https://s3.example.com
  RCLONE_CONFIG_SRC_ACCESS_KEY_ID: mein-access-key
  RCLONE_CONFIG_SRC_SECRET_ACCESS_KEY: bitte-setzen
  S3_BUCKET: mein-bucket
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: lionbackup-s3
spec:
  schedule: "30 1 * * *"       # taeglich 01:30
  timeZone: Europe/Berlin
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 1
      template:
        spec:
          restartPolicy: Never
          initContainers:
            - name: bucket-sync
              image: docker.io/rclone/rclone:1.71
              command: ["/bin/sh", "-ec"]
              args:
                - |
                  # Config + Schluessel fuer den Client-Benutzer bereitstellen
                  # (Secret-Mounts sind root:root und gruppen-lesbar — s. o.):
                  install -m 0600 -o 65532 -g 65532 /secret/lionbackup-s3.yaml /conf/lionbackup-s3.yaml
                  install -m 0644 -o 65532 -g 65532 /secret/backup.pub /conf/backup.pub
                  rclone sync "src:$S3_BUCKET" /bucket --transfers 8
              envFrom:
                - secretRef:
                    name: lionbackup-s3
              volumeMounts:
                - name: bucket
                  mountPath: /bucket
                - name: conf
                  mountPath: /conf
                - name: conf-secret
                  mountPath: /secret
                  readOnly: true
          containers:
            - name: lionbackup
              image: git.lionbackup.cloud/lionbackup/lionbackup-client:0.1.16
              args: ["--config", "/conf/lionbackup-s3.yaml"]
              volumeMounts:
                - name: bucket
                  mountPath: /bucket
                  readOnly: true
                - name: conf
                  mountPath: /conf
                  readOnly: true
                - name: tmp
                  mountPath: /tmp
          volumes:
            - name: bucket
              emptyDir: {}
            - name: tmp
              emptyDir: {}
            - name: conf
              emptyDir: {}
            - name: conf-secret
              secret:
                secretName: lionbackup-conf

Das Secret lionbackup-conf muss dafür zusätzlich die Datei lionbackup-s3.yaml enthalten (kubectl create secret generic lionbackup-conf --from-file=lionbackup.yaml --from-file=lionbackup-s3.yaml --from-file=backup.pub). Bei großen Buckets statt emptyDir ein PersistentVolumeClaim als bucket-Volume verwenden — dann lädt rclone sync bei jedem Lauf nur die Änderungen nach.

Wiederherstellen — bei jedem Provider

Der Ernstfall ist der Moment, für den das alles gebaut ist — und er braucht ausdrücklich nicht den alten Cloud-Provider. Auf einer beliebigen Maschine: Client herunterladen, eine Konfiguration mit einem Read-Token anlegen (identisch zur obigen, nur mit dem Read- statt dem Write-Token), dazu der private Schlüssel aus dem Tresor:

# Was liegt vor?
lionbackup-client --config lionbackup-read.yaml --list

# Herunterladen und entschluesseln — der private Schluessel kommt NUR hier zum Einsatz:
lionbackup-client --config lionbackup-read.yaml \
  --restore <file_id aus --list> --identity backup.key --target ./restore

# PostgreSQL: Dump in eine neue Datenbank einspielen — egal bei welchem Provider.
gunzip -c restore/dump/app.sql.gz | psql --host <neue-db> --username app app

# MariaDB (--password ohne Wert fragt interaktiv nach):
gunzip -c restore/dump/app.sql.gz | mariadb --host <neue-db> --user app --password --database=app

Testen Sie genau diesen Weg regelmäßig — ein Backup, das nie zurückgespielt wurde, ist kein verlässliches Backup. Und weil der Dump providerneutral ist, ist der Test zugleich die Probe für den Tag, an dem Sie den Provider wechseln müssen.

Noch kein lionbackup-Projekt? Registrieren, Projekt anlegen, Write-Token erzeugen — die Beispiele oben laufen dann unverändert. Fragen zur Einrichtung beantwortet unser Kontakt oder die Dokumentation.