Der 35-Watt-Mitbewohner (Teil 4): Ein Docker Cleanup und die chirurgische Datenrettung
Manchmal will man nur mal kurz aufräumen und reißt versehentlich das halbe Haus ab. Genau so erging es mir letzte Woche mit meinem Home Lab. Ich wollte auf dem Lenovo M920q ein wenig Platz schaffen und führte ein routinemäßiges Docker Cleanup durch. Ein paar ungenutzte Images löschen, verwaiste Volumes entfernen. Routine eben.
Doch als ich danach meine Gitea Instanz aufrief, traf mich der Schlag: Die Seite war leer. Keine Repositories, keine User, alles war auf Werkseinstellungen zurückgesetzt. Mein kompletter Code war weg.

Der fatale Fehler im Compose File
Wie konnte das passieren? Die Antwort lag in meiner docker-compose.yml. Ich hatte beim Einrichten einen massiven Architektur-Fehler gemacht. Mein Volume Mapping sah so aus:
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
volumes:
- /var/lib/docker/volumes/infra_volume/_data/gitea_data:/data
Auf den ersten Blick sieht das aus wie ein normaler Pfad. Das Problem ist aber, dass /var/lib/docker/volumes/ der interne Tresorraum von Docker ist. Ich habe Docker ausgetrickst und per Bind Mount direkt auf die rohen Daten eines anderen Volumes (infra_volume) zugegriffen. Da Docker diese Abhängigkeit nicht verstanden hat, stufte der Cleanup Job das ursprüngliche Volume als “ungenutzt” ein und hat es restlos gelöscht.
Die chirurgische Rettung aus dem Proxmox Backup
Die VM komplett aus dem Backup wiederherzustellen, hätte ewig gedauert und mein restliches Setup überschrieben. Ich wollte nur an genau diesen einen Ordner im Backup heran. Ich musste die Daten also am offenen Herzen operieren und direkt aus der Proxmox Sicherung extrahieren, ohne die VM jemals zu booten.
Das Backup lag als komprimierte Datei auf meinem Storage:
vzdump-qemu-100-2026_02_23-01_00_02.vma.zst
Zuerst musste ich die Kompression entfernen:
zstd -d vzdump-qemu-100-2026_02_23-01_00_02.vma.zst
Danach entpackte ich das rohe VMA Archiv mit dem Proxmox eigenen Tool in einen temporären Ordner:
vma extract vzdump-qemu-100-2026_02_23-01_00_02.vma /mnt/restore
Jetzt hatte ich eine rohe Image Datei namens tmp-disk-drive-scsi0.raw vor mir liegen. Ein solches Image kann man nicht einfach mounten. Es enthält eigene Partitionen und im Falle von Ubuntu auch LVM Strukturen. Ich musste das Image als Loop Device einhängen:
losetup -f
losetup /dev/loop0 /mnt/restore/tmp-disk-drive-scsi0.raw
kpartx -av /dev/loop0
Mit dem Befehl kpartx machte ich die Partitionen im Image für das Host System sichtbar. Da Ubuntu LVM nutzt, musste ich die Volume Group innerhalb des Backups aufwecken:
vgscan
vgchange -ay
Das System fand sofort die Volume Group ubuntu-vg. Jetzt konnte ich die Partition endlich wie eine normale Festplatte einhängen und zu meinen verlorenen Daten navigieren:
mount -t ext4 /dev/ubuntu-vg/ubuntu-lv /mnt/vm
cd /mnt/vm/var/lib/docker/volumes/infra_volume/_data/gitea_data
Da waren sie wieder. Meine Repositories und die Gitea Datenbank waren gerettet.
Die NFS Falle und ein SQLite Wunder
Beim Zurückkopieren gab es direkt die nächste Hürde. Mein neues Ziel war ein sicheres NFS Share auf meiner Synology NAS. NFS nutzt standardmäßig root_squash, was bedeutet, dass der Root User des Proxmox Servers nicht einfach Dateien mit vollen Rechten auf das Share schieben darf. Ein normales Kopieren schlug fehl.
Die Lösung war rsync ohne die Übernahme der Rechte:
rsync -rlt --no-perms --no-owner --no-group /mnt/vm/var/... /mnt/gitea/
Die Erleichterung war groß, aber von kurzer Dauer. Die Datei gitea.db war korrupt, vermutlich weil das Backup genau in dem Moment lief, als die Datenbank beschrieben wurde. Zum Glück hat SQLite einen mächtigen Reparaturmodus:
sqlite3 gitea.db ".recover" | sqlite3 gitea_fixed.db
Falls das abbricht, kann man SQLite zwingen, Fehler beim Importieren zu ignorieren:
sqlite3 gitea.db ".recover" | sqlite3 -init <(echo ".bail off") gitea_fixed.db
Das neue und sichere Setup
Nachdem ich die Datenbank ersetzt und alles sauber ausgehängt hatte (umount, vgchange -an, losetup -d), war es Zeit, aus dem Fehler zu lernen.
Mein neues Compose File verzichtet auf gefährliche Pfade und speichert die Daten stattdessen über einen direkten Mount Punkt sicher auf der NAS. Egal was Docker in Zukunft auf dem Lenovo anstellt, die Daten liegen unantastbar auf dem Storage:
version: "3.9"
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: always
networks: [cloudflare]
ports:
- "3000:3000"
- "2222:2222"
volumes:
- /mnt/gitea:/data
runner:
image: gitea/act_runner:latest
container_name: gitea-runner
restart: always
depends_on: [gitea]
networks: [cloudflare]
environment:
GITEA_RUNNER_NAME: CI-Runner
CONFIG_FILE: /data/config.yaml
GITEA_INSTANCE_URL: http://gitea:3000
GITEA_RUNNER_REGISTRATION_TOKEN_FILE: /run/secrets/gitea_runner_token
volumes:
- runner_data:/data
- /var/run/docker.sock:/var/run/docker.sock
networks:
cloudflare:
external: true
volumes:
runner_data:
external: true
secrets:
gitea_runner_token:
file: /data/compose/secrets/gitea/gitea_runner_token
Lessons Learned
Was habe ich daraus gelernt? Nutze niemals /var/lib/docker/volumes/ als Quelle für einen Bind Mount. Und vor allem: Ein Backup ist erst dann etwas wert, wenn man weiß, wie man im Notfall chirurgisch genau an die Daten herankommt.
Die wichtigsten Erkenntnisse:
- Niemals Docker-interne Paths für Bind Mounts verwenden
- NFS Mounts immer mit
--no-perms --no-owner --no-groupverwenden - SQLite Reparaturmodus kennen und nutzen können
- Backups regelmäßig testen und Wiederherstellungsprozesse dokumentieren