Диск заполнен: как найти и освободить место на Linux-сервере

Опубликовано 13 мин. чтения

Если df показывает 100 процентов, а du ничего не находит: полный путь от измерения до освобождения места, для Debian 12 и 13, а также Ubuntu 22.04 и 24.04.

Заполненный накопитель редко предупреждает заранее. Обычно первым падает какой-нибудь сервис: MariaDB пишет в лог OS error 28, nginx отдаёт 500, резервное копирование обрывается с write error: No space left on device, а в самом неудачном случае на машину уже никто не заходит по SSH, потому что sshd не может создать файлы сессии. В этой статье разобран весь путь: измерить, найти, освободить, проверить. Включая те два случая, на которых большинство инструкций заканчивается, а именно когда df показывает переполнение, а du ничего не находит.

Сначала измерить: какой раздел вообще заполнен?

Прежде чем что-то удалять, нужно понять, какая файловая система пострадала. У заполненного /boot совсем другие причины, чем у заполненного /var.

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

Вторая строка скрывает псевдофайловые системы. На Ubuntu это особенно полезно, потому что там каждое установленное snap-приложение появляется как отдельная точка монтирования squashfs через loop и засоряет вывод. Кстати, такие loop-монтирования всегда заполнены на 100 процентов, это нормально и проблемой не является.

Важна колонка Mounted on. Типичные ситуации на серверах:

Точка монтированияОбычная причина переполнения
/логи, Docker, кэш apt, данные приложений
/bootстарые ядра и их образы initramfs
/varжурнал, rsyslog, почтовая очередь, базы данных, Docker
/tmpоборванные загрузки, сессии, остатки сборок

Ещё одна деталь, которая часто сбивает с толку: ext4 по умолчанию резервирует пять процентов ёмкости для пользователя root. Сервис, работающий от www-data или mysql, получит No space left on device уже в тот момент, когда root в ту же секунду ещё спокойно пишет на диск. На чисто файловых разделах (то есть не на корневой файловой системе) этот резерв можно безопасно уменьшить:

tune2fs -m 1 /dev/sdb1

На / резерв лучше оставить. Это ровно та подушка, за счёт которой переполненную систему вообще ещё можно починить.

Как пользоваться du и не заблудиться

Классический подход: спускаться по одному уровню за раз и всегда с -x:

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

-x — самый важный ключ во всей статье. Он удерживает du в пределах одной файловой системы и не даёт команде провалиться в /proc, /sys, смонтированные сетевые ресурсы или диски с резервными копиями. Без -x поиск идёт минутами и выдаёт цифры, которые к переполненному разделу отношения не имеют. sort -h правильно сортирует размеры в человекочитаемом виде, поэтому самый крупный кусок оказывается внизу.

Кому удобнее кликать интерактивно, тот ставит ncdu и запускает его тоже с -x:

apt-get install -y ncdu
ncdu -x /

Отдельные крупные файлы быстрее найти напрямую:

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

Здесь регулярно возникают два подводных камня, которые ведут к неверным выводам. Первый: du считает занятые блоки, а не логический размер файла. У разрежённых файлов (табличные пространства баз данных, образы виртуальных дисков) эти два значения расходятся очень сильно. Сравнение делает разницу видимой:

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

Второй: du считает жёсткие ссылки только один раз. Тот, кто работает не от root, дополнительно получает строки вида du: cannot read directory '/var/lib/private': Permission denied и вместе с ними систематически заниженные суммы. Поэтому весь анализ следует вести от root или через sudo.

Ограничить журнал systemd

Журнал — самый частый тихий пожиратель места на серверах. По умолчанию ему разрешено занимать десять процентов файловой системы с потолком в четыре гигабайта, и дополнительно он держит свободными 15 процентов файловой системы. На диске объёмом 500 гигабайт это до четырёх гигабайт чистого лога.

journalctl --disk-usage

Здесь есть реальное различие между дистрибутивами, о котором многие инструкции умалчивают. Попадёт ли журнал на диск вообще, при настройке по умолчанию Storage=auto зависит исключительно от того, существует ли каталог /var/log/journal:

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

На минимальных установках Debian и во многих облачных образах Debian 12 и Debian 13 каталога /var/log/journal нет. Журнал тогда лежит в /run/log/journal, то есть в RAM, исчезает при каждой перезагрузке и диск не нагружает вовсе. Зато он нагружает оперативную память. Ubuntu Server 22.04 и 24.04, наоборот, обычно создают этот каталог и пишут журнал на диск постоянно. Проверять, а не угадывать.

Освободить место немедленно:

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

Вызов --rotate перед этим не украшение: опции vacuum удаляют только уже заархивированные файлы журнала и никогда не трогают активный. Если основную часть объёма занимает как раз активный файл, то без предварительной ротации внешне не происходит ничего, и именно на этом спотыкаются читатели, копирующие команду из форумного поста.

Ограничить навсегда через отдельный файл, чтобы будущие обновления пакетов ничего не перезаписали:

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

Проверка, что это действительно подействовало: journalctl --disk-usage теперь обязан показывать меньшее значение, а df -h обязан показывать больше свободного места. Если размер журнала уменьшается, а df остаётся прежним, значит, какой-то процесс до сих пор держит открытыми уже удалённые файлы. Об этом ниже.

Второе различие между дистрибутивами: на классических серверных установках Debian и Ubuntu параллельно часто работает ещё и rsyslog, который пишет те же сообщения второй раз в /var/log/syslog. В минимальных образах Ubuntu (облако, контейнер) rsyslog, наоборот, отсутствует. Проверяется командой ls -l /var/log/syslog. Если файл существует и он огромный, то проблема не в журнале, а в отсутствующем или сломанном правиле logrotate в /etc/logrotate.d/.

Кэш apt и старые ядра

Скачанные пакеты остаются лежать на диске после установки. На долго работающем сервере это быстро превращается в несколько гигабайт.

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

apt-get clean полностью очищает /var/cache/apt/archives, а apt-get autoclean удаляет только те пакеты, которых больше нет в источниках. Чего clean не трогает, так это списки пакетов в /var/lib/apt/lists. При большом числе подключённых источников они дорастают до нескольких сотен мегабайт и безопасно пересоздаются:

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

Вторая строка здесь не необязательное дополнение, а обязанность, причём немедленная. Между удалением списков и следующим apt-get update apt не знает ни одного пакета: любой apt-get install в этом состоянии обрывается с E: Unable to locate package ... и кодом возврата 100, хотя пакет в источниках, разумеется, есть.

Вторая классика — старые ядра. Ubuntu через unattended-upgrades постоянно ставит новые ядра, но убирает старые только тогда, когда это явно разрешено. Каждое ядро вместе с initramfs занимает около 100-150 мегабайт в разделе /boot, размер которого часто составляет всего 512 мегабайт или один гигабайт.

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

Работающее ядро из uname -r при этом не удаляется никогда, как и самое новое. На Ubuntu подстраховываются тем, что прописывают в /etc/apt/apt.conf.d/50unattended-upgrades строку Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";. На Debian 12 и 13 unattended-upgrades по умолчанию не активен, там /boot растёт только в том случае, если кто-то регулярно обновляет систему вручную и никогда не прибирается.

Если /boot уже заполнен и apt больше не отрабатывает

Вот случай, который по-настоящему болезнен. Типичные сообщения дословно:

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)

Теперь база данных пакетов находится в наполовину готовом состоянии, и каждый следующий вызов apt спотыкается на том же самом месте. Выход, именно в такой последовательности:

  1. Записать вывод uname -r. Эту версию не трогают ни при каких обстоятельствах.
  2. Вывести ls -lh /boot и найти самую старую версию из тех, что сейчас не работают.
  3. Удалить только её initrd.img-*, но не vmlinuz-*. Файл initramfs с большим отрывом самый крупный и создаётся заново в любой момент.
  4. Выполнить apt-get -f install, чтобы dpkg смог завершить прерванную настройку.
  5. Только после этого apt-get autoremove --purge, чтобы старые пакеты исчезли чисто и вместе с записью в GRUB.
  6. Запустить update-grub и прочитать вывод.

Чего делать не стоит: удалять командой rm файлы ядер из /boot наугад, а остальное игнорировать. dpkg тогда по-прежнему считает пакеты установленными, GRUB предлагает пункты меню, которые ведут в пустоту, и следующая перезагрузка заканчивается в аварийной консоли GRUB. Кто уже успел удалить, возвращает согласованность через apt-get install --reinstall затронутого пакета или через dpkg --purge, после чего обязателен update-grub.

Проверка: df -h /boot снова показывает свободное место, dpkg -l 'linux-image-*' перечисляет всего две или три записи со статусом ii, а вывод update-grub называет ровно те ядра, которые действительно лежат в /boot.

Docker, Snap и логи контейнеров

На хостах с Docker ответ почти всегда лежит в /var/lib/docker. Не гадать, а спросить:

docker system df
docker system df -v

Подробный вариант аккуратно разделяет образы, контейнеры, тома и кэш сборки. После этого прибираться прицельно:

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

Слово об осторожности с ключом --volumes: он удаляет и те тома, к которым не привязан ни один запущенный контейнер. Кто держит базу данных в именованном томе и как раз остановил контейнер, потеряет вместе с этим данные. Без свежей резервной копии команде docker system prune -a --volumes на продуктивном сервере делать нечего.

Недооценённая статья расхода — логи контейнеров в /var/lib/docker/containers/*/*-json.log. Драйвер по умолчанию json-file без явного указания не ротирует их вообще. Разговорчивый контейнер за месяцы записывает так десятки гигабайт в один-единственный файл. Средство против этого в /etc/docker/daemon.json:

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

После этого systemctl restart docker. Внимание, и об этом почти нигде не пишут: настройка действует только на вновь создаваемые контейнеры. Существующие сохраняют свою старую конфигурацию до тех пор, пока их однажды не пересоздадут, в случае Compose через docker compose up -d --force-recreate.

Версии Docker здесь заметно различаются. Debian 12 поставляет docker.io 20.10, Debian 13 приносит 26.1, Ubuntu 22.04 и 24.04 дошли тем временем до 29.1. Чем новее версия, тем больше места занимает кэш BuildKit и тем важнее docker builder prune, который один только docker system prune не всегда очищает полностью.

На Ubuntu добавляется Snap. Старые ревизии остаются лежать отключёнными и продолжают занимать место:

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

Значение 2 — это минимум, который принимает snapd, меньшие значения отклоняются с сообщением об ошибке. В Debian никакого Snap нет, там этот раздел отпадает без замены.

df показывает переполнение, du ничего не находит

Теперь интересный случай. df -h показывает 100 процентов, а сумма по du даёт лишь половину. Для этого есть ровно четыре правдоподобные причины.

Удалённые, но всё ещё открытые файлы

Это с большим отрывом самая частая причина. Кто-то выполнил rm /var/log/riesig.log, пока сервис ещё держал файл открытым. Запись в каталоге исчезла, поэтому du больше ничего не видит. Блоки остаются занятыми до тех пор, пока не закрыт последний файловый дескриптор, поэтому df продолжает их учитывать.

apt-get install -y lsof
lsof +L1

В колонке NLINK тогда стоит 0, а после пути указано (deleted). Если lsof ничего не нашёл, вывода нет, а код возврата равен 1, и это не ошибка. Без lsof можно обойтись и напрямую через файловую систему процессов:

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

Важно: выполнять обязательно от root. Обычный пользователь видит только собственные дескрипторы и пропускает тем самым ровно те системные сервисы, которые в девяти случаях из десяти и оказываются виновниками.

Чистый способ освободить место — перезапустить сервис, например systemctl restart rsyslog. Если перезапуск невозможен, файл можно урезать до нулевой длины через его дескриптор. Идентификатор процесса и номер дескриптора берутся из вывода lsof:

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

Это немедленно освобождает блоки, процесс после этого продолжает писать. Метод предназначен исключительно для лог-файлов, открытых в режиме дозаписи. К файлам баз данных, образам виртуальных машин и вообще ко всему с произвольным доступом применять его нельзя никогда, там он приводит к потере данных.

По чему видно, что получилось: df -h сразу показывает больше свободного места, а lsof +L1 больше не выводит эту запись. Повторный прогон du, наоборот, не изменится совсем, ведь там файл и раньше был невидим. Именно в этом и причина того, что перезагрузка сервера будто бы волшебным образом решает проблему.

Файлы, скрытые под точкой монтирования

Классика: кто-то записал данные в /mnt/backup ещё до того, как туда был примонтирован настоящий диск. Данные так и лежат на корневой файловой системе, но закрыты сверху точкой монтирования. Увидеть это можно только через второй взгляд на ту же самую файловую систему:

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

Bind-монтирование безопасно, оно ничего не перевешивает и снимается обратно командой umount.

Резерв для root и ошибка прав доступа

Уже упомянутые пять процентов резерва ext4 объясняют разрыв между «ещё не совсем заполнено» и «сервисы уже падают». И наконец: кто запускает du не от root, целых деревьев каталогов не видит. Мнимая разница тогда попросту оказывается проблемой прав. На btrfs и ZFS дополнительным объяснением могут быть снимки, которые du тоже никогда не показывает.

Когда заканчивается не место, а inode

Существует второй вид «переполнения», который выдаёт ровно то же сообщение об ошибке. Каждый файл и каждый каталог занимает один inode, а их количество в ext4 задаётся раз и навсегда в момент форматирования.

df -i
stat -f /

Если в df -i колонка IUse% показывает 100, а df -h при этом сообщает о достатке свободного места, диагноз однозначен: у сервера проблема не с местом, а с количеством. Миллионы крошечных файлов израсходовали все inode. Сообщение об ошибке при этом всё равно No space left on device, и именно поэтому большинство ищет не там.

Виновников находят, считая файлы, а не байты:

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

Частые кандидаты: почтовая очередь в /var/spool/postfix или /var/spool/exim4, сессии PHP в /var/lib/php/sessions, каталоги кэша веб-приложений, вынесенные зависимости Node и никогда не очищаемый /tmp.

При самом удалении поджидает следующее препятствие. rm /var/lib/php/sessions/* при очень большом числе файлов падает с bash: /usr/bin/rm: Argument list too long, потому что командная строка выходит за предел по размеру. Способ, который работает всегда:

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

Что нужно знать: количество inode у существующей файловой системы ext4 задним числом увеличить нельзя. Помогает только увеличение самой файловой системы (при этом количество inode растёт пропорционально) либо её пересоздание с более плотным размещением, например через mkfs.ext4 -i 8192 /dev/sdb1. Кто заведомо работает с очень большим числом мелких файлов, например на почтовых серверах или в архивах изображений, тому лучше подойдёт XFS, потому что XFS выделяет inode динамически и практически заканчивается только тогда, когда заканчивается и место. Debian и Ubuntu по умолчанию форматируют в ext4, XFS нужно выбирать сознательно.

Проверить и сделать так, чтобы это не повторилось

Уборка считается успешной только тогда, когда сходятся три вещи: df -h показывает больше свободного места, df -i показывает снижение занятости inode, и сервис, который изначально упал, снова работает. Команда, отработавшая без сообщений об ошибке, сама по себе ещё не доказательство. Особенно journalctl --vacuum-size и docker system prune охотно и молча не делают ровным счётом ничего.

Для постоянной эксплуатации оправдывает себя короткий список: жёстко ограничить журнал через SystemMaxUse, задать ротацию логов Docker в daemon.json, на Ubuntu включить Remove-Unused-Kernel-Packages, охватить собственные логи приложений через /etc/logrotate.d/ и завести простое cron-задание, которое отправляет письмо при превышении порога. На KVM-серверах и выделенных серверах вдобавок имеет смысл вынести /var или хотя бы /var/log на отдельный раздел. Тогда разбушевавшийся лог парализует один сервис, но не операционную систему, и на машину в любом случае ещё можно будет зайти по SSH.

Частые вопросы

Почему df показывает 100 процентов, хотя du находит заметно меньше?
В подавляющем большинстве случаев процессы всё ещё держат открытыми уже удалённые файлы. Запись в каталоге исчезла, поэтому du ничего не видит, но блоки остаются занятыми до тех пор, пока не закрыт последний файловый дескриптор. Обнаружить это можно командой "lsof +L1" от root, освободить место перезапуском сервиса или командой "truncate -s 0 /proc/PID/fd/N". Другие объяснения: файлы, скрытые под точками монтирования, пять процентов резерва root в ext4 и запуск du без прав root.
Сколько места может занимать журнал systemd и как ограничить его навсегда?
По умолчанию десять процентов файловой системы с потолком в четыре гигабайта, дополнительно journald держит свободными 15 процентов файловой системы. Постоянное ограничение задают файлом в /etc/systemd/journald.conf.d/ через SystemMaxUse, после чего перезапускают systemd-journald. Для немедленного эффекта сначала "journalctl --rotate", а затем "journalctl --vacuum-size=200M", потому что vacuum удаляет только заархивированные файлы журнала.
Журнал в Debian и Ubuntu лежит одинаково?
Нет. При настройке по умолчанию Storage=auto решает исключительно то, существует ли каталог /var/log/journal. На многих минимальных установках Debian 12 и Debian 13 этого каталога нет, журнал тогда лежит в RAM в /run/log/journal и исчезает при перезагрузке. Ubuntu Server 22.04 и 24.04 обычно создают этот каталог и пишут журнал на диск. Команда "ls -d /var/log/journal" проясняет это за секунду.
/boot заполнен, а apt обрывается с ошибкой dpkg. Что делать?
Сначала записать вывод "uname -r", затем найти в /boot самую старую неработающую версию и удалить только её initrd.img, но не файл vmlinuz. После этого "apt-get -f install", чтобы dpkg завершил прерванную настройку, следом "apt-get autoremove --purge" и в конце "update-grub". Удаление файлов ядер наугад приводит к пунктам меню GRUB, которые ведут в пустоту.
Безопасна ли команда "docker system prune -a --volumes"?
На продуктивном сервере без свежей резервной копии нет. Ключ --volumes удаляет и те тома, к которым сейчас не привязан ни один запущенный контейнер, то есть при определённых обстоятельствах базу данных остановленного контейнера. Безопаснее путь через "docker system df -v" для диагностики, а затем прицельно "docker image prune -a" или "docker builder prune".
Что делать, если df -i показывает inode занятыми на 100 процентов?
Тогда не хватает не места, а количества возможных файлов. Виновников находят командой "find /var -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -20", обычно это почтовая очередь, сессии PHP, кэши или /tmp. Удаляют через find с -delete, потому что rm со звёздочкой падает с "Argument list too long". Количество inode у файловой системы ext4 задним числом увеличить нельзя, только увеличением или пересозданием файловой системы, как вариант можно перейти на XFS.

Linux Администрирование серверов Debian Ubuntu Дисковое пространство systemd Docker Диагностика неполадок