Um 3 Uhr morgens vibrierte das Diensttelefon: Monitoring meldete die /-Partition bei 98 % Belegung, MySQL konnte keine Binlogs schreiben, und CI-Deployments brachen mitten im docker pull mit no space left on device ab. Per SSH und df -h fanden wir auf Root gerade einmal 412 MB frei – kein Malware-Befall, sondern drei Monate unbeachtete Logs, Docker-Layer, apt-Cache und Journal, die langsam eine 40-GB-VPS-Platte auffraßen.
Dieses Szenario ist auf kleinen VPS-Instanzen allgegenwärtig: wenig Speicher, Container rund um die Uhr, ständige Image-Pulls und unerbittliches Logging. Manuelles rm -rf ist langsam und fehleranfällig; das zuverlässige Vorgehen heißt zuerst die größten Verbraucher finden, dann regenerierbare Verzeichnisse per Skript in einem Rutsch bereinigen. Dieser Artikel bündelt das One-Click-Cleanup-Skript, das wir in Produktion wiederholt eingesetzt haben – mit klaren Hinweisen, was sicher automatisiert werden kann und was eine zweite Prüfung braucht.
Voraussetzung: SSH-Zugang und sudo. Wenn gar keine Verbindung klappt (z. B. weil SSH-Keys nicht geschrieben werden können), zuerst unseren Leitfaden zu abgelehnten SSH-Verbindungen auf Linux-Servern nutzen, um volle Platte vs. Netzwerk/Firewall zu unterscheiden. Bevor Sie UFW-Regeln anfassen, zuerst Speicher freimachen – Dienste brauchen Platz für Logs, bevor wieder alles normal läuft.
Volle Platte: Was bricht zuerst?
Ist das Root-Dateisystem voll, wirken Linux-Symptome oft zufällig: SSH geht noch, aber vim speichert nicht, apt install scheitert, Docker-Container starten nicht, Nginx liefert 500, während error.log nicht mehr wächst. MySQL und PostgreSQL gehen schreibgeschützt oder stürzen ab; systemd-Dienste starten in Schleifen neu, weil pid-Dateien oder Sockets nicht angelegt werden können. Cron-Jobs scheitern leise, wenn Logs nicht mehr beschrieben werden können.
Schritt eins ist immer der Blick auf Mountpoints – nicht das Raten, welcher Ordner „groß aussieht“. Ein separates /data-Volume kann noch Luft haben, während Root blockiert. „Platte voll“ ohne df -hT als eine Zahl zu behandeln, hat schon manchen in die falsche Spur geschickt:
# Freier Speicher pro Partition df -hT # Größte Verzeichnisse direkt unter / (kann dauern) sudo du -xh --max-depth=1 / 2>/dev/null | sort -hr | head -20 # Inode-Erschöpfung wirkt wie volle Platte df -ih
du braucht auf Hosts mit Millionen Dateien lange. Ist die Maschine schon träge, gezielt prüfen: sudo du -sh /var/lib/docker /var/log /var/cache. Das offizielle du(1)-Handbuch erklärt -x (ein Dateisystem) und --apparent-size – bei Bind-Mounts oder separaten Volumes nicht von Summen über Mount-Grenzen hinweg täuschen lassen.
Typische Speicherfresser: Referenztabelle
Jeder Stack ist anders, aber die folgenden Pfade tauchen auf Ubuntu- und Debian-VPS ständig auf. Web + Docker + CI auf einer 40-GB-Platte ist ein vertrautes Rezept: Journal und Container-Layer wachsen im Hintergrund, Anwendungslogs explodieren bei Traffic-Spitzen. Die Spalte Auto-sicher? markiert, ob ein Skript sie auf einen Schlag leeren darf – Zeilen mit „Vorsicht“ brauchen eine Abhängigkeitsprüfung. Im Zweifel du dry-run und mit lsof +D /path prüfen, welcher Prozess Dateien offen hält, bevor gelöscht wird.
| Pfad / Quelle | Typische Größe | Auto-sicher? | Hinweise |
|---|---|---|---|
/var/lib/docker |
Mehrere GB bis zig GB | Teilweise | Dangling Images, gestoppte Container, ungenutzte Volumes; Daten-Volumes laufender Container nicht löschen |
/var/log/journal |
1–10+ GB | Ja | systemd-Journal wächst schnell ohne Größenlimit |
/var/log/*.log |
Hunderte MB bis GB | Vorsicht | Truncate oder logrotate; direktes rm kann offene fds und stale Inodes hinterlassen |
/var/cache/apt |
Hunderte MB bis mehrere GB | Ja | apt-get clean ist sicher – entfernt nur heruntergeladene .deb-Pakete |
/tmp, /var/tmp |
Variabel | Teilweise | Dateien ohne Zugriff seit 7 Tagen löschen; prüfen, ob Apps Uploads hier ablegen |
Nginx proxy_cache |
Mehrere GB | Ja | Cache baut sich neu auf; nach unserem Nginx-Reverse-Proxy-Tuning kurzzeitig mehr MISS erwarten |
Alte Kernel unter /boot |
Hunderte MB | Ja | apt autoremove --purge entfernt alte Kernel; aktuellen und vorherigen behalten |
| Core Dumps, Absturzberichte | Variabel | Ja | /var/crash, Anwendungs-core.*-Dateien |
/home, /var/lib/mysql, Object-Storage-Mounts oder Backup-Verzeichnisse oben, hilft Cache-Cleanup nicht – Sie brauchen Archivierung, Resize oder Cold-Data-Migration. Skripte holen regenerierbaren Speicher zurück; sie ersetzen keine Kapazitätsplanung.
One-Click-Cleanup-Skript (Copy-Paste)
Das Skript unten läuft standardmäßig im Dry-Run: Es zeigt freizugebenden Speicher und geplante Befehle. Mit --apply wird tatsächlich gelöscht. Immer zuerst dry-run und Ausgabe prüfen – Teams, die das einmal übersprungen und ein wochenaltes Basis-Image als einzige Kopie eines stillgelegten Dienstes geprunt haben, kennen wir. Module sind getrennt; ohne Docker den Docker-Block auskommentieren, ohne proxy_cache den Nginx-Teil.
#!/usr/bin/env bash # Server-Disk-Cleanup — nur regenerierbarer Cache # Nutzung: sudo ./disk-cleanup.sh # Vorschau # sudo ./disk-cleanup.sh --apply # ausführen set -euo pipefail APPLY=false [[ "${1:-}" == "--apply" ]] && APPLY=true log() { echo "[$(date '+%F %T')] $*"; } run() { if $APPLY; then log "EXEC: $*" eval "$@" else log "DRY-RUN: $*" fi } before=$(df -h / | awk 'NR==2 {print $3 " used, avail " $4}') log "Before: $before" # 1. apt-Cache if command -v apt-get >/dev/null; then run "apt-get clean -y" run "apt-get autoremove -y --purge" fi # 2. systemd journal — letzte 7 Tage oder 500MB behalten if command -v journalctl >/dev/null; then run "journalctl --vacuum-time=7d" run "journalctl --vacuum-size=500M" fi # 3. Ungenutzte Docker-Ressourcen (ohne benannte Volume-Geschäftsdaten) if command -v docker >/dev/null && docker info >/dev/null 2>&1; then run "docker system prune -af --filter 'until=168h'" run "docker builder prune -af --filter 'until=168h'" fi # 4. Temp-Verzeichnisse — 7 Tage ohne Zugriff run "find /tmp /var/tmp -type f -atime +7 -print 2>/dev/null | head -20" if $APPLY; then find /tmp /var/tmp -type f -atime +7 -delete 2>/dev/null || true fi # 5. Nginx proxy_cache (cache_path anpassen) NGINX_CACHE="/var/cache/nginx" if [[ -d "$NGINX_CACHE" ]]; then run "du -sh '$NGINX_CACHE'" run "find '$NGINX_CACHE' -type f -delete 2>/dev/null || true" fi # 6. Absturz-Dumps [[ -d /var/crash ]] && run "rm -rf /var/crash/*" # 7. pip / npm Cache (falls vorhanden) [[ -d /root/.cache/pip ]] && run "rm -rf /root/.cache/pip/*" for u in /home/*; do [[ -d "$u/.npm" ]] && run "npm cache clean --force --cache '$u/.npm' 2>/dev/null || rm -rf '$u/.npm/_cacache'" done after=$(df -h / | awk 'NR==2 {print $3 " used, avail " $4}') log "After: $after" $APPLY || log "Dry-Run fertig. Wenn ok: sudo $0 --apply"
Rechte setzen: sudo chmod 750 /usr/local/sbin/disk-cleanup.sh. Beim ersten Lauf ohne --apply die DRY-RUN-Ausgabe speichern. Zeigt der Docker-Teil große Image-Löschungen, prüfen, ob nicht die einzige lokale Kopie eines bereits gelöschten Registry-Tags verschwindet.
docker system prune -a entfernt alle ungenutzten Images und erzwingt beim nächsten Deploy einen vollen Re-Pull – schmerzhaft bei knappem VPS-Bandbreite. Sicherer: --filter until=168h, damit nur wochenalte ungenutzte Layer gehen, oder regelmäßig docker image prune -f nur für Dangling Images. Siehe die offizielle Docker-Pruning-Dokumentation.
Modul für Modul: was weg darf, was nicht
journalctl: --vacuum-time und --vacuum-size lassen sich kombinieren; systemd kürzt Binärlogs gemäß der journalctl-Dokumentation. Für ein dauerhaftes Limit SystemMaxUse=500M in /etc/systemd/journald.conf, dann systemctl restart systemd-journald.
Logdateien: Bei großen, noch geöffneten Logs ist truncate -s 0 /var/log/nginx/access.log sicherer als rm – der Prozess behält sein fd; Löschen der Datei entfernt nur den Verzeichniseintrag, ohne Speicher sofort freizugeben. Langfristig logrotate mit täglicher Rotation und N Generationen für Nginx und Anwendungslogs.
apt: apt-get clean leert nur /var/cache/apt/archives; autoremove --purge entfernt alte Kernel und verwaiste Abhängigkeiten. Nach Kernel-Upgrade aktuellen und vorherigen Kernel behalten. In Produktion vor autoremove uname -r gegen dpkg -l 'linux-image-*' prüfen.
Geschäftsdaten nicht anfassen: /var/lib/mysql, /var/lib/postgresql, Redis-RDB, Upload-Verzeichnisse und Backup-Mounts liegen außerhalb des Skripts. Stehen sie bei du oben, brauchen Sie Archivierung, Resize oder Cold Storage – kein Cache-Cleanup.
Nach dem Cleanup: verhindern, dass die Platte wieder leise voll läuft
Root von 98 % auf 60 % zu bringen, ist nur der Anfang. Ohne Leitplanken wächst das Journal in sechs Wochen wieder auf 8 GB, Docker-Layer häufen sich nach jedem Deploy, und ein Traffic-Peak wiederholt den Vorfall. Unser Mindest-Präventions-Stack für kleine VPS:
- Alert-Schwellen: Cloud-Konsole oder Prometheus bei 80 %, SMS/Pager bei 85 % – nicht bis 95 % warten.
- Wöchentlicher Cron:
0 4 * * 0 root /usr/local/sbin/disk-cleanup.sh --apply >> /var/log/disk-cleanup.log 2>&1 - journald + logrotate Limits: unbegrenztes Wachstum in ein erträgliches Deck verwandeln.
Bei Docker Compose max-size und max-file für json-file-Logging setzen – sonst füllt stdout eines Containers die Platte genauso sicher wie verwaiste Image-Layer, und diese Falle wird oft übersehen.
Checkliste vor dem Eingriff
In Ihr Runbook übernehmen und bei der nächsten Meldung der Reihe nach abarbeiten:
df -hTunddf -ih— Speicher vs. Inode-Erschöpfungdu -shfür docker, journal, log, cache- Skript dry-run → menschliche Prüfung →
--apply - Nach Cleanup
df -h, dann DB, Nginx, Docker spot-checken - journald-Limits, logrotate, Cron und 80-%-Alerts ergänzen
Volle Platte ist nie nur „ein paar Dateien löschen“ – es ist ein Gesundheitscheck für Kapazität und Logging-Policy. Das Skript ersetzt panisches rm um 3 Uhr durch einen wiederholbaren Standard. Dokumentieren, was gelöscht wurde und wie weit df sprang – oft fehlt dann eine logrotate-Stanza oder Container-Logging ohne Rotation. Der Rest ist Prävention, damit der Pager ruhig bleibt.
Build-Artefakte fressen die Platte? Rollen trennen
Läuft auf Ihrer VPS Produktion und lokales xcodebuild, mehrstufige Docker-Builds oder Ollama-Model-Caches, kann eine kleine Platte in einer Compile-Woche verschwinden – DerivedData und Image-Layer konkurrieren mit Produktionslogs auf derselben SSD.
Sauberer getrennt: Produktions-VPS schlank halten, schwere Builds woanders – Cloud-Mac für iOS-Pakete, dedizierter CI-Runner, Produktion zieht nur finale Artefakte.
Mac mini M4 mit Unified Memory und niedrigem Leerlaufverbrauch eignet sich als „nur Build, kein 24/7-Traffic“-Knoten. Mit Linux-VPS für Runtime kämpfen Compile-Caches nicht mehr mit Produktionslogs um dieselbe Platte. Planen Sie diese Trennung, lagern Sie schwere Arbeit zuerst in die Cloud-Mac aus – VPSSpark-Tarife ansehen und weniger 3-Uhr-Sessions mit df -h.