- Опубликовано: 9 июл 2026
- 22
Диск закончился внезапно: куда деваются гигабайты на VPS
Часть 5 из 14 · Серия «Сервер в бою»
Ещё в первой статье серии я назвал забитый диск причиной трети ночных аварий, а в четвёртой мы настроили алерт на 90% — и, возможно, он у вас уже сработал. Знакомая мысль в этот момент: «Как 90%?! Я же ничего не загружал!» Сайт весит два гигабайта, база — ещё три, диск — сорок. Где остальные тридцать пять?
Спойлер: их съел сам сервер. VPS — это организм с обменом веществ: он непрерывно производит логи, кэши, временные файлы и копии самого себя. Если продукты жизнедеятельности не убирать, они заполнят всё доступное пространство — вопрос только времени. Сегодня проведём полную ревизию: научимся быстро находить, куда делось место, разберём всех типовых пожирателей по одному, познакомимся с двумя коварными спецслучаями (inodes и файлы-призраки) — и настроим уборку так, чтобы слово «внезапно» из заголовка больше к вам не относилось.
Почему полный диск ломает всё «странно»
Короткое напоминание, почему это вообще авария, а не неудобство. Когда на диске 0 байт, отказывает не «загрузка файлов» — отказывает вся запись: MySQL не может писать в таблицы и журналы и переходит в защитный режим или падает; PHP не может создать файл сессии — пользователей разлогинивает; certbot не может сохранить обновлённый сертификат; и — вишенка — логи тоже не пишутся, то есть авария прячет собственные улики. Отсюда фирменный почерк полного диска: ломается всё сразу, странно и в разных местах. Если симптомы выглядят бессвязно — df -h должна быть первой командой, а не десятой.
Шаг первый: находим виновника за две минуты
Начинаем сверху и спускаемся, как по карте. Общая картина:
df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 40G 38G 0G 100% /
Диагноз подтверждён. Теперь — кто именно. Классический способ: спускаться по дереву каталогов, на каждом шаге спрашивая «кто тут самый толстый»:
du -h --max-depth=1 / 2>/dev/null | sort -rh | head
Увидели, что раздулся /var — повторяем для него, потом для /var/log, и так до конкретного файла. Работает, но муторно. Способ поприятнее — ncdu, интерактивный анализатор:
apt install ncdu
ncdu -x /
Он просканирует диск и покажет дерево, отсортированное по размеру, по которому вы ходите стрелками, проваливаясь в самые тяжёлые ветки. Флаг -x важен: не выходить за пределы файловой системы (чтобы виртуальные /proc и смонтированные диски не путали картину). Две минуты — и виновник найден. В 9 случаях из 10 это будет кто-то из следующего списка.
Галерея пожирателей: типовые подозреваемые
Подозреваемый №1: логи. Мы уже знаем: сервер разговорчив, и за разговорчивость платят гигабайтами. Быстрая проверка обоих миров логов:
du -sh /var/log/* 2>/dev/null | sort -rh | head
journalctl --disk-usage
Частые находки: access.log нагруженного сайта на десятки гигабайт (особенно если его долбят боты — привет, статья про fail2ban), journald, разросшийся до 4 ГБ (его лимит по умолчанию — до 10% диска), и какой-нибудь error.log приложения, куда со скоростью пулемёта сыплется одна и та же нотация, которую все привыкли игнорировать. Журналу срочную помощь оказываем сразу:
journalctl --vacuum-size=500M
Подозреваемый №2: Docker. Если на сервере есть Docker — почти наверняка часть диска у него, и немалая. Docker ничего не удаляет сам: старые образы после каждого обновления, остановленные контейнеры, «висячие» слои от пересборок, кэш сборки, тома от давно удалённых проектов. Сводка:
docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 24 3 11.2GB 9.4GB (84%)
Containers 5 3 820MB 790MB
Local Volumes 12 2 6.1GB 4.8GB (78%)
Build Cache 117 0 5.5GB 5.5GB
Двадцать гигабайт, из которых нужны три, — обычное дело. Уборка:
docker system prune -a
С одной оговоркой на всю жизнь: prune с флагом --volumes удаляет и неиспользуемые тома — а в томах живут данные, в том числе базы остановленных проектов. Прежде чем добавлять --volumes, посмотрите docker volume ls и убедитесь, что там нет ничего живого. И отдельная тихая дыра — логи самих контейнеров: по умолчанию Docker пишет stdout каждого контейнера в json-файл без ограничения размера. Проверьте du -sh /var/lib/docker/containers/*/*-json.log — и если там гигабайты, пропишите лимит в /etc/docker/daemon.json:
{ "log-driver": "json-file", "log-opts": { "max-size": "20m", "max-file": "3" } }
Подозреваемый №3: старые ядра и кэш apt. Каждое обновление ядра оставляет предыдущее «на всякий случай», по 200–400 МБ штука, плюс кэш скачанных пакетов. За год набегает пара гигабайт. Лечится штатно и безопасно:
apt autoremove --purge
apt clean
(Про то, почему обновления вообще накапливаются и как обновляться без страха, — следующая, шестая статья.)
Подозреваемый №4: вы сами. Самый обидный пункт галереи. Дамп базы «на минутку» перед рискованной операцией — dump_final_2(1).sql на 4 ГБ в домашнем каталоге. Архив backup_old.tar.gz, который переехал на новый сервер вместе со старым сервером. Каталог site_backup_before_redesign/. Найдём всё крупное и забытое одним махом:
find / -xdev -type f -size +500M -exec ls -lh {} \; 2>/dev/null
Каждый найденный файл — маленькое свидание с собой прошлым. И заодно повод напомнить: бэкапы должны жить не на том же сервере — подробно об этом мы уже писали. Дамп на своём же диске — это не бэкап, это пожиратель места с ложным чувством безопасности в комплекте.
Два спецслучая, о которых не расскажет df -h
Спецслучай первый: место есть, а «No space left on device». Файловая система хранит не только байты, но и inodes — записи о самих файлах, — и их количество конечно. Миллионы мелких файлов (кэш сессий PHP, который никто не чистит; очередь писем; node_modules в пяти копиях) способны исчерпать inodes при полупустом диске. Проверка:
df -i
Если IUse% — 100%, а df -h показывает свободное место, — вот ваша разгадка. Найти рассадник мелочи: du --inodes --max-depth=1 /var | sort -rn | head — и дальше вниз по дереву, как обычно.
Спецслучай второй: файл удалили, а место не вернулось. Классика: нашли лог на 15 ГБ, сделали rm, а df -h не изменился. Почему: в Linux файл живёт, пока его держит хоть один процесс. Вы удалили имя, но nginx продолжает писать в открытый файл-призрак — место занято, а найти его теперь нельзя даже через ncdu. Проверка:
lsof +L1 | grep deleted
Лечение — перезапустить (или послать сигнал переоткрыть логи) процессу-владельцу: systemctl reload nginx. И правило на будущее: растущие логи не удаляют, а обнуляют — truncate -s 0 файл.log освобождает место мгновенно и без призраков. А ещё лучше — чтобы их обрезал не человек по ночам, а автоматика по расписанию. Переходим к главному.
Logrotate: уборка по расписанию
Всё, что мы делали выше, — разовая уборка. Повторится ли «внезапно» — зависит от того, налажена ли регулярная. За ротацию текстовых логов в Debian/Ubuntu отвечает logrotate: он уже установлен, уже крутится ежедневно и уже обслуживает системные логи и nginx — конфиги лежат в /etc/logrotate.d/. Проблема обычно не в его отсутствии, а в двух вещах: настройки по умолчанию слишком щедрые, и ваши собственные логи (приложения, скриптов, cron-задач) им не охвачены вообще.
Разберём конфиг на примере — заведём ротацию для логов гипотетического приложения, /etc/logrotate.d/myapp:
/var/www/myapp/storage/logs/*.log {
daily # проверять ежедневно
rotate 14 # хранить 14 архивов, старше — удалять
maxsize 100M # или ротировать раньше, если файл раздулся
compress # архивы сжимать (текстовые логи жмутся в ~10 раз)
delaycompress # кроме самого свежего — вдруг он ещё дописывается
missingok # нет файла — не ошибка
notifempty # пустой — не трогать
copytruncate # скопировать и обнулить на месте (см. ниже)
}
Про copytruncate — важная развилка, прямо связанная с файлами-призраками выше. Стандартная схема ротации — переименовать файл и попросить сервис открыть новый (postrotate со systemctl reload ...). Она аккуратнее, но требует, чтобы приложение умело переоткрывать лог. Если не умеет (а самописные скрипты и многие приложения — не умеют), оно продолжит писать в переименованный файл вечно. copytruncate решает это в лоб: копирует содержимое и обнуляет оригинал на месте — приложение ничего не замечает. Цена — теоретическая потеря пары строк в момент копирования; для логов приложения это приемлемо.
Проверить конфиг, не дожидаясь ночи:
logrotate -d /etc/logrotate.d/myapp # репетиция, без изменений
logrotate -f /etc/logrotate.d/myapp # принудительная ротация
И вторая половина уборки — журнал systemd, который logrotate не трогает. Ставим ему постоянный потолок в /etc/systemd/journald.conf:
SystemMaxUse=500M
и systemctl restart systemd-journald. Всё, оба мира логов под контролем.
Чек-лист: диск под контролем
| Что | Разовая уборка | Постоянная профилактика |
|---|---|---|
| Текстовые логи | truncate -s 0 жирным |
logrotate с maxsize на все свои логи |
| Журнал systemd | journalctl --vacuum-size=500M |
SystemMaxUse=500M в journald.conf |
| Docker | docker system prune -a |
лимит логов в daemon.json + prune по расписанию |
| Ядра и apt | apt autoremove --purge && apt clean |
раз в месяц вместе с обновлениями |
| Забытые дампы | find / -xdev -size +500M |
бэкапы — на другой сервер, автоматически |
| Ранняя тревога | — | алерт Netdata на 85–90% (статья 4) |
Пункты «по расписанию» из правой колонки напрашиваются на автоматизацию — и мы её сделаем в двенадцатой статье, когда будем разбирать cron и systemd-таймеры: одна задача в неделю на prune, одна на autoremove, и ручной уборки в вашей жизни больше нет.
Главная мысль на вынос даже не техническая. Диск не кончается внезапно — он кончается месяцами, просто без свидетелей. «Внезапность» — это разница между сервером, на который смотрят (алерт на 90%, ротация настроена, prune по расписанию), и сервером, на который смотрят только когда он уже лёг. Первый вариант дешевле во всех валютах, включая сон.
В следующей статье — вторая по популярности причина «ничего не трогал, само сломалось», только с обратным знаком: обновления. Разберёмся, почему apt upgrade пугает сильнее, чем должен, что такое unattended-upgrades, чем на проде опасен dist-upgrade — и когда переезд на новый релиз Ubuntu действительно нужен, а когда это чистый зуд перфекциониста.
- 1 Сервер упал в три ночи: разбор полётов без паники
- 2 Логи, которые говорят: учимся читать journalctl, nginx и syslog
- 3 Fail2ban и брутфорс: как перестать кормить ботов
- 4 Мониторинг для одного сервера: Uptime Kuma, Netdata и здравый смысл
- 5 Диск закончился внезапно: куда деваются гигабайты на VPS вы здесь
- 6 Обновления без страха: apt upgrade, который ничего не сломает скоро
- 7 Свой VPN на VPS за полчаса: WireGuard без магии скоро
- 8 Несколько сайтов на одном VPS: делим ресурсы без драки скоро
- 9 Тормозит сайт — виноват не хостинг: тюним nginx и PHP-FPM скоро
- 10 MySQL на маленьком VPS: почему база съедает всю память скоро
- 11 Кэширование на пальцах: Redis, FastCGI cache и когда хватит просто заголовков скоро
- 12 Cron, systemd-таймеры и скрипты: автоматизируем рутину сервера скоро
- 13 Деплой без FTP в 2026-м: git pull, хуки и простой CI/CD на своём VPS скоро
- 14 Переезд на другой VPS без даунтайма: чек-лист миграции скоро
Была статья полезной: