Trois heures du matin, l’alerte monitoring fait vibrer le téléphone d’astreinte : la partition / est à 98 %, MySQL n’écrit plus les binlogs, et le déploiement CI s’arrête net au docker pull avec no space left on device. On se connecte, on lance df -h : il reste 412 Mo sur la racine — pas une intrusion, juste trois mois de logs, couches Docker, cache apt et journal systemd qui ont progressivement saturé un disque VPS de 40 Go.
Ce scénario est monnaie courante sur les petits VPS : disque modeste, conteneurs 24 h/24, pulls d’images et journalisation continue. Un rm -rf à la main est lent et dangereux ; la bonne approche, c’est identifier les gros consommateurs, puis nettoyer en lot les répertoires « régénérables ». Cet article reprend le script de nettoyage que nous exécutons régulièrement en production, avec des repères clairs sur ce qui peut être automatisé et ce qui mérite une relecture humaine.
Prérequis : accès SSH et sudo. Si vous ne pouvez plus vous connecter (clé SSH impossible à écrire, par exemple), commencez par notre guide de dépannage SSH refusé sur serveur Linux pour distinguer disque plein et problème réseau/pare-feu. Avant de retoucher les règles UFW, libérez d’abord de l’espace — les services ont besoin de marge pour écrire leurs logs.
Disque plein : qu’est-ce qui casse en premier ?
Quand le système de fichiers racine est saturé, Linux produit des symptômes déroutants : SSH fonctionne encore, mais vim ne sauvegarde plus, apt install échoue, Docker refuse de démarrer les conteneurs, et Nginx renvoie des 500 tandis que error.log cesse de grossir. MySQL et PostgreSQL passent en lecture seule ou plantent ; systemd relance les services en boucle faute de pouvoir créer un pid ou un socket. Les tâches cron échouent silencieusement quand elles ne peuvent plus appendre à leur fichier de log.
La première étape, c’est toujours de regarder les points de montage — pas de deviner quel dossier « paraît gros ». Un volume /data séparé peut encore avoir de la marge tandis que la racine est bloquée ; sans df -hT, on envoie facilement l’équipe dans la mauvaise direction :
# Espace libre par partition df -hT # Plus gros répertoires au premier niveau sous / (peut être lent) sudo du -xh --max-depth=1 / 2>/dev/null | sort -hr | head -20 # Épuisement des inodes ressemble aussi à un « disque plein » df -ih
du peut prendre longtemps sur des hôtes avec des millions de fichiers. Si la machine rame déjà, ciblez les suspects : sudo du -sh /var/lib/docker /var/log /var/cache. Le manuel du(1) détaille -x (rester sur un seul système de fichiers) et --apparent-size — ne vous laissez pas tromper par les totaux quand des bind mounts ou volumes séparés entrent en jeu.
Gros consommateurs courants : tableau de référence
Chaque stack diffère, mais les chemins ci-dessous reviennent constamment sur les VPS Ubuntu et Debian. Web + Docker + CI sur un seul disque de 40 Go, c’est une recette familière : le journal et les couches conteneur grossissent en arrière-plan pendant que les logs applicatifs explosent aux pics de trafic. Le tableau indique si un script peut les vider en toute sécurité — les lignes « prudence » exigent de vérifier les dépendances avant toute suppression.
| Chemin / source | Taille typique | Auto-sûr ? | Notes |
|---|---|---|---|
/var/lib/docker |
Plusieurs Go à dizaines de Go | Partiel | Images orphelines, conteneurs arrêtés, volumes inutilisés ; ne pas toucher aux data volumes des conteneurs actifs |
/var/log/journal |
1–10+ Go | Oui | Le journal systemd gonfle vite sans plafond de taille |
/var/log/*.log |
Centaines de Mo à Go | Prudence | Truncate ou logrotate ; supprimer le fichier peut laisser des fd ouverts et des inodes fantômes |
/var/cache/apt |
Centaines de Mo à plusieurs Go | Oui | apt-get clean est sûr — ne retire que les .deb téléchargés |
/tmp, /var/tmp |
Variable | Partiel | Supprimer les fichiers non accédés depuis 7 jours ; attention aux uploads temporaires d’applications |
Nginx proxy_cache |
Plusieurs Go | Oui | Le cache se reconstruit ; si vous avez suivi notre guide d’optimisation du proxy inverse Nginx, attendez-vous à des MISS temporaires |
Anciens noyaux sous /boot |
Centaines de Mo | Oui | apt autoremove --purge retire les anciens kernels ; garder l’actuel et le précédent |
| Core dumps, rapports de crash | Variable | Oui | /var/crash, fichiers core.* applicatifs |
/home, /var/lib/mysql, des montages object storage ou des répertoires de backup dominent le classement, le nettoyage de cache ne suffira pas — il faut archiver, agrandir le disque ou migrer les données froides. Les scripts récupèrent de l’espace régénérable ; ils ne remplacent pas la planification de capacité.
Script de nettoyage en un clic (prêt à copier)
Le script ci-dessous est en mode dry-run par défaut : il affiche l’espace qui serait libéré et les commandes qui seraient exécutées. Passez --apply pour supprimer réellement. Dry-run d’abord, relecture humaine ensuite — nous avons déjà vu une équipe sauter cette étape, prune une image base vieille d’une semaine qui était la dernière copie d’un service retiré, et reconstruire un pipeline de déploiement pendant une heure. Les modules sont séparés pour commenter ce dont vous n’avez pas besoin (ignorez Docker si non installé, ou le cache Nginx sans proxy_cache).
#!/usr/bin/env bash # Nettoyage disque serveur en un clic — cache régénérable uniquement # Usage: sudo ./disk-cleanup.sh # aperçu # sudo ./disk-cleanup.sh --apply # exécution 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. cache apt if command -v apt-get >/dev/null; then run "apt-get clean -y" run "apt-get autoremove -y --purge" fi # 2. journal systemd — conserver 7 jours ou 500 Mo if command -v journalctl >/dev/null; then run "journalctl --vacuum-time=7d" run "journalctl --vacuum-size=500M" fi # 3. ressources Docker inutilisées (hors données métier dans volumes nommés) 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. répertoires temporaires — non accédés depuis 7 jours 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 (adapter cache_path au vôtre) 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. vidages crash [[ -d /var/crash ]] && run "rm -rf /var/crash/*" # 7. cache pip / npm (si présent) [[ -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 terminé. Quand vous êtes prêt : sudo $0 --apply"
Permissions : sudo chmod 750 /usr/local/sbin/disk-cleanup.sh. Au premier lancement, sans --apply, conservez la sortie DRY-RUN. Si la section Docker annonce de grosses suppressions d’images, vérifiez que vous ne retirez pas la seule copie locale d’une image dont le tag registry a déjà été supprimé.
docker system prune -a supprime toutes les images inutilisées, forçant un re-pull complet au prochain déploiement — pénible sur un VPS à bande passante limitée. Plus prudent : --filter until=168h pour ne retirer que les couches inutilisées depuis une semaine, ou docker image prune -f régulièrement pour les images dangling. Voir la documentation officielle Docker sur le pruning.
Module par module : ce qui part, ce qui reste
journalctl : --vacuum-time et --vacuum-size se combinent ; systemd rogne les journaux binaires selon la documentation journalctl. Pour un plafond permanent, définissez SystemMaxUse=500M dans /etc/systemd/journald.conf, puis systemctl restart systemd-journald.
Fichiers de log : pour un gros log encore ouvert par un processus, truncate -s 0 /var/log/nginx/access.log est plus sûr que rm — le processus garde son fd, et supprimer le fichier ne libère l’espace qu’au redémarrage ou à la réouverture. À long terme, configurez logrotate pour que Nginx et les applications tournent leurs logs quotidiennement en conservant N copies.
apt : apt-get clean ne touche qu’à /var/cache/apt/archives ; autoremove --purge retire anciens kernels et dépendances orphelines. Après une mise à jour de kernel, gardez l’actuel et le précédent pour pouvoir revenir en arrière si le nouveau ne boote pas. En production, nous comparons uname -r à dpkg -l 'linux-image-*' avant autoremove.
Ne pas toucher aux données métier : /var/lib/mysql, /var/lib/postgresql, fichiers RDB Redis, répertoires d’upload utilisateur et points de montage backup sont hors périmètre. Quand du les place en tête, il faut archiver, agrandir le disque ou migrer vers du stockage froid — pas nettoyer le cache.
Après le nettoyage : empêcher le disque de se remplir en silence
Passer la racine de 98 % à 60 % n’est que le début. Sans garde-fous, les mêmes répertoires regrossissent — journal à 8 Go en six semaines, couches Docker après chaque déploiement, pic de trafic qui rejoue l’incident. Notre minimum préventif sur petit VPS, en trois points :
- Seuils d’alerte : console cloud ou Prometheus self-hosted à 80 %, SMS ou pager à 85 % — n’attendez pas 95 %.
- Cron hebdomadaire :
0 4 * * 0 root /usr/local/sbin/disk-cleanup.sh --apply >> /var/log/disk-cleanup.log 2>&1 - Plafonds journald + logrotate : transformer la croissance illimitée en plafond supportable.
Sur une stack Docker Compose, fixez max-size et max-file sur le logging json-file — sinon le stdout d’un seul conteneur peut saturer le disque aussi sûrement que des couches d’images orphelines, et ce piège est trop souvent ignoré.
Checklist avant intervention
Recopiez ces points dans votre runbook et suivez-les dans l’ordre à la prochaine alerte :
df -hTetdf -ih— espace ou épuisement des inodes ?du -shciblé sur docker, journal, logs et cache- Dry-run du script → relecture humaine →
--apply - Vérification
df -haprès nettoyage, puis contrôle des services (DB, Nginx, Docker) - Ajouter limites journald, logrotate, cron et alertes à 80 %
Un disque plein, ce n’est jamais « supprimer quelques fichiers » — c’est un bilan capacité et politique de logs. Le script vous fait passer du rm paniqué à 3 h du matin à une procédure standard reproductible. Documentez ce que vous avez vidé et le delta df ; ce post-mortem d’une ligne révèle souvent un stanza logrotate manquant. Le reste, c’est la prévention pour que le pager se taise — et que le prochain astreinte hérite d’un playbook, pas d’un mystère.
Les builds mangent votre disque ? Séparez les rôles
Si votre VPS fait tourner la prod et des xcodebuild locaux, des builds Docker multi-couches ou des caches de modèles Ollama, un petit disque disparaît vite pendant une semaine de compilation — DerivedData et couches d’images en concurrence avec les logs de prod sur le même SSD.
Une répartition plus saine : garder le VPS de prod léger et déplacer les builds lourds ailleurs — Mac cloud pour packaging iOS, runner CI dédié, la prod ne tirant que les artefacts finaux.
Le Mac mini M4, avec sa mémoire unifiée et sa faible consommation au repos, convient bien comme nœud « build only, pas de trafic 24 h/24 ». Couplé à un VPS Linux pour le runtime, les caches de compilation ne se battent plus avec les logs de prod pour le même disque. Si vous planifiez cette séparation, déplacez d’abord le lourd vers un Mac cloud — découvrir les offres VPSSpark et passer moins de nuits à 3 h devant df -h.