Disco lleno: encontrar y liberar espacio en un servidor Linux

Publicado el 15 min de lectura

Cuando df informa del 100% y du no encuentra nada: el camino completo, desde la medición hasta la liberación de espacio, para Debian 12 y 13 y para Ubuntu 22.04 y 24.04.

Un disco lleno rara vez avisa con educación. Lo normal es que primero falle un servicio: MariaDB escribe OS error 28 en el log, nginx responde con un 500, un backup se interrumpe con write error: No space left on device y, si la cosa se pone realmente fea, ya nadie entra por SSH en la máquina porque sshd no puede crear los archivos de sesión. Este artículo recorre el camino completo: medir, localizar, liberar y comprobar. Incluidos los dos casos en los que la mayoría de las guías se detiene, es decir, cuando df dice que está lleno y du no encuentra nada.

Primero medir: ¿qué partición está llena realmente?

Antes de borrar nada hay que tener claro qué sistema de archivos está afectado. Un /boot lleno tiene causas completamente distintas a las de un /var lleno.

df -h
df -hT -x tmpfs -x devtmpfs -x squashfs

La segunda línea oculta los pseudosistemas de archivos. En Ubuntu resulta especialmente útil, porque allí cada aplicación Snap instalada aparece como su propio montaje loop squashfs y ensucia la salida. Por cierto, esos montajes loop siempre están ocupados al 100%: es normal y no supone ningún problema.

Lo importante es la columna Mounted on. Constelaciones típicas en servidores:

Punto de montajeCausa habitual cuando se llena
/logs, Docker, caché de apt, datos de aplicaciones
/bootkernels antiguos y sus imágenes initramfs
/varjournal, rsyslog, cola de correo, bases de datos, Docker
/tmpsubidas interrumpidas, sesiones, restos de compilaciones

Otro detalle que suele generar confusión: ext4 reserva por defecto el cinco por ciento de la capacidad para el usuario root. Un servicio que se ejecuta como www-data o mysql ya recibe un No space left on device mientras que root, en ese mismo segundo, todavía puede escribir sin problemas. En particiones puras de datos (es decir, no en el sistema de archivos raíz) esa reserva se puede bajar sin riesgo:

tune2fs -m 1 /dev/sdb1

En / conviene mantener la reserva. Es justo el colchón que permite reparar un sistema que se ha llenado del todo.

Usar du correctamente, en vez de perderse por el camino

El punto de partida clásico consiste en ir nivel por nivel, siempre con -x:

du -xh --max-depth=1 / | sort -h
du -xh --max-depth=1 /var | sort -h

El -x es el parámetro más importante de todo el artículo. Mantiene a du dentro de un único sistema de archivos y evita que el comando se meta en /proc, /sys, recursos de red montados o discos de backup. Sin -x la búsqueda tarda minutos y devuelve cifras que no tienen nada que ver con la partición llena. sort -h ordena correctamente los tamaños legibles por humanos, de modo que el bloque más grande queda abajo del todo.

Quien prefiera navegar de forma interactiva puede instalar ncdu y arrancarlo también con -x:

apt-get install -y ncdu
ncdu -x /

Los archivos grandes sueltos se localizan más rápido de forma directa:

find /var -xdev -type f -size +100M -exec ls -lh {} +

Aquí hay dos trampas que llevan una y otra vez a conclusiones equivocadas. La primera: du cuenta bloques ocupados, no el tamaño lógico del archivo. En archivos dispersos (tablespaces de bases de datos, imágenes de discos virtuales) ambos valores se separan muchísimo. La comparación lo deja a la vista:

du -sh /var/log
du --apparent-size -sh /var/log

La segunda: du cuenta los enlaces duros una sola vez. Quien trabaje sin root recibe además líneas como du: cannot read directory '/var/lib/private': Permission denied y, con ello, sumas sistemáticamente demasiado pequeñas. Conviene hacer todos los análisis como root o con sudo.

Limitar el journal de systemd

El journal es el devorador silencioso de espacio más habitual en servidores. Por defecto puede ocupar el diez por ciento del sistema de archivos, con un tope de cuatro gigabytes, y además mantiene libre el 15% del sistema de archivos. En un disco de 500 gigabytes eso son hasta cuatro gigabytes de puro log.

journalctl --disk-usage

Aquí hay una diferencia real entre distribuciones que muchas guías se callan. Que el journal acabe o no en el disco depende, con el valor por defecto Storage=auto, únicamente de si existe el directorio /var/log/journal:

ls -d /var/log/journal
ls -d /run/log/journal

En instalaciones mínimas de Debian y en muchas imágenes cloud de Debian 12 y Debian 13 no existe /var/log/journal. El journal queda entonces en /run/log/journal, es decir, en RAM, desaparece con cada reinicio y no carga el disco en absoluto. A cambio, carga la memoria RAM. Ubuntu Server 22.04 y 24.04, por su parte, suelen crear el directorio y registran de forma permanente en disco. Comprobar en lugar de suponer.

Liberar espacio de inmediato:

journalctl --rotate
journalctl --vacuum-size=200M
journalctl --vacuum-time=7d

La llamada previa a --rotate no es ningún adorno: las opciones vacuum borran exclusivamente archivos de journal ya archivados, nunca el que está activo en ese momento. Si el archivo activo es el que ocupa la mayor parte, sin rotar antes parece que no ocurre nada, y ahí es donde tropiezan justamente los lectores que copian el comando de un mensaje de foro.

Para limitarlo de forma permanente conviene usar un archivo propio, así las futuras actualizaciones de paquetes no sobrescriben nada:

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

Comprobación de que realmente ha surtido efecto: journalctl --disk-usage debe indicar ahora un valor menor y df -h debe mostrar más espacio libre. Si el journal encoge mientras df se mantiene igual, algún proceso sigue manteniendo abiertos archivos ya borrados. Más sobre esto un poco más abajo.

Segunda diferencia entre distribuciones: en las instalaciones de servidor clásicas de Debian y Ubuntu suele ejecutarse además rsyslog, que escribe los mismos mensajes una segunda vez en /var/log/syslog. En las imágenes mínimas de Ubuntu (cloud, container) falta rsyslog. Se comprueba con ls -l /var/log/syslog. Si el archivo existe y es enorme, el problema no es el journal, sino una regla de logrotate ausente o rota bajo /etc/logrotate.d/.

La caché de apt y los kernels antiguos

Los paquetes descargados se quedan ahí después de la instalación. En un servidor que lleva mucho tiempo funcionando eso son enseguida varios gigabytes.

du -sh /var/cache/apt
apt-get clean
du -sh /var/lib/apt/lists

apt-get clean vacía por completo /var/cache/apt/archives, mientras que apt-get autoclean solo elimina los paquetes que ya no existen en los repositorios. Lo que clean no toca son las listas de paquetes bajo /var/lib/apt/lists. Con muchos repositorios añadidos pueden alcanzar varios cientos de megabytes y se pueden reconstruir sin riesgo:

rm -rf /var/lib/apt/lists/*
apt-get update

La segunda línea no es opcional, es obligatoria, y además de inmediato. Entre el borrado de las listas y el siguiente apt-get update, apt no conoce ni un solo paquete: cualquier apt-get install en ese estado aborta con E: Unable to locate package ... y código de salida 100, aunque el paquete esté por supuesto disponible en los repositorios.

El segundo clásico son los kernels antiguos. Ubuntu instala kernels nuevos continuamente a través de unattended-upgrades, pero solo limpia los viejos si se le permite expresamente. Cada kernel ocupa, junto con su initramfs, entre 100 y 150 megabytes en un /boot que a menudo solo tiene entre 512 megabytes y un gigabyte.

uname -r
dpkg -l 'linux-image-*'
apt-get autoremove --purge

El kernel en ejecución que devuelve uname -r nunca se elimina, ni tampoco el más reciente. En Ubuntu se previene el problema poniendo en /etc/apt/apt.conf.d/50unattended-upgrades la línea Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";. En Debian 12 y 13, unattended-upgrades no está activo por defecto: allí /boot solo crece si alguien actualiza a mano con regularidad y nunca limpia.

Cuando /boot ya está lleno y apt no llega a terminar

Este es el caso que de verdad duele. Mensajes típicos, literalmente:

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)

En ese momento la base de datos de paquetes está a medias y cualquier llamada posterior a apt falla en el mismo punto. La salida, en este orden:

  1. Anotar uname -r. Esa versión no se toca bajo ningún concepto.
  2. Sacar la salida de ls -lh /boot y localizar la versión más antigua que no esté en ejecución.
  3. Borrar únicamente su initrd.img-*, no el vmlinuz-*. El archivo initramfs es con diferencia el más grande y se puede regenerar en cualquier momento.
  4. Ejecutar apt-get -f install para que dpkg pueda terminar la configuración interrumpida.
  5. Solo después apt-get autoremove --purge, para que los paquetes antiguos desaparezcan limpiamente junto con su entrada en GRUB.
  6. Lanzar update-grub y leer la salida.

Lo que no conviene hacer: borrar con rm archivos de kernel de /boot al azar e ignorar el resto. dpkg sigue creyendo entonces que los paquetes están instalados, GRUB ofrece entradas que apuntan al vacío y el siguiente reinicio acaba en el prompt de rescate de GRUB. Quien ya haya borrado recupera la coherencia con apt-get install --reinstall del paquete afectado o con dpkg --purge, y después obligatoriamente update-grub.

Comprobación: df -h /boot vuelve a mostrar espacio libre, dpkg -l 'linux-image-*' lista ya solo dos o tres entradas con estado ii, y la salida de update-grub nombra exactamente los kernels que están realmente en /boot.

Docker, Snap y logs de contenedores

En hosts Docker la respuesta está casi siempre en /var/lib/docker. No hay que adivinar, hay que preguntar:

docker system df
docker system df -v

La variante detallada separa limpiamente imágenes, contenedores, volúmenes y caché de compilación. Después se limpia de forma dirigida:

docker image prune -a
docker builder prune
docker system prune -a

Una advertencia sobre --volumes: ese parámetro borra también volúmenes a los que no está enganchado ningún contenedor en ejecución. Quien tenga su base de datos en un volumen con nombre y acabe de parar el contenedor pierde así los datos. Sin un backup reciente, docker system prune -a --volumes no pinta nada en un servidor productivo.

La partida infravalorada son los logs de contenedores bajo /var/lib/docker/containers/*/*-json.log. El driver por defecto json-file no rota absolutamente nada mientras no se le indique de forma explícita. Un contenedor hablador escribe así, a lo largo de meses, decenas de gigabytes en un único archivo. El remedio va en /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "50m", "max-file": "3" }
}

Después, systemctl restart docker. Atención, y esto no aparece casi en ninguna guía: el ajuste solo vale para los contenedores creados de nuevo. Los existentes conservan su configuración antigua hasta que se vuelvan a crear una vez, con Compose por ejemplo mediante docker compose up -d --force-recreate.

Las versiones de Docker se diferencian bastante en este punto. Debian 12 trae docker.io 20.10, Debian 13 incluye 26.1, y Ubuntu 22.04 y 24.04 van ya por la 29.1. Cuanto más nueva es la versión, más espacio ocupa la caché de BuildKit, y más importante se vuelve docker builder prune, que docker system prune por sí solo no siempre vacía del todo.

En Ubuntu se suma Snap. Las revisiones antiguas se quedan desactivadas y siguen ocupando espacio:

snap list --all
snap set system refresh.retain=2

El valor 2 es el mínimo que acepta snapd, los valores menores se rechazan con un mensaje de error. Debian no lleva Snap, así que allí esta sección sobra por completo.

df dice que está lleno, du no encuentra nada

Ahora viene el caso interesante. df -h muestra el 100%, pero la suma de du da solo la mitad. Para eso existen exactamente cuatro causas plausibles.

Archivos borrados que siguen abiertos

Es con diferencia el motivo más frecuente. Alguien ha ejecutado rm /var/log/riesig.log mientras un servicio mantenía el archivo abierto. La entrada de directorio ha desaparecido, por eso du ya no ve nada. Los bloques siguen ocupados hasta que se cierra el último descriptor de archivo, por eso df los sigue viendo.

apt-get install -y lsof
lsof +L1

En la columna NLINK aparece entonces un 0 y detrás de la ruta pone (deleted). Si lsof no encuentra nada, no hay salida y el valor de retorno es 1, lo cual no es ningún error. Sin lsof también se puede ir directamente al sistema de archivos de procesos:

ls -l /proc/*/fd 2>/dev/null | grep deleted

Importante: hay que ejecutarlo obligatoriamente como root. Como usuario normal solo se ven los descriptores propios y así se pasan por alto justamente los servicios del sistema, que en nueve de cada diez casos son los causantes.

La forma limpia de liberar el espacio es reiniciar el servicio, por ejemplo con systemctl restart rsyslog. Si un reinicio no es viable, el archivo se puede truncar a longitud cero a través de su descriptor. El ID de proceso y el número de descriptor salen de la salida de lsof:

truncate -s 0 /proc/1234/fd/7

Eso libera los bloques al instante y el proceso sigue escribiendo después. Este método está pensado para archivos de log puros, abiertos en modo de anexado. Nunca se debe aplicar a archivos de bases de datos, a imágenes de máquinas virtuales ni a nada con acceso aleatorio, porque ahí provoca pérdida de datos.

Cómo se reconoce que ha funcionado: df -h muestra de inmediato más espacio libre y lsof +L1 ya no lista la entrada. Una segunda pasada de du, en cambio, no cambia en nada, porque allí el archivo ya era invisible antes. Justo por eso reiniciar el servidor parece resolver el problema por arte de magia.

Archivos ocultos bajo un punto de montaje

Un clásico: alguien ha escrito datos en /mnt/backup antes de que el disco real estuviera montado ahí. Los datos siguen en el sistema de archivos raíz, pero quedan tapados por el montaje que está encima. Solo se hacen visibles a través de una segunda vista del mismo sistema de archivos:

mkdir -p /mnt/rootview
mount --bind / /mnt/rootview
du -xh --max-depth=2 /mnt/rootview | sort -h
umount /mnt/rootview

El bind mount es inofensivo, no cambia nada de sitio y se retira de nuevo con umount.

La reserva de root y el fallo de permisos

El cinco por ciento de reserva de ext4 ya mencionado explica el hueco entre "todavía no está del todo lleno" y "los servicios ya están fallando". Y por último: quien lance du sin root no ve árboles de directorios enteros. La supuesta diferencia es entonces sencillamente un problema de permisos. En btrfs y ZFS se suman los snapshots como posible explicación, que du tampoco muestra nunca.

Cuando lo que se agota no es el espacio, sino los inodos

Existe un segundo tipo de "lleno" que genera exactamente el mismo mensaje de error. Cada archivo y cada directorio ocupa un inodo, y en ext4 su cantidad queda fijada en el momento del formateo.

df -i
stat -f /

Si en df -i la columna IUse% marca 100 mientras df -h informa de espacio libre de sobra, el diagnóstico es claro: el servidor no tiene un problema de espacio, sino de cantidad. Millones de archivos diminutos han consumido todos los inodos. Aun así el mensaje de error sigue siendo No space left on device, y justamente por eso la mayoría busca en el sitio equivocado.

A los causantes se les encuentra contando archivos en lugar de bytes:

find /var -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -20

Candidatos habituales son la cola de correo bajo /var/spool/postfix o /var/spool/exim4, las sesiones de PHP bajo /var/lib/php/sessions, los directorios de caché de aplicaciones web, las dependencias de Node desplegadas en disco y un /tmp que nunca se vacía.

Al borrar espera el siguiente obstáculo. rm /var/lib/php/sessions/* falla cuando hay muchísimos archivos y devuelve bash: /usr/bin/rm: Argument list too long, porque la línea de comandos supera el límite de tamaño. El camino que siempre funciona:

find /var/lib/php/sessions -type f -mtime +7 -delete

Lo que hay que saber: la cantidad de inodos de un sistema de archivos ext4 existente no se puede aumentar a posteriori. Solo queda agrandar el sistema de archivos (con lo que los inodos crecen de forma proporcional) o volver a crearlo con una densidad mayor, por ejemplo con mkfs.ext4 -i 8192 /dev/sdb1. Quien vaya a trabajar previsiblemente con muchísimos archivos pequeños, por ejemplo en servidores de correo o en archivos de imágenes, está mejor servido con XFS, porque XFS crea los inodos de forma dinámica y prácticamente solo se queda sin ellos cuando también se acaba el espacio. Debian y Ubuntu formatean por defecto con ext4, XFS hay que elegirlo de forma consciente.

Comprobar y conseguir que no vuelva a pasar

Una limpieza solo se puede dar por buena cuando encajan tres cosas: df -h muestra más espacio libre, df -i muestra una ocupación de inodos a la baja, y el servicio que falló al principio vuelve a funcionar. Un comando que ha terminado sin mensaje de error no es, por sí solo, ninguna prueba. Sobre todo journalctl --vacuum-size y docker system prune tienden a no hacer absolutamente nada sin decir ni una palabra.

Para el funcionamiento diario da buen resultado una lista corta: limitar el journal de forma estricta con SystemMaxUse, configurar la rotación de logs de Docker en daemon.json, activar en Ubuntu Remove-Unused-Kernel-Packages, recoger los logs de las aplicaciones propias mediante /etc/logrotate.d/ y montar un cron job sencillo que envíe un correo al superar un umbral. En servidores KVM y dedicados merece la pena, además, poner /var o al menos /var/log en una partición propia. Así, un log que se descontrola deja fuera de juego un servicio, pero no el sistema operativo, y en cualquier caso todavía se puede entrar por SSH en la máquina.

Preguntas frecuentes

¿Por qué df muestra el 100% cuando du encuentra bastante menos?
En la inmensa mayoría de los casos hay procesos que mantienen abiertos archivos ya borrados. La entrada de directorio ha desaparecido, por eso du ya no ve nada, pero los bloques siguen ocupados hasta que se cierra el último descriptor de archivo. Se localiza con "lsof +L1" como root y se libera reiniciando el servicio o con "truncate -s 0 /proc/PID/fd/N". Otras explicaciones son archivos ocultos bajo un punto de montaje, el cinco por ciento de reserva de root en ext4 y una pasada de du sin permisos de root.
¿Cuánto espacio puede ocupar el journal de systemd y cómo lo limito de forma permanente?
Por defecto el diez por ciento del sistema de archivos, con un tope de cuatro gigabytes, y journald mantiene además libre el 15% del sistema de archivos. El límite permanente se define en un archivo bajo /etc/systemd/journald.conf.d/ con SystemMaxUse y después se reinicia systemd-journald. Para un efecto inmediato, primero "journalctl --rotate" y luego "journalctl --vacuum-size=200M", porque vacuum solo elimina archivos de journal ya archivados.
¿El journal se guarda igual en Debian y en Ubuntu?
No. Con el valor por defecto Storage=auto lo único que decide es si existe /var/log/journal. En muchas instalaciones mínimas de Debian 12 y Debian 13 falta ese directorio: el journal queda entonces en RAM bajo /run/log/journal y desaparece al reiniciar. Ubuntu Server 22.04 y 24.04 suelen crear el directorio y registran en disco. Un "ls -d /var/log/journal" lo aclara en un segundo.
/boot está lleno y apt aborta con un error de dpkg. ¿Qué hago ahora?
Primero anota "uname -r", después busca en /boot la versión más antigua que no esté en ejecución y borra únicamente su initrd.img, no el archivo vmlinuz. Luego "apt-get -f install" para que dpkg termine la configuración interrumpida, a continuación "apt-get autoremove --purge" y al final "update-grub". Borrar archivos de kernel al azar deja entradas de GRUB que apuntan al vacío.
¿Es seguro "docker system prune -a --volumes"?
En un servidor productivo sin un backup reciente, no. El parámetro --volumes elimina también volúmenes a los que en ese momento no está enganchado ningún contenedor en ejecución, es decir, quizá la base de datos de un contenedor parado. Es más seguro diagnosticar con "docker system df -v" y actuar después de forma dirigida con "docker image prune -a" o "docker builder prune".
¿Qué hago si df -i muestra los inodos ocupados al 100%?
Entonces no falta espacio, sino cantidad de archivos posibles. A los causantes se les encuentra con "find /var -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -20", normalmente la cola de correo, las sesiones de PHP, cachés o /tmp. Se borra con find y -delete, porque rm con asterisco falla con "Argument list too long". La cantidad de inodos de un sistema de archivos ext4 no se puede aumentar a posteriori, solo agrandando o recreando el sistema de archivos, o bien usando XFS.

Linux Administración de servidores Debian Ubuntu Espacio de almacenamiento systemd Docker Resolución de problemas