Disco cheio: encontrar e libertar espaço no servidor Linux

Publicado a 15 min de leitura

Quando o df indica 100% e o du não encontra nada: o caminho completo, da medição até à libertação de espaço, para Debian 12 e 13 e para Ubuntu 22.04 e 24.04.

Um disco cheio raramente avisa com delicadeza. Normalmente falha primeiro um serviço: o MariaDB escreve OS error 28 no log, o nginx responde com 500, um backup aborta com write error: No space left on device e, no pior dos casos, já ninguém consegue chegar à máquina por SSH, porque o sshd deixa de conseguir criar ficheiros de sessão. Este artigo mostra o caminho completo: medir, localizar, libertar, verificar. Incluindo os dois casos em que a maioria dos guias se fica pelo meio, ou seja, quando o df indica que está cheio e o du não encontra nada.

Medir primeiro: que partição está afinal cheia?

Antes de apagar seja o que for, é preciso saber que sistema de ficheiros está afetado. Um /boot cheio tem causas completamente diferentes de um /var cheio.

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

A segunda linha oculta os pseudossistemas de ficheiros. No Ubuntu isso é especialmente útil, porque ali cada aplicação Snap instalada aparece como um loop mount squashfs próprio e enche a saída de ruído. Já agora, esses loop mounts estão sempre ocupados a 100%, o que é normal e não representa problema nenhum.

A coluna importante é a Mounted on. Constelações típicas em servidores:

MountpointCausa habitual quando está cheio
/Logs, Docker, cache do apt, dados de aplicações
/bootkernels antigos e as respetivas imagens initramfs
/varJournal, rsyslog, fila de correio, bases de dados, Docker
/tmpuploads interrompidos, sessões, restos de builds

Mais um pormenor que causa confusão com frequência: por predefinição, o ext4 reserva cinco por cento da capacidade para o utilizador root. Um serviço que corra como www-data ou mysql já recebe um No space left on device enquanto o root, no mesmo segundo, ainda escreve sem qualquer problema. Em partições de dados puras (ou seja, fora do sistema de ficheiros raiz), essa reserva pode ser reduzida sem risco:

tune2fs -m 1 /dev/sdb1

Em / a reserva deve ficar como está. É precisamente essa almofada que permite reparar um sistema que encheu por completo.

Usar o du corretamente, em vez de se perder no caminho

A abordagem clássica é percorrer um nível de cada vez, sempre com -x:

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

O -x é a opção mais importante de todo o artigo. Mantém o du dentro de um único sistema de ficheiros e evita que o comando mergulhe em /proc, /sys, partilhas de rede montadas ou discos de backup. Sem o -x, a pesquisa demora minutos e devolve números que nada têm a ver com a partição cheia. O sort -h ordena corretamente os tamanhos legíveis por humanos, pelo que o maior bloco fica em baixo.

Quem preferir navegar de forma interativa instala o ncdu e inicia-o igualmente com -x:

apt-get install -y ncdu
ncdu -x /

Ficheiros grandes isolados encontram-se mais depressa de forma direta:

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

Há duas armadilhas que levam regularmente a conclusões erradas. Primeira: o du conta blocos ocupados, não o tamanho lógico do ficheiro. Em ficheiros esparsos (tablespaces de bases de dados, imagens de discos virtuais), os dois valores afastam-se bastante. A comparação torna isso visível:

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

Segunda: o du conta os hard links apenas uma vez. Quem trabalhar sem root recebe ainda linhas como du: cannot read directory '/var/lib/private': Permission denied e, com isso, somas sistematicamente demasiado baixas. Faça portanto todas as análises como root ou com sudo.

Limitar o journal do systemd

Em servidores, o journal é o devorador silencioso de armazenamento mais frequente. Por predefinição pode ocupar dez por cento do sistema de ficheiros, com um limite máximo de quatro gigabytes, e mantém ainda 15 por cento do sistema de ficheiros livres. Num disco de 500 gigabytes, isso dá até quatro gigabytes só de log.

journalctl --disk-usage

Aqui existe uma diferença real entre distribuições que muitos guias omitem. Com a predefinição Storage=auto, o facto de o journal chegar sequer ao disco depende apenas de o diretório /var/log/journal existir:

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

Em instalações mínimas de Debian e em muitas imagens cloud de Debian 12 e Debian 13, o /var/log/journal não existe. Nesse caso, o journal fica em /run/log/journal, ou seja, em RAM, desaparece a cada reinício e não pesa nada no disco. Em contrapartida, pesa na memória. Já o Ubuntu Server 22.04 e 24.04 costumam criar o diretório e registam permanentemente no disco. Verifique, em vez de adivinhar.

Para libertar espaço de imediato:

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

A chamada de --rotate antes disso não é decoração: as opções vacuum apagam exclusivamente ficheiros de journal já arquivados, nunca o que está ativo no momento. Se o ficheiro ativo representar a maior parte, sem rotação prévia parece não acontecer nada, e é exatamente aí que tropeçam os leitores que copiam o comando de um fórum.

Para limitar de forma duradoura, use um ficheiro próprio, para que atualizações de pacotes posteriores não sobrescrevam 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

Para confirmar que surtiu mesmo efeito: o journalctl --disk-usage tem agora de indicar um valor menor e o df -h tem de mostrar mais espaço livre. Se o tamanho do journal encolher enquanto o df se mantém inalterado, há um processo que ainda mantém abertos ficheiros já apagados. Voltamos a isso mais abaixo.

Segunda diferença entre distribuições: em instalações clássicas de servidor de Debian e Ubuntu corre muitas vezes o rsyslog em paralelo, que escreve as mesmas mensagens uma segunda vez para /var/log/syslog. Em imagens mínimas de Ubuntu (cloud, container), o rsyslog não está presente. Verifique com ls -l /var/log/syslog. Se o ficheiro existir e for enorme, o problema não é o journal, mas sim uma regra de logrotate em falta ou avariada em /etc/logrotate.d/.

Cache do apt e kernels antigos

Os pacotes descarregados ficam guardados depois da instalação. Num servidor que corre há muito tempo, isso chega rapidamente a vários gigabytes.

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

O apt-get clean esvazia por completo o /var/cache/apt/archives, o apt-get autoclean apenas os pacotes que já não existem nas fontes. O que o clean não toca são as listas de pacotes em /var/lib/apt/lists. Com muitas fontes ativas, essas listas podem chegar a várias centenas de megabytes e podem ser reconstruídas sem risco:

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

A segunda linha não é opcional, é obrigatória, e logo a seguir. Entre a eliminação das listas e o próximo apt-get update, o apt deixa de conhecer um único pacote: qualquer apt-get install nesse estado aborta com E: Unable to locate package ... e código de saída 100, embora o pacote esteja obviamente disponível nas fontes.

O segundo clássico são os kernels antigos. Através do unattended-upgrades, o Ubuntu instala continuamente kernels novos, mas só remove os antigos se isso for autorizado de forma explícita. Cada kernel ocupa, já com o initramfs, cerca de 100 a 150 megabytes num /boot que muitas vezes tem apenas 512 megabytes a um gigabyte.

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

O kernel em execução indicado por uname -r nunca é removido, tal como o mais recente de cada momento. No Ubuntu previne-se o problema colocando em /etc/apt/apt.conf.d/50unattended-upgrades a linha Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";. No Debian 12 e 13, o unattended-upgrades não está ativo por predefinição, pelo que ali o /boot só cresce se alguém atualizar regularmente à mão e nunca arrumar.

Quando o /boot já está cheio e o apt deixa de correr até ao fim

Este é o caso que dói mesmo a sério. Mensagens típicas, tal como aparecem:

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)

Agora a base de dados de pacotes está num estado meio concluído e qualquer chamada seguinte ao apt falha no mesmo ponto. A saída, por esta ordem:

  1. Anote o uname -r. Nesta versão não se toca em circunstância alguma.
  2. Execute ls -lh /boot e identifique a versão mais antiga que não esteja em execução.
  3. Apague apenas o respetivo initrd.img-*, não o vmlinuz-*. O ficheiro initramfs é de longe o maior e pode ser gerado de novo a qualquer momento.
  4. Execute apt-get -f install, para que o dpkg possa concluir a configuração interrompida.
  5. Só depois apt-get autoremove --purge, para que os pacotes antigos desapareçam de forma limpa, incluindo a respetiva entrada no GRUB.
  6. Corra update-grub e leia a saída.

O que não se deve fazer: apagar ficheiros de kernel do /boot ao calhas com rm e ignorar o resto. O dpkg continua a achar que os pacotes estão instalados, o GRUB oferece entradas que apontam para o vazio e o reinício seguinte acaba na prompt de recuperação do GRUB. Quem já tiver apagado recupera a consistência com apt-get install --reinstall do pacote afetado ou com dpkg --purge, seguido obrigatoriamente de update-grub.

Verificação: o df -h /boot volta a mostrar espaço livre, o dpkg -l 'linux-image-*' lista apenas duas ou três entradas com o estado ii e a saída do update-grub nomeia exatamente os kernels que estão mesmo em /boot.

Docker, Snap e logs de containers

Em hosts Docker, a resposta está quase sempre em /var/lib/docker. Não adivinhe, pergunte:

docker system df
docker system df -v

A variante detalhada separa de forma clara imagens, containers, volumes e cache de build. Depois disso, arrume de forma direcionada:

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

Uma palavra de cautela quanto ao --volumes: esta opção apaga também volumes aos quais não esteja ligado nenhum container em execução. Quem tenha a base de dados num volume nomeado e tenha acabado de parar o container perde assim os dados. Sem um backup atual, o docker system prune -a --volumes não tem lugar num servidor de produção.

A rubrica subestimada são os logs de containers em /var/lib/docker/containers/*/*-json.log. Sem uma indicação explícita, o driver predefinido json-file não roda absolutamente nada. Um container falador escreve assim, ao longo de meses, dezenas de gigabytes num único ficheiro. O antídoto fica em /etc/docker/daemon.json:

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

A seguir, systemctl restart docker. Atenção, e isto quase nenhum guia refere: a definição só vale para containers recém-criados. Os existentes mantêm a configuração antiga até serem criados de novo, no caso do Compose através de docker compose up -d --force-recreate.

As versões do Docker diferem bastante neste ponto. O Debian 12 traz docker.io 20.10, o Debian 13 traz 26.1 e o Ubuntu 22.04 e 24.04 estão entretanto na 29.1. Quanto mais recente for a versão, mais espaço ocupa a cache do BuildKit e mais importante se torna o docker builder prune, porque o docker system prune sozinho nem sempre a esvazia por completo.

No Ubuntu junta-se ainda o Snap. As revisões antigas ficam desativadas mas guardadas e continuam a ocupar espaço:

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

O valor 2 é o mínimo que o snapd aceita, valores menores são rejeitados com uma mensagem de erro. O Debian não traz Snap, pelo que ali esta secção simplesmente não se aplica.

O df diz que está cheio, o du não encontra nada

Agora o caso interessante. O df -h mostra 100%, mas a soma do du dá apenas metade. Para isso existem exatamente quatro causas plausíveis.

Ficheiros apagados mas ainda abertos

Esta é, de longe, a razão mais frequente. Alguém executou rm /var/log/riesig.log enquanto um serviço ainda mantinha o ficheiro aberto. A entrada de diretório desapareceu, por isso o du deixa de ver alguma coisa. Os blocos continuam ocupados até que o último descritor de ficheiro seja fechado, e por isso o df continua a vê-los.

apt-get install -y lsof
lsof +L1

Na coluna NLINK aparece então um 0 e, a seguir ao caminho, surge (deleted). Se o lsof não encontrar nada, não há saída nenhuma e o valor de retorno é 1, o que não é um erro. Sem o lsof também dá para o fazer diretamente através do sistema de ficheiros de processos:

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

Importante: execute obrigatoriamente como root. Enquanto utilizador normal só vê os seus próprios descritores e escapam-lhe precisamente os serviços de sistema que, em nove de cada dez casos, são os responsáveis.

A forma limpa de libertar o espaço é reiniciar o serviço, por exemplo com systemctl restart rsyslog. Se um reinício estiver fora de questão, o ficheiro pode ser truncado para comprimento zero através do seu descritor. O ID do processo e o número do descritor vêm da saída do lsof:

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

Isto liberta os blocos de imediato e o processo continua a escrever a seguir. Este método destina-se a ficheiros de log puros, abertos em modo de acréscimo. Nunca o aplique a ficheiros de bases de dados, a imagens de máquinas virtuais ou a qualquer outra coisa com acesso aleatório, porque aí provoca perda de dados.

Como se reconhece que resultou: o df -h mostra logo mais espaço livre e o lsof +L1 deixa de listar a entrada. Uma segunda passagem do du, essa, não muda absolutamente nada, porque ali o ficheiro já era invisível antes. É exatamente por isso que reiniciar o servidor parece resolver o problema por magia.

Ficheiros escondidos por baixo de um mountpoint

Um clássico: alguém escreveu dados em /mnt/backup antes de o disco propriamente dito estar ali montado. Os dados continuam no sistema de ficheiros raiz, mas ficam escondidos pelo mount que está por cima. Isso só se torna visível através de uma segunda vista do mesmo sistema de ficheiros:

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

O bind mount não tem perigo, não muda nenhuma montagem e é removido novamente com umount.

A reserva de root e o erro de permissões

Os cinco por cento de reserva do ext4 já mencionados explicam a distância entre "ainda não está totalmente cheio" e "os serviços já falham agora". E, por fim: quem inicia o du sem root não vê árvores de diretórios inteiras. A diferença aparente é então simplesmente um problema de permissões. Em btrfs e ZFS entram ainda em conta os snapshots como explicação, que o du também nunca mostra.

Quando o que acaba não é o espaço, mas os inodes

Existe um segundo tipo de "cheio" que produz exatamente a mesma mensagem de erro. Cada ficheiro e cada diretório ocupa um inode e, no ext4, o número deles está fixado desde o momento da formatação.

df -i
stat -f /

Se no df -i a coluna IUse% estiver a 100 enquanto o df -h indica bastante espaço livre, o diagnóstico é claro: o servidor não tem um problema de espaço, tem um problema de quantidade. Milhões de ficheiros minúsculos gastaram todos os inodes. Ainda assim, a mensagem de erro continua a ser No space left on device, e é precisamente por isso que a maioria procura no sítio errado.

Para encontrar os responsáveis, conte ficheiros em vez de bytes:

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

Candidatos frequentes são a fila de correio em /var/spool/postfix ou /var/spool/exim4, as sessões de PHP em /var/lib/php/sessions, diretórios de cache de aplicações web, árvores de dependências do Node espalhadas pelo disco e um /tmp que nunca é esvaziado.

Na própria eliminação espera o obstáculo seguinte. Com muitíssimos ficheiros, o rm /var/lib/php/sessions/* falha com bash: /usr/bin/rm: Argument list too long, porque a linha de comandos ultrapassa o limite de tamanho. O caminho que funciona sempre:

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

O que é preciso saber: o número de inodes de um sistema de ficheiros ext4 já existente não pode ser aumentado depois. Só ajuda aumentar o sistema de ficheiros (com isso os inodes crescem proporcionalmente) ou criá-lo de novo com uma densidade maior, por exemplo com mkfs.ext4 -i 8192 /dev/sdb1. Quem já saiba de antemão que vai trabalhar com muitíssimos ficheiros pequenos, como em servidores de correio ou arquivos de imagens, fica mais bem servido com XFS, porque o XFS cria inodes dinamicamente e, na prática, só se esgota quando o espaço também se esgota. O Debian e o Ubuntu formatam por predefinição com ext4, o XFS tem de ser escolhido de forma consciente.

Verificar e garantir que não volta a acontecer

Uma ação de limpeza só conta como bem-sucedida quando três coisas batem certo: o df -h mostra mais espaço livre, o df -i mostra uma ocupação de inodes a descer e o serviço que originalmente falhou volta a correr. Um comando que terminou sem mensagem de erro não é, por si só, uma prova. Sobretudo o journalctl --vacuum-size e o docker system prune gostam de não fazer rigorosamente nada, e sem dizer uma palavra.

Para a operação contínua, dá bom resultado uma lista curta: limitar o journal de forma rígida com SystemMaxUse, definir a rotação dos logs do Docker no daemon.json, ativar no Ubuntu o Remove-Unused-Kernel-Packages, abranger os logs das suas próprias aplicações através de /etc/logrotate.d/ e criar um cronjob simples que envie um e-mail quando um limite for ultrapassado. Em servidores KVM e em servidores dedicados vale ainda a pena colocar /var, ou pelo menos /var/log, numa partição própria. Assim, um descontrolo no log paralisa um serviço, mas não o sistema operativo, e continua sempre a ser possível chegar à máquina por SSH.

Perguntas frequentes

Porque é que o df mostra 100% quando o du encontra bastante menos?
Na esmagadora maioria dos casos há processos que ainda mantêm abertos ficheiros já apagados. A entrada de diretório desapareceu, por isso o du deixa de ver alguma coisa, mas os blocos continuam ocupados até que o último descritor de ficheiro seja fechado. Detete isso com "lsof +L1" como root e liberte o espaço com um reinício do serviço ou com "truncate -s 0 /proc/PID/fd/N". Outras explicações são ficheiros escondidos por baixo de mountpoints, os cinco por cento de reserva de root no ext4 e uma passagem do du sem permissões de root.
Quanto espaço pode o journal do systemd ocupar e como o limito de forma duradoura?
Por predefinição, dez por cento do sistema de ficheiros, com um limite máximo de quatro gigabytes; além disso, o journald mantém 15 por cento do sistema de ficheiros livres. Para o limitar de forma duradoura usa-se um ficheiro em /etc/systemd/journald.conf.d/ com SystemMaxUse e reinicia-se depois o systemd-journald. Para efeito imediato, primeiro "journalctl --rotate" e só depois "journalctl --vacuum-size=200M", porque o vacuum só remove ficheiros de journal já arquivados.
O journal fica no mesmo sítio em Debian e Ubuntu?
Não. Com a predefinição Storage=auto, o que decide é apenas a existência de /var/log/journal. Em muitas instalações mínimas de Debian 12 e Debian 13 esse diretório não existe, pelo que o journal fica em RAM, em /run/log/journal, e desaparece no reinício. O Ubuntu Server 22.04 e 24.04 costumam criar o diretório e registam no disco. Um "ls -d /var/log/journal" esclarece isso num segundo.
O /boot está cheio e o apt aborta com um erro do dpkg. E agora?
Anote primeiro o "uname -r", depois procure em /boot a versão mais antiga que não esteja em execução e apague apenas o respetivo initrd.img, não o ficheiro vmlinuz. A seguir "apt-get -f install", para que o dpkg conclua a configuração interrompida, depois "apt-get autoremove --purge" e, no fim, "update-grub". Apagar ficheiros de kernel ao calhas leva a entradas de GRUB que apontam para o vazio.
O "docker system prune -a --volumes" é seguro?
Num servidor de produção sem backup atual, não. A opção --volumes remove também volumes aos quais não esteja ligado nenhum container em execução, ou seja, eventualmente a base de dados de um container parado. Mais seguro é o caminho pelo "docker system df -v" para o diagnóstico e depois, de forma direcionada, "docker image prune -a" ou "docker builder prune".
O que fazer quando o df -i mostra os inodes ocupados a 100%?
Nesse caso não falta espaço, falta o número de ficheiros possíveis. Os responsáveis identificam-se com "find /var -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -20", tipicamente a fila de correio, as sessões de PHP, caches ou /tmp. A eliminação faz-se com find e -delete, porque o rm com asterisco falha em "Argument list too long". O número de inodes de um sistema de ficheiros ext4 não pode ser aumentado depois, apenas através do aumento ou da recriação do sistema de ficheiros, ou em alternativa recorrendo a XFS.

Linux Administração de servidores Debian Ubuntu Armazenamento systemd Docker Resolução de problemas