Disco pieno: trovare e liberare spazio su un server Linux
Quando df segnala il 100% e du non trova nulla: il percorso completo dalla misurazione alla liberazione dello spazio, per Debian 12 e 13 e Ubuntu 22.04 e 24.04.
Un disco pieno raramente si annuncia con garbo. Di solito il primo segnale è un servizio che cade: MariaDB scrive OS error 28 nel log, nginx risponde con un 500, un backup si interrompe con write error: No space left on device e, quando va davvero male, nessuno riesce più a entrare sulla macchina via SSH perché sshd non riesce più a creare i file di sessione. Questo articolo percorre la strada completa: misurare, trovare, liberare, verificare. Compresi i due casi in cui quasi tutte le guide si fermano, cioè quando df segnala pieno e du non trova nulla.
Prima misurare: quale partizione è davvero piena?
Prima di cancellare qualsiasi cosa deve essere chiaro quale filesystem è interessato. Un /boot pieno ha cause completamente diverse da un /var pieno.
df -h
df -hT -x tmpfs -x devtmpfs -x squashfs
La seconda riga esclude gli pseudo-filesystem. Su Ubuntu è particolarmente utile, perché lì ogni applicazione Snap installata compare come mount loop squashfs a sé stante e riempie l'output di righe inutili. Questi mount loop, tra l'altro, risultano sempre occupati al 100%: è normale e non è un problema.
La colonna che conta è Mounted on. Situazioni tipiche sui server:
| Mountpoint | Causa abituale quando è pieno |
|---|---|
| / | log, Docker, cache di apt, dati delle applicazioni |
| /boot | kernel vecchi e le relative immagini initramfs |
| /var | journal, rsyslog, coda di posta, database, Docker |
| /tmp | upload interrotti, sessioni, residui di build |
Un altro dettaglio che genera spesso confusione: ext4 riserva per impostazione predefinita il cinque per cento della capacità all'utente root. Un servizio che gira come www-data o mysql riceve quindi già un No space left on device, mentre root nello stesso istante può ancora scrivere senza problemi. Sulle partizioni di soli dati (quindi non sul filesystem radice) questa riserva si può abbassare senza rischi:
tune2fs -m 1 /dev/sdb1
Su / la riserva va invece lasciata dov'è. È esattamente il margine che ti permette di riparare un sistema che si è riempito.
Usare du nel modo giusto, invece di perdersi
L'approccio classico procede un livello alla volta, sempre con -x:
du -xh --max-depth=1 / | sort -h
du -xh --max-depth=1 /var | sort -h
L'opzione -x è la più importante di tutto l'articolo. Tiene du dentro un solo filesystem e impedisce che il comando finisca dentro /proc, /sys, condivisioni di rete montate o dischi di backup. Senza -x la ricerca dura minuti e restituisce numeri che con la partizione piena non c'entrano nulla. sort -h ordina correttamente le dimensioni in formato leggibile, così il blocco più grosso finisce in fondo.
Se preferisci muoverti in modo interattivo, installa ncdu e avvialo anch'esso con -x:
apt-get install -y ncdu
ncdu -x /
I singoli file di grandi dimensioni si trovano più in fretta per via diretta:
find /var -xdev -type f -size +100M -exec ls -lh {} +
Qui due trappole portano regolarmente a conclusioni sbagliate. La prima: du conta i blocchi occupati, non la dimensione logica del file. Con i file sparsi (tablespace di database, immagini di dischi virtuali) i due valori divergono parecchio. Il confronto lo rende visibile:
du -sh /var/log
du --apparent-size -sh /var/log
La seconda: du conta gli hard link una volta sola. Se lavori senza root ottieni in più righe come du: cannot read directory '/var/lib/private': Permission denied e quindi totali sistematicamente troppo bassi. Esegui perciò tutte le analisi come root oppure con sudo.
Limitare il journal di systemd
Sui server il journal è il divoratore silenzioso di spazio più frequente. Per impostazione predefinita può occupare il dieci per cento del filesystem, con un tetto di quattro gigabyte, e in più tiene libero il 15 per cento del filesystem. Su un disco da 500 gigabyte si arriva così a quattro gigabyte di solo log.
journalctl --disk-usage
Qui c'è una differenza concreta tra le distribuzioni, che molte guide passano sotto silenzio. Con l'impostazione predefinita Storage=auto, che il journal finisca o meno sul disco dipende soltanto dal fatto che la directory /var/log/journal esista:
ls -d /var/log/journal
ls -d /run/log/journal
Nelle installazioni minimali di Debian e in molte immagini cloud di Debian 12 e Debian 13 la directory /var/log/journal non esiste. Il journal sta allora sotto /run/log/journal, cioè in RAM, sparisce a ogni riavvio e non pesa affatto sul disco. Pesa invece sulla RAM. Ubuntu Server 22.04 e 24.04 creano di norma quella directory e scrivono i log in modo permanente su disco. Meglio verificare che tirare a indovinare.
Per liberare spazio subito:
journalctl --rotate
journalctl --vacuum-size=200M
journalctl --vacuum-time=7d
La chiamata a --rotate messa davanti non è un ornamento: le opzioni vacuum cancellano esclusivamente i file di journal già archiviati, mai quello attivo in quel momento. Se è proprio il file attivo a occupare la maggior parte dello spazio, senza rotazione preventiva sembra non succedere nulla, ed è esattamente qui che inciampa chi copia il comando da un post di forum.
Per il limite permanente conviene un file dedicato, così i futuri aggiornamenti dei pacchetti non sovrascrivono nulla:
mkdir -p /etc/systemd/journald.conf.d
printf '[Journal]\nSystemMaxUse=200M\nRuntimeMaxUse=50M\n' > /etc/systemd/journald.conf.d/00-size.conf
systemctl restart systemd-journald
La verifica che abbia davvero avuto effetto: journalctl --disk-usage deve ora indicare un valore più basso e df -h deve mostrare più spazio libero. Se la dimensione del journal cala mentre df resta identico, allora c'è un processo che tiene ancora aperti dei file cancellati. Più avanti ci torniamo.
Seconda differenza tra distribuzioni: sulle installazioni server classiche di Debian e Ubuntu gira spesso in parallelo anche rsyslog, che scrive gli stessi messaggi una seconda volta in /var/log/syslog. Nelle immagini Ubuntu minimali (cloud, container) rsyslog manca invece del tutto. Verifica con ls -l /var/log/syslog. Se il file esiste ed è enorme, il problema non è il journal, ma una regola logrotate mancante o rotta sotto /etc/logrotate.d/.
Cache di apt e kernel vecchi
I pacchetti scaricati restano sul disco anche dopo l'installazione. Su un server in funzione da tempo diventano in fretta diversi gigabyte.
du -sh /var/cache/apt
apt-get clean
du -sh /var/lib/apt/lists
apt-get clean svuota completamente /var/cache/apt/archives, apt-get autoclean rimuove solo i pacchetti che nei repository non esistono più. Quello che clean non tocca sono gli elenchi dei pacchetti sotto /var/lib/apt/lists. Con molti repository configurati possono arrivare a diverse centinaia di megabyte e si possono ricostruire senza rischi:
rm -rf /var/lib/apt/lists/*
apt-get update
La seconda riga non è un optional, è un obbligo, e va eseguita subito. Tra la cancellazione degli elenchi e il successivo apt-get update apt non conosce più nemmeno un pacchetto: in quello stato ogni apt-get install si interrompe con E: Unable to locate package ... e codice di uscita 100, anche se ovviamente il pacchetto nei repository c'è.
Il secondo classico sono i kernel vecchi. Ubuntu, tramite unattended-upgrades, installa di continuo kernel nuovi, ma rimuove quelli vecchi solo se glielo permetti esplicitamente. Ogni kernel occupa, initramfs compreso, circa da 100 a 150 megabyte in un /boot che spesso è grande soltanto da 512 megabyte a un gigabyte.
uname -r
dpkg -l 'linux-image-*'
apt-get autoremove --purge
Il kernel in esecuzione, quello indicato da uname -r, non viene mai rimosso, e nemmeno il più recente. Su Ubuntu ci si mette al riparo impostando in /etc/apt/apt.conf.d/50unattended-upgrades la riga Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";. Su Debian 12 e 13 unattended-upgrades non è attivo per impostazione predefinita: lì /boot cresce solo se qualcuno aggiorna regolarmente a mano e non fa mai pulizia.
Quando /boot è già pieno e apt non arriva più in fondo
Questo è il caso che fa davvero male. I messaggi tipici, parola per parola:
update-initramfs: failed for /boot/initrd.img-6.8.0-60-generic with 1.
dpkg: error processing package linux-image-6.8.0-60-generic (--configure):
installed linux-image-6.8.0-60-generic package post-installation script subprocess returned error exit status 1
E: Sub-process /usr/bin/dpkg returned an error code (1)
A questo punto il database dei pacchetti è in uno stato incompleto e ogni ulteriore invocazione di apt fallisce nello stesso punto. La via d'uscita, in quest'ordine:
- Annota l'output di
uname -r. Questa versione non si tocca in nessun caso. - Esegui
ls -lh /boote individua la versione più vecchia tra quelle non in esecuzione. - Cancella soltanto il suo
initrd.img-*, non ilvmlinuz-*. Il file initramfs è di gran lunga il più grande e si può rigenerare in qualsiasi momento. - Esegui
apt-get -f install, così dpkg può portare a termine la configurazione interrotta. - Solo dopo lancia
apt-get autoremove --purge, così i pacchetti vecchi spariscono in modo pulito, voce GRUB compresa. - Fai girare
update-grube leggi l'output.
Quello che invece non va fatto: cancellare a caso file di kernel da /boot con rm e ignorare il resto. dpkg continua a credere che i pacchetti siano installati, GRUB propone voci che puntano nel vuoto e il riavvio successivo finisce nel prompt di ripristino di GRUB. Se hai già cancellato, recuperi la coerenza con apt-get install --reinstall del pacchetto interessato oppure con dpkg --purge, e subito dopo update-grub è obbligatorio.
Verifica: df -h /boot mostra di nuovo spazio libero, dpkg -l 'linux-image-*' elenca ormai solo due o tre voci con stato ii e l'output di update-grub nomina esattamente i kernel che si trovano davvero in /boot.
Docker, Snap e log dei container
Sugli host Docker la risposta sta quasi sempre in /var/lib/docker. Non tirare a indovinare, chiedi:
docker system df
docker system df -v
La variante dettagliata separa in modo netto immagini, container, volumi e cache di build. Poi si fa pulizia in modo mirato:
docker image prune -a
docker builder prune
docker system prune -a
Una raccomandazione di prudenza su --volumes: questa opzione cancella anche i volumi a cui non è collegato nessun container in esecuzione. Se tieni il tuo database in un volume con nome e hai appena fermato il container, con quel comando perdi i dati. Senza un backup recente, docker system prune -a --volumes non ha posto su un server di produzione.
La voce sottovalutata sono i log dei container sotto /var/lib/docker/containers/*/*-json.log. Il driver predefinito json-file, senza un'indicazione esplicita, non ruota affatto. Un container loquace arriva così, nel giro di qualche mese, a scrivere decine di gigabyte in un unico file. Il rimedio va in /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "3" }
}
Poi systemctl restart docker. Attenzione, ed è un punto che quasi nessuna guida riporta: l'impostazione vale solo per i container creati da quel momento in poi. Quelli esistenti mantengono la vecchia configurazione finché non vengono ricreati, con Compose quindi tramite docker compose up -d --force-recreate.
Le versioni di Docker si differenziano parecchio su questo punto. Debian 12 fornisce docker.io 20.10, Debian 13 porta la 26.1, Ubuntu 22.04 e 24.04 sono ormai alla 29.1. Più recente è la versione, più spazio occupa la cache di BuildKit e più importante diventa docker builder prune, che docker system prune da solo non svuota sempre del tutto.
Su Ubuntu si aggiunge Snap. Le revisioni vecchie restano lì disattivate e continuano a occupare spazio:
snap list --all
snap set system refresh.retain=2
Il valore 2 è il minimo che snapd accetta, i valori più bassi vengono rifiutati con un messaggio di errore. Debian non ha Snap a bordo, quindi lì questa sezione decade senza sostituti.
df segnala pieno, du non trova nulla
Ed eccoci al caso interessante. df -h mostra il 100%, ma la somma calcolata da du arriva solo alla metà. Le cause plausibili sono esattamente quattro.
File cancellati, ma ancora aperti
Questa è di gran lunga la causa più frequente. Qualcuno ha eseguito rm /var/log/riesig.log mentre un servizio teneva ancora il file aperto. La voce nella directory è sparita, per questo du non vede più nulla. I blocchi restano occupati finché non viene chiuso l'ultimo descrittore di file, per questo df continua a vederli.
apt-get install -y lsof
lsof +L1
Nella colonna NLINK compare allora uno 0 e dopo il percorso trovi (deleted). Se lsof non trova nulla non stampa niente e restituisce il codice 1: non è un errore. Senza lsof si può passare direttamente dal filesystem dei processi:
ls -l /proc/*/fd 2>/dev/null | grep deleted
Importante: esegui il comando assolutamente come root. Da utente normale vedi solo i tuoi descrittori e ti sfuggono proprio i servizi di sistema che in nove casi su dieci sono i responsabili.
La strada pulita per liberare lo spazio è riavviare il servizio, per esempio con systemctl restart rsyslog. Se un riavvio non è praticabile, il file si può troncare a lunghezza zero passando dal suo descrittore. L'ID del processo e il numero del descrittore li prendi dall'output di lsof:
truncate -s 0 /proc/1234/fd/7
Così i blocchi si liberano subito e il processo continua a scrivere. Questo metodo è pensato per i soli file di log aperti in modalità append. Su file di database, immagini di macchine virtuali o qualunque altra cosa ad accesso casuale non va mai applicato, lì porta alla perdita di dati.
Come capire che ha funzionato: df -h mostra subito più spazio libero e lsof +L1 non elenca più quella voce. Una seconda esecuzione di du invece non cambia di una virgola, perché lì il file era già invisibile prima. È anche esattamente il motivo per cui un riavvio del server sembra risolvere il problema per magia.
File nascosti sotto un mountpoint
Il classico: qualcuno ha scritto dati in /mnt/backup prima che il disco vero e proprio fosse montato lì. I dati restano sul filesystem radice, ma il mount sovrapposto li nasconde. Si rendono visibili solo tramite una seconda vista dello stesso filesystem:
mkdir -p /mnt/rootview
mount --bind / /mnt/rootview
du -xh --max-depth=2 /mnt/rootview | sort -h
umount /mnt/rootview
Il bind mount non è pericoloso: non sposta nulla e si rimuove di nuovo con umount.
La riserva di root e l'errore di permessi
Il cinque per cento di riserva ext4 già citato spiega lo scarto tra "non ancora del tutto pieno" e "i servizi stanno già fallendo". E infine: chi avvia du senza root non vede interi rami di directory. La differenza apparente è allora semplicemente un problema di permessi. Su btrfs e ZFS entrano in gioco come spiegazione anche gli snapshot, che pure du non mostra mai.
Quando a finire non è lo spazio, ma gli inode
Esiste un secondo tipo di "pieno" che produce esattamente lo stesso messaggio di errore. Ogni file e ogni directory occupa un inode, e su ext4 il loro numero è fissato fin dal momento della formattazione.
df -i
stat -f /
Se in df -i la colonna IUse% è a 100 mentre df -h segnala spazio libero in abbondanza, la diagnosi è inequivocabile: il server non ha un problema di spazio, ma di quantità. Milioni di file minuscoli hanno consumato tutti gli inode. Il messaggio di errore resta comunque No space left on device, ed è proprio per questo che quasi tutti cercano nel posto sbagliato.
I responsabili si trovano contando i file invece dei byte:
find /var -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -20
Candidati frequenti sono la coda di posta sotto /var/spool/postfix o /var/spool/exim4, le sessioni PHP sotto /var/lib/php/sessions, le directory di cache delle applicazioni web, le dipendenze Node scaricate in locale e un /tmp mai svuotato.
Al momento della cancellazione ti aspetta l'ostacolo successivo. Con moltissimi file rm /var/lib/php/sessions/* fallisce con bash: /usr/bin/rm: Argument list too long, perché la riga di comando supera il limite di dimensione. La strada che funziona sempre:
find /var/lib/php/sessions -type f -mtime +7 -delete
Quello che devi sapere: il numero di inode di un filesystem ext4 esistente non si può aumentare a posteriori. Le uniche vie sono ingrandire il filesystem (gli inode crescono in proporzione) oppure ricrearlo con una densità maggiore, per esempio con mkfs.ext4 -i 8192 /dev/sdb1. Se prevedi di lavorare con moltissimi file piccoli, per esempio su server di posta o archivi di immagini, XFS è la scelta migliore, perché XFS crea gli inode in modo dinamico e in pratica li esaurisce solo quando finisce anche lo spazio. Debian e Ubuntu formattano per impostazione predefinita con ext4, XFS va scelto consapevolmente.
Verificare e fare in modo che non si ripeta
Una pulizia si può considerare riuscita solo quando tre cose coincidono: df -h mostra più spazio libero, df -i mostra un'occupazione di inode in calo e il servizio che era caduto all'inizio funziona di nuovo. Un comando terminato senza messaggi di errore, da solo, non dimostra ancora nulla. In particolare journalctl --vacuum-size e docker system prune tendono a non fare assolutamente nulla senza dire una parola.
Per l'esercizio quotidiano funziona bene una lista breve: limitare il journal in modo rigido con SystemMaxUse, impostare la rotazione dei log di Docker nel daemon.json, su Ubuntu attivare Remove-Unused-Kernel-Packages, far coprire i log delle proprie applicazioni da /etc/logrotate.d/ e predisporre un semplice cron job che mandi una mail al superamento di una soglia. Su server KVM e server dedicati conviene inoltre spostare /var, o almeno /var/log, su una partizione dedicata. Così un log impazzito mette fuori gioco un servizio, ma non il sistema operativo, e in ogni caso continui a poter entrare sulla macchina via SSH.
Domande frequenti
Perché df mostra il 100% mentre du trova molto meno?
Quanto spazio può occupare il journal di systemd e come lo limito in modo permanente?
Il journal si trova nello stesso posto su Debian e Ubuntu?
/boot è pieno e apt si interrompe con un errore di dpkg. E adesso?
"docker system prune -a --volumes" è sicuro?
Che cosa faccio se df -i mostra gli inode occupati al 100%?
2026 KernelHost GmbH. Tutti i diritti riservati. Questa guida è protetta dal diritto d'autore. La ripubblicazione su altri siti web, anche parziale o in forma modificata, non è consentita senza il nostro consenso scritto. Le citazioni con indicazione della fonte e un link sono le benvenute.

