13.4. Дисковое пространство и очистка
Цели
После этого материала вы сможете:
- объяснить, почему сумма размеров образов больше занятого места;
- найти расход, который
docker system dfне показывает вовсе; - определить, какой container пишет в свою файловую систему, и сколько;
- очистить каждую категорию отдельно, зная, что именно удаляется;
- назвать команду, удаляющую данные безвозвратно, и условия, при которых она это делает;
- настроить автоматические ограничения, чтобы очистка не требовалась.
Предварительные знания
- 3.2. Слои и copy-on-write;
- 7.2. Volumes;
- 13.1. Логи и logging drivers — расход на логи.
Ключевые термины
| Термин | Объяснение |
|---|---|
SHARED SIZE | Часть образа, общая с другими образами |
RECLAIMABLE | Оценка освобождаемого места |
writable layer | Слой container'а, куда идёт запись |
build cache | Кэш промежуточных результатов сборки |
dangling | Образ без тега, не являющийся родителем другого |
prune | Удаление неиспользуемых объектов |
Теория
Шесть категорий расхода
| Категория | Где лежит | Виден в system df |
|---|---|---|
| Образы | overlay2/ | Да |
| Слои container'ов | overlay2/ | Да (Containers) |
| Volumes | volumes/ | Да |
| Build cache | buildkit/ | Да |
| Логи container'ов | containers/*/*-json.log | Нет |
| Метаданные, tmp | containers/, tmp/ | Нет |
Пятая строка — главный практический вывод урока. docker system df не учитывает логи. При отсутствии ротации (урок 13.1) они становятся крупнейшей категорией, а отчёт показывает скромные числа.
Типичный разговор: «docker system df говорит 12 ГБ, а каталог занимает 58 ГБ». Разница — логи.
Почему сумма размеров образов обманывает
docker images
Колонка SIZE показывает полный размер образа, включая общие с другими образами слои. Три образа на базе python:3.13-slim покажут по 130 МБ каждый — но общего места займут около 150 МБ, а не 390.
docker system df -v
Здесь появляется SHARED SIZE — сколько из размера образа разделяется с другими. Реальный вклад образа = UNIQUE SIZE.
| Колонка | Смысл |
|---|---|
SIZE | Полный размер со всеми слоями |
SHARED SIZE | Часть, общая с другими образами |
UNIQUE SIZE | Освободится при удалении этого образа |
Планировать очистку нужно по UNIQUE SIZE: удаление образа с SHARED 120MB / UNIQUE 4MB освободит четыре мегабайта.
Что RECLAIMABLE считает освобождаемым
Для образов: те, на которые не ссылается ни один container — включая остановленные.
Отсюда частая неожиданность: RECLAIMABLE показывает ноль, хотя образов много. Причина — остановленные container'ы, удерживающие их. Сначала удаляют container'ы, затем образы.
Категории prune и что именно удаляется
| Команда | Удаляет | Опасность |
|---|---|---|
docker container prune | Остановленные container'ы | Низкая: данные в volumes остаются |
docker image prune | Образы без тега (dangling) | Низкая |
docker image prune -a | Все образы без container'а | Средняя: повторное скачивание |
docker builder prune | Build cache | Низкая: замедлит следующую сборку |
docker network prune | Неиспользуемые сети | Низкая |
docker volume prune | Неиспользуемые volumes | ВЫСОКАЯ: потеря данных |
docker system prune | Всё выше, кроме volumes и всех образов | Средняя |
docker system prune -a --volumes | Всё | ВЫСОКАЯ |
Две строки требуют отдельного внимания.
docker volume prune удаляет volumes, не подключённые ни к одному container'у. Если база данных была в анонимном volume, а container удалён — данные исчезнут. Восстановления нет.
docker system prune по умолчанию volumes не трогает. Это разумная защита, и её снимает флаг --volumes. Команду docker system prune -a --volumes не следует выполнять «для очистки» без понимания, что она удалит.
Выборочная очистка через фильтры
docker image prune -a --filter 'until=168h'
docker builder prune --filter 'until=72h'
docker container prune --filter 'until=24h'
docker image prune -a --filter 'label!=keep=true'
Фильтр until считает от времени создания, а не последнего использования. Образ, созданный год назад и используемый ежедневно, под until=168h попадёт — если на него не ссылается container.
Более надёжный подход — метки:
LABEL keep=true
docker image prune -a --filter 'label!=keep=true'
Кто пишет в свою файловую систему
docker ps -s
Колонка SIZE показывает два числа:
SIZE
124MB (virtual 1.2GB)
| Число | Смысл |
|---|---|
| Первое | Записано в writable layer этим container'ом |
virtual | Плюс размер образа |
Первое число — то, что интересно: сколько container записал сверх образа. Значение в сотни мегабайт означает, что приложение пишет в файловую систему container'а вместо volume (урок 7.1).
Предупреждение: docker ps -s медленная. Она обходит файловые системы всех container'ов; на большом хозяйстве занимает десятки секунд.
Автоматические ограничения
Лучше не убирать, а не сорить. Три настройки в /etc/docker/daemon.json:
{
"log-driver": "local",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"builder": {
"gc": {
"enabled": true,
"defaultKeepStorage": "10GB",
"policy": [
{"keepStorage": "10GB", "filter": ["unused-for=168h"]},
{"keepStorage": "20GB", "all": true}
]
}
}
}
| Настройка | Что ограничивает |
|---|---|
log-opts | Логи — категорию, невидимую в system df |
builder.gc | Build cache — обычно вторую по величине |
| Явные volumes вместо анонимных | Предотвращает накопление осиротевших |
Первые две закрывают большую часть проблемы без ручных действий.
Внутренний механизм
Почему удаление образа не освобождает место сразу
Слои общие. Удаление образа убирает ссылку на слои; сами слои удаляются, только когда на них не осталось ссылок ни от образов, ни от container'ов.
Отсюда наблюдение: docker rmi крупного образа освободил 20 МБ вместо 800. Остальное — общие слои, используемые другими образами.
Почему логи не попадают в system df
docker system df опрашивает подсистемы образов, container'ов, volumes и билдера. Логи не относятся ни к одной: это файлы, создаваемые logging driver рядом с метаданными container'а.
Проверить их объём можно только напрямую:
sudo du -sh /var/lib/docker/containers/*/ | sort -h | tail
Команды и примеры
Полная картина расхода
mkdir -p /tmp/disk && cd /tmp/disk
echo "═══ сводка по категориям ═══"
docker system df 2>/dev/null | sed 's/^/ /'
echo "═══ подробно: образы ═══"
docker system df -v 2>/dev/null | sed -n '/^Images space usage/,/^Containers space usage/p' \
| head -12 | sed 's/^/ /'
echo "═══ фактический размер каталога Docker ═══"
root="$(docker info --format '{{.DockerRootDir}}' 2>/dev/null)"
echo " каталог: $root"
if sudo test -d "$root" 2>/dev/null; then
sudo du -sh "$root" 2>/dev/null | sed 's/^/ всего: /'
echo " по подкаталогам:"
sudo du -sh "$root"/*/ 2>/dev/null | sort -h | tail -6 | sed 's/^/ /'
else
cat <<'TXT'
(чтение требует прав root)
При наличии доступа было бы видно:
всего: 58G
по подкаталогам:
1.2G /var/lib/docker/volumes/
8.4G /var/lib/docker/buildkit/
12G /var/lib/docker/overlay2/
36G /var/lib/docker/containers/ ← логи
Расхождение с docker system df — это логи.
TXT
fi
Ожидаемый вывод:
═══ сводка по категориям ═══
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 14 3 4.312GB 3.891GB (90%)
Containers 6 2 184.2MB 12.4MB (6%)
Local Volumes 9 2 1.204GB 942.1MB (78%)
Build Cache 87 0 2.418GB 2.418GB
═══ подробно: образы ═══
Images space usage:
REPOSITORY TAG IMAGE ID CREATED SIZE SHARED SIZE UNIQUE SIZE
myapp 1.2 8f3a2b1c4d5e 2 days ago 142.1MB 124.8MB 17.3MB
myapp 1.1 7e2b1a0c3d4f 5 days ago 141.9MB 124.8MB 17.1MB
python 3.13-slim 9c8d7e6f5a4b 1 week ago 124.8MB 124.8MB 0B
═══ фактический размер каталога Docker ═══
каталог: /var/lib/docker
(чтение требует прав root)
При наличии доступа было бы видно:
всего: 58G
по подкаталогам:
1.2G /var/lib/docker/volumes/
8.4G /var/lib/docker/buildkit/
12G /var/lib/docker/overlay2/
36G /var/lib/docker/containers/ ← логи
Расхождение с docker system df — это логи.
Три строки в разделе образов показывают главную ловушку колонки SIZE.
myapp:1.2 и myapp:1.1 весят по 142 МБ, но их UNIQUE SIZE — по 17 МБ. Удаление обоих освободит 34 МБ, а не 284.
У python:3.13-slim UNIQUE SIZE равен нулю: все его слои используются образами myapp. Удаление не освободит ничего.
Расход на логи: то, чего нет в отчёте
cd /tmp/disk
echo "═══ создаём container с активным логированием ═══"
docker run -d --name loghog -e PYTHONUNBUFFERED=1 python:3.13-slim \
python -c "
import time
line = 'x' * 500
for i in range(60000):
print(f'{i:06d} {line}')
" > /dev/null
sleep 8
echo "═══ что говорит docker system df ═══"
docker system df 2>/dev/null | grep -E 'TYPE|Containers' | sed 's/^/ /'
echo "═══ что на самом деле ═══"
logpath="$(docker inspect loghog --format '{{.LogPath}}')"
if sudo test -r "$logpath" 2>/dev/null; then
sudo du -h "$logpath" | sed 's/^/ файл лога: /'
else
docker logs loghog 2>/dev/null | wc -c \
| python3 -c "
import sys
n = int(sys.stdin.read().strip())
print(f' полезный текст в логе: {n / 1024 / 1024:.1f} МиБ')
print(f' на диске с метаданными JSON: примерно {n * 1.2 / 1024 / 1024:.1f} МиБ')
print(' (точный размер требует прав root на чтение файла)')
"
fi
echo "═══ как найти самые прожорливые ═══"
cat <<'TXT'
sudo du -sh /var/lib/docker/containers/*/ | sort -h | tail -10
Даёт размер каталога КАЖДОГО container'а, включая его лог.
Это единственный способ увидеть категорию, которой нет
в docker system df.
Сопоставить каталог с именем:
for d in /var/lib/docker/containers/*/; do
id=$(basename "$d")
name=$(docker inspect "$id" --format '{{.Name}}' 2>/dev/null)
printf '%s %s\n' "$(sudo du -sh "$d" | cut -f1)" "${name:-удалён}"
done | sort -h
TXT
docker rm -f loghog > /dev/null
Ожидаемый вывод:
═══ создаём container с активным логированием ═══
═══ что говорит docker system df ═══
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Containers 7 3 184.3MB 12.4MB (6%)
═══ что на самом деле ═══
полезный текст в логе: 30.5 МиБ
на диске с метаданными JSON: примерно 36.6 МиБ
(точный размер требует прав root на чтение файла)
═══ как найти самые прожорливые ═══
sudo du -sh /var/lib/docker/containers/*/ | sort -h | tail -10
Даёт размер каталога КАЖДОГО container'а, включая его лог.
Это единственный способ увидеть категорию, которой нет
в docker system df.
docker system df говорит о 184 МБ на все container'ы. Один только что созданный container имеет лог на 36 МБ, и в эти 184 МБ он не входит.
Категория Containers в отчёте — это writable layers, а не логи.
Кто пишет в свою файловую систему
cd /tmp/disk
echo "═══ container, пишущий в свой слой ═══"
docker run -d --name writer python:3.13-slim sh -c '
dd if=/dev/zero of=/tmp/big.bin bs=1M count=120 2>/dev/null
sleep 120
' > /dev/null
echo "═══ container, пишущий в volume ═══"
docker volume create writer-data > /dev/null
docker run -d --name writer-vol -v writer-data:/data python:3.13-slim sh -c '
dd if=/dev/zero of=/data/big.bin bs=1M count=120 2>/dev/null
sleep 120
' > /dev/null
sleep 6
echo "═══ docker ps -s (медленная команда) ═══"
docker ps -s --format 'table {{.Names}}\t{{.Size}}' 2>/dev/null \
| head -5 | sed 's/^/ /'
echo "═══ как читать ═══"
cat <<'TXT'
Формат: ЗАПИСАНО (virtual ВСЕГО)
Первое число — сколько container записал в свой writable layer.
virtual — плюс размер образа.
writer: 120MB записано → пишет в файловую систему container'а
writer-vol: около 0 записано → пишет в volume
Второй вариант правильный: данные переживают пересоздание
container'а и не раздувают его слой.
ПРЕДУПРЕЖДЕНИЕ: docker ps -s обходит файловые системы всех
container'ов. На большом хозяйстве это десятки секунд.
TXT
docker rm -f writer writer-vol > /dev/null
docker volume rm writer-data > /dev/null
Ожидаемый вывод:
═══ container, пишущий в свой слой ═══
═══ container, пишущий в volume ═══
═══ docker ps -s (медленная команда) ═══
NAMES SIZE
writer-vol 0B (virtual 124.8MB)
writer 125.8MB (virtual 250.6MB)
═══ как читать ═══
Формат: ЗАПИСАНО (virtual ВСЕГО)
Первое число — сколько container записал в свой writable layer.
virtual — плюс размер образа.
writer: 120MB записано → пишет в файловую систему container'а
writer-vol: около 0 записано → пишет в volume
Второй вариант правильный: данные переживают пересоздание
container'а и не раздувают его слой.
ПРЕДУПРЕЖДЕНИЕ: docker ps -s обходит файловые системы всех
container'ов. На большом хозяйстве это десятки секунд.
Две строки различаются в двадцать раз при одинаковой работе. Разница только в том, куда направлена запись.
Предварительный просмотр перед очисткой
cd /tmp/disk
cat > preview-prune.sh <<'SH'
#!/usr/bin/env bash
# Показывает, ЧТО будет удалено, до выполнения prune.
set -uo pipefail
printf '\n Предварительный просмотр очистки\n\n'
# 1. Остановленные container'ы
printf ' Остановленные container'"'"'ы:\n'
stopped="$(docker ps -a --filter 'status=exited' --filter 'status=created' \
--format '{{.Names}}\t{{.Status}}' 2>/dev/null)"
if [ -n "$stopped" ]; then
echo "$stopped" | head -8 | sed 's/^/ /'
printf ' итого: %s\n' "$(echo "$stopped" | grep -c .)"
else
printf ' нет\n'
fi
# 2. Образы без тега
printf '\n Образы без тега (dangling):\n'
dangling="$(docker images -f 'dangling=true' --format '{{.ID}}\t{{.Size}}' 2>/dev/null)"
if [ -n "$dangling" ]; then
echo "$dangling" | head -8 | sed 's/^/ /'
printf ' итого: %s\n' "$(echo "$dangling" | grep -c .)"
else
printf ' нет\n'
fi
# 3. Образы без container'а — с UNIQUE SIZE
printf '\n Образы без container'"'"'а (удалит только image prune -a):\n'
used="$(docker ps -a --format '{{.Image}}' 2>/dev/null | sort -u)"
docker images --format '{{.Repository}}:{{.Tag}}\t{{.Size}}' 2>/dev/null \
| grep -v '<none>' | head -20 | while IFS=$'\t' read -r ref size; do
if ! echo "$used" | grep -qF "${ref%%:*}"; then
printf ' %-40s %s\n' "$ref" "$size"
fi
done
# 4. Volumes без container'а — ОПАСНО
printf '\n Volumes без container'"'"'а — ⚠ УДАЛЕНИЕ БЕЗВОЗВРАТНО:\n'
orphan="$(docker volume ls -qf 'dangling=true' 2>/dev/null)"
if [ -n "$orphan" ]; then
echo "$orphan" | head -10 | while read -r v; do
mp="$(docker volume inspect "$v" --format '{{.Mountpoint}}' 2>/dev/null)"
sz="$(sudo du -sh "$mp" 2>/dev/null | cut -f1 || echo "?")"
printf ' %-46s %s\n' "${v:0:46}" "$sz"
done
printf ' итого: %s\n' "$(echo "$orphan" | grep -c .)"
printf ' ⚠ проверьте каждый: анонимный volume мог содержать данные БД\n'
else
printf ' нет\n'
fi
# 5. Build cache
printf '\n Build cache:\n'
docker system df 2>/dev/null | awk '/Build Cache/ {printf " записей %s, размер %s, освобождаемо %s\n", $3, $4, $5}'
printf '\n Команды по возрастанию риска:\n'
printf ' docker container prune безопасно\n'
printf ' docker image prune безопасно (только dangling)\n'
printf ' docker builder prune --filter until=72h безопасно (замедлит сборку)\n'
printf ' docker image prune -a повторное скачивание образов\n'
printf ' docker volume prune ⚠ ПОТЕРЯ ДАННЫХ\n'
printf ' docker system prune -a --volumes ⚠ ВСЁ СРАЗУ\n'
printf '\n'
SH
chmod +x preview-prune.sh
echo "═══ готовим объекты для очистки ═══"
docker run --name pv-stopped alpine:3.21 true > /dev/null 2>&1
docker volume create pv-orphan > /dev/null
docker run --rm -v pv-orphan:/d alpine:3.21 \
sh -c 'dd if=/dev/zero of=/d/data.bin bs=1M count=20 2>/dev/null' > /dev/null 2>&1
./preview-prune.sh
docker rm -f pv-stopped > /dev/null 2>&1
docker volume rm pv-orphan > /dev/null 2>&1
Ожидаемый вывод:
═══ готовим объекты для очистки ═══
Предварительный просмотр очистки
Остановленные container'ы:
pv-stopped Exited (0) 2 seconds ago
итого: 1
Образы без тега (dangling):
нет
Образы без container'а (удалит только image prune -a):
alpine:3.21 7.83MB
python:3.13-slim 124.8MB
Volumes без container'а — ⚠ УДАЛЕНИЕ БЕЗВОЗВРАТНО:
pv-orphan 20M
итого: 1
⚠ проверьте каждый: анонимный volume мог содержать данные БД
Build cache:
записей 87, размер 2.418GB, освобождаемо 2.418GB
Команды по возрастанию риска:
docker container prune безопасно
docker image prune безопасно (только dangling)
docker builder prune --filter until=72h безопасно (замедлит сборку)
docker image prune -a повторное скачивание образов
docker volume prune ⚠ ПОТЕРЯ ДАННЫХ
docker system prune -a --volumes ⚠ ВСЁ СРАЗУ
Раздел volumes — тот, ради которого написан скрипт. Он показывает размер каждого осиротевшего volume, а значит, даёт основание задуматься: 20 МБ данных где-то были нужны.
Что именно делает каждый prune
cd /tmp/disk
echo "═══ проверка: удаляет ли system prune volumes ═══"
docker volume create test-vol > /dev/null
docker run --rm -v test-vol:/d alpine:3.21 sh -c 'echo данные > /d/file.txt' > /dev/null
echo " volume создан, содержит файл"
docker system prune -f > /dev/null 2>&1
if docker volume inspect test-vol > /dev/null 2>&1; then
content="$(docker run --rm -v test-vol:/d alpine:3.21 cat /d/file.txt 2>/dev/null)"
echo " после docker system prune: volume ЖИВ, содержимое = $content"
else
echo " после docker system prune: volume УДАЛЁН"
fi
echo "═══ а с флагом --volumes ═══"
docker system prune -f --volumes > /dev/null 2>&1
if docker volume inspect test-vol > /dev/null 2>&1; then
echo " после --volumes: volume ещё жив"
docker volume rm test-vol > /dev/null 2>&1
else
echo " после --volumes: volume УДАЛЁН вместе с данными"
fi
echo "═══ вывод ═══"
cat <<'TXT'
docker system prune БЕЗ флага volumes не трогает — это защита.
Флаг --volumes её снимает.
Правило: если не уверены, что в volumes нет нужных данных,
флаг --volumes не используют. Восстановления нет.
Безопасная последовательность:
1. docker container prune освобождает ссылки
2. docker image prune dangling-образы
3. docker builder prune --filter until=72h
4. проверить volumes ВРУЧНУЮ, удалять поимённо
TXT
Ожидаемый вывод:
═══ проверка: удаляет ли system prune volumes ═══
volume создан, содержит файл
после docker system prune: volume ЖИВ, содержимое = данные
═══ а с флагом --volumes ═══
после --volumes: volume УДАЛЁН вместе с данными
═══ вывод ═══
docker system prune БЕЗ флага volumes не трогает — это защита.
Флаг --volumes её снимает.
Правило: если не уверены, что в volumes нет нужных данных,
флаг --volumes не используют. Восстановления нет.
Безопасная последовательность:
1. docker container prune освобождает ссылки
2. docker image prune dangling-образы
3. docker builder prune --filter until=72h
4. проверить volumes ВРУЧНУЮ, удалять поимённо
Проверка выполнена на настоящем volume с настоящим файлом — это надёжнее, чем полагаться на документацию.
Build cache: часто крупнейшая категория
cd /tmp/disk
echo "═══ размер build cache ═══"
docker system df 2>/dev/null | grep -E 'TYPE|Build' | sed 's/^/ /'
echo "═══ подробно ═══"
if docker buildx du --verbose 2>/dev/null | head -12 | sed 's/^/ /'; then
:
else
docker builder du 2>/dev/null | head -8 | sed 's/^/ /' \
|| echo " подробный отчёт недоступен в этой версии"
fi
echo "═══ выборочная очистка ═══"
cat <<'TXT'
Полная очистка (следующая сборка будет долгой):
docker builder prune -a
По возрасту — разумный компромисс:
docker builder prune --filter until=72h
По объёму — оставить не более N:
docker builder prune --reserved-space 10GB
Автоматически, через daemon.json:
"builder": {
"gc": {
"enabled": true,
"defaultKeepStorage": "10GB",
"policy": [
{"keepStorage": "10GB", "filter": ["unused-for=168h"]},
{"keepStorage": "20GB", "all": true}
]
}
}
Последний вариант предпочтителен: очистка не требует
вмешательства и не происходит в неподходящий момент.
TXT
Ожидаемый вывод:
═══ размер build cache ═══
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Build Cache 87 0 2.418GB 2.418GB
═══ подробно ═══
ID RECLAIMABLE SIZE LAST ACCESSED
x8k2m4n7p1q3r5s6 true 842.1MB 2 days ago
a1b2c3d4e5f6g7h8 true 614.3MB 5 days ago
z9y8x7w6v5u4t3s2 true 311.7MB 12 days ago
═══ выборочная очистка ═══
Полная очистка (следующая сборка будет долгой):
docker builder prune -a
...
RECLAIMABLE для build cache почти всегда равен полному размеру: кэш по определению восстановим. Цена очистки — время следующей сборки, а не потеря данных.
План освобождения места
cd /tmp/disk
cat > cleanup-plan.py <<'PY'
"""Строит план освобождения места: категории, объём, риск, команда.
Порядок — по возрастанию риска, а не по объёму: сначала то,
что удаляется безопасно.
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
def run(*args: str) -> str:
r = subprocess.run(args, capture_output=True, text=True)
return r.stdout
def parse_size(text: str) -> float:
"""Возвращает размер в МиБ."""
m = re.match(r"^([\d.]+)\s*([KMGT]?i?B)$", text.strip(), re.I)
if not m:
return 0.0
value = float(m.group(1))
unit = m.group(2).upper().rstrip("IB") or "B"
return value * {"B": 1 / 1024 / 1024, "K": 1 / 1024, "M": 1,
"G": 1024, "T": 1024 * 1024}.get(unit[0], 0)
def system_df() -> dict[str, dict[str, float]]:
out = {}
for line in run("docker", "system", "df").splitlines()[1:]:
parts = re.split(r"\s{2,}", line.strip())
if len(parts) >= 5:
reclaim = parts[4].split()[0]
out[parts[0]] = {"размер": parse_size(parts[3]),
"освобождаемо": parse_size(reclaim)}
return out
def count(*args: str) -> int:
return len([l for l in run(*args).splitlines() if l.strip()])
def build_plan() -> list[dict[str, object]]:
df = system_df()
stopped = count("docker", "ps", "-aq", "--filter", "status=exited")
dangling = count("docker", "images", "-qf", "dangling=true")
orphan_vols = count("docker", "volume", "ls", "-qf", "dangling=true")
return [
{
"шаг": 1, "категория": "остановленные container'ы",
"объектов": stopped,
"освободит_МиБ": df.get("Containers", {}).get("освобождаемо", 0),
"риск": "нет", "необратимо": False,
"команда": "docker container prune -f",
},
{
"шаг": 2, "категория": "образы без тега",
"объектов": dangling,
"освободит_МиБ": 0.0,
"риск": "нет", "необратимо": False,
"команда": "docker image prune -f",
},
{
"шаг": 3, "категория": "build cache старше 72 ч",
"объектов": 0,
"освободит_МиБ": df.get("Build Cache", {}).get("освобождаемо", 0),
"риск": "следующая сборка дольше", "необратимо": False,
"команда": "docker builder prune -f --filter until=72h",
},
{
"шаг": 4, "категория": "логи container'ов",
"объектов": 0,
"освободит_МиБ": 0.0,
"риск": "потеря истории логов", "необратимо": True,
"команда": "настроить ротацию в daemon.json и пересоздать container'ы",
},
{
"шаг": 5, "категория": "образы без container'а",
"объектов": 0,
"освободит_МиБ": df.get("Images", {}).get("освобождаемо", 0),
"риск": "повторное скачивание", "необратимо": False,
"команда": "docker image prune -af --filter until=168h",
},
{
"шаг": 6, "категория": "осиротевшие volumes",
"объектов": orphan_vols,
"освободит_МиБ": df.get("Local Volumes", {}).get("освобождаемо", 0),
"риск": "ПОТЕРЯ ДАННЫХ", "необратимо": True,
"команда": "проверить каждый вручную, затем docker volume rm ИМЯ",
},
]
if __name__ == "__main__":
plan = build_plan()
safe = sum(s["освободит_МиБ"] for s in plan if not s["необратимо"])
risky = sum(s["освободит_МиБ"] for s in plan if s["необратимо"])
print(f" {'шаг':>3} {'категория':<28} {'объектов':>9} {'освободит':>11} риск")
print(" " + "─" * 76)
for s in plan:
mark = "⚠" if s["необратимо"] else " "
size = f"{s['освободит_МиБ']:.0f} МиБ" if s["освободит_МиБ"] else "—"
print(f" {s['шаг']:>3} {s['категория']:<28} {s['объектов']:>9} {size:>11} {mark} {s['риск']}")
print()
print(f" безопасно освободится: {safe:.0f} МиБ")
print(f" требует решения: {risky:.0f} МиБ")
print()
print(" Команды по шагам:")
for s in plan:
mark = "⚠ " if s["необратимо"] else " "
print(f" {mark}{s['шаг']}. {s['команда']}")
PY
echo "═══ план освобождения ═══"
python3 cleanup-plan.py
cd /tmp && rm -rf /tmp/disk
Ожидаемый вывод:
═══ план освобождения ═══
шаг категория объектов освободит риск
────────────────────────────────────────────────────────────────────────────
1 остановленные container'ы 4 12 МиБ нет
2 образы без тега 2 — нет
3 build cache старше 72 ч 0 2476 МиБ следующая сборка дольше
4 логи container'ов 0 — ⚠ потеря истории логов
5 образы без container'а 0 3984 МиБ повторное скачивание
6 осиротевшие volumes 3 942 МиБ ⚠ ПОТЕРЯ ДАННЫХ
безопасно освободится: 6472 МиБ
требует решения: 942 МиБ
Команды по шагам:
1. docker container prune -f
2. docker image prune -f
3. docker builder prune -f --filter until=72h
⚠ 4. настроить ротацию в daemon.json и пересоздать container'ы
5. docker image prune -af --filter until=168h
⚠ 6. проверить каждый вручную, затем docker volume rm ИМЯ
Разделение итога на «безопасно» и «требует решения» — суть плана. Шесть гигабайт освобождаются без раздумий; последний гигабайт требует проверки каждого volume.
Практическое упражнение
Задание. Найдите, что заняло место, и освободите его безопасно.
Требования:
- Показать расхождение между
docker system dfи фактическим размером каталога Docker, назвав причину. - Показать, почему сумма колонки
SIZEвdocker imagesне равна занятому месту. - Определить, какой container пишет в свою файловую систему, и сколько.
- Доказать проверкой, что
docker system pruneбез флага не удаляет volumes, а с флагом — удаляет. - Построить план очистки с разделением на обратимое и необратимое.
- Настроить автоматические ограничения, снимающие необходимость ручной очистки.
Подсказки
Подсказка 1
Для пункта 1 сравнивайте docker system df с du каталога из docker info --format '{{.DockerRootDir}}'.
Подсказка 2
Пункт 2: сравните сумму SIZE из docker images с суммой UNIQUE SIZE из docker system df -v.
Подсказка 3
Для пункта 4 создайте volume с файлом, выполните оба варианта prune и проверьте содержимое после каждого.
Решение
Показать решение
mkdir -p /tmp/disklab && cd /tmp/disklab
# ─── Анализатор расхода ───────────────────────────────────────────────
cat > analyze.py <<'PY'
"""Разбирает расход места по категориям, включая невидимые для system df."""
from __future__ import annotations
import json
import re
import subprocess
import sys
def run(*args: str) -> str:
return subprocess.run(args, capture_output=True, text=True).stdout
def to_mib(text: str) -> float:
m = re.match(r"^\s*([\d.]+)\s*([kKmMgGtT]?)i?B?\s*$", text)
if not m:
return 0.0
v = float(m.group(1))
return v * {"": 1 / 1024 / 1024, "K": 1 / 1024, "M": 1,
"G": 1024, "T": 1024 * 1024}[m.group(2).upper()]
def system_df() -> dict[str, dict[str, float]]:
out = {}
for line in run("docker", "system", "df").splitlines()[1:]:
cols = re.split(r"\s{2,}", line.strip())
if len(cols) >= 5:
out[cols[0]] = {
"всего": int(cols[1]) if cols[1].isdigit() else 0,
"активно": int(cols[2]) if cols[2].isdigit() else 0,
"размер_МиБ": to_mib(cols[3]),
"освобождаемо_МиБ": to_mib(cols[4].split()[0]),
}
return out
def images_detail() -> dict[str, float]:
"""Сумма SIZE против суммы UNIQUE SIZE — показывает вклад общих слоёв."""
total_size = unique_size = 0.0
section = False
for line in run("docker", "system", "df", "-v").splitlines():
if line.startswith("Images space usage"):
section = True
continue
if section and line.startswith(("Containers space", "Local Volumes space")):
break
if not section or not line.strip() or line.startswith("REPOSITORY"):
continue
cols = re.split(r"\s{2,}", line.strip())
if len(cols) >= 7:
total_size += to_mib(cols[4])
unique_size += to_mib(cols[6])
return {"сумма_SIZE_МиБ": total_size, "сумма_UNIQUE_МиБ": unique_size}
def log_usage() -> dict[str, object]:
"""Объём логов — категория, отсутствующая в system df."""
total = 0
per_container = []
for cid in run("docker", "ps", "-aq").split():
path = run("docker", "inspect", cid, "--format", "{{.LogPath}}").strip()
name = run("docker", "inspect", cid, "--format", "{{.Name}}").strip().lstrip("/")
# Косвенная оценка: объём, отдаваемый docker logs
out = subprocess.run(["docker", "logs", cid],
capture_output=True)
size = len(out.stdout) + len(out.stderr)
if size:
per_container.append({"имя": name, "байт": size})
total += size
per_container.sort(key=lambda d: -d["байт"])
return {"оценка_МиБ": total / 1024 / 1024, "топ": per_container[:5]}
if __name__ == "__main__":
df = system_df()
print(json.dumps({
"system_df": df,
"образы": images_detail(),
"логи": log_usage(),
"видно_в_df_МиБ": sum(v["размер_МиБ"] for v in df.values()),
}, ensure_ascii=False, indent=2))
PY
# ─── План очистки ─────────────────────────────────────────────────────
cat > plan.py <<'PY'
"""План очистки: шаги по возрастанию риска, обратимое отдельно от необратимого."""
from __future__ import annotations
import json
import re
import subprocess
def run(*args: str) -> str:
return subprocess.run(args, capture_output=True, text=True).stdout
def to_mib(t: str) -> float:
m = re.match(r"^\s*([\d.]+)\s*([kKmMgGtT]?)i?B?\s*$", t)
if not m:
return 0.0
return float(m.group(1)) * {"": 1 / 1024 / 1024, "K": 1 / 1024, "M": 1,
"G": 1024, "T": 1024 * 1024}[m.group(2).upper()]
def reclaimable() -> dict[str, float]:
out = {}
for line in run("docker", "system", "df").splitlines()[1:]:
c = re.split(r"\s{2,}", line.strip())
if len(c) >= 5:
out[c[0]] = to_mib(c[4].split()[0])
return out
def n(*args: str) -> int:
return len([x for x in run(*args).split() if x])
STEPS = [
("остановленные container'ы", "Containers", False, "нет",
"docker container prune -f"),
("образы без тега", None, False, "нет",
"docker image prune -f"),
("build cache старше 72 ч", "Build Cache", False, "следующая сборка дольше",
"docker builder prune -f --filter until=72h"),
("образы без container'а", "Images", False, "повторное скачивание",
"docker image prune -af --filter until=168h"),
("логи без ротации", None, True, "потеря истории логов",
"настроить log-opts в daemon.json, пересоздать container'ы"),
("осиротевшие volumes", "Local Volumes", True, "ПОТЕРЯ ДАННЫХ",
"проверить каждый, затем docker volume rm ИМЯ"),
]
def main() -> None:
rec = reclaimable()
rows = []
for i, (name, key, irreversible, risk, cmd) in enumerate(STEPS, 1):
rows.append({
"шаг": i, "категория": name,
"освободит_МиБ": rec.get(key, 0.0) if key else 0.0,
"необратимо": irreversible, "риск": risk, "команда": cmd,
})
safe = sum(r["освободит_МиБ"] for r in rows if not r["необратимо"])
risky = sum(r["освободит_МиБ"] for r in rows if r["необратимо"])
print(f" {'шаг':>3} {'категория':<28} {'освободит':>11} риск")
print(" " + "─" * 74)
for r in rows:
mark = "⚠" if r["необратимо"] else " "
size = f"{r['освободит_МиБ']:.0f} МиБ" if r["освободит_МиБ"] else "—"
print(f" {r['шаг']:>3} {r['категория']:<28} {size:>11} {mark} {r['риск']}")
print()
print(f" обратимо: {safe:.0f} МиБ")
print(f" необратимо: {risky:.0f} МиБ — требует проверки перед удалением")
print()
print(json.dumps({"обратимо_МиБ": round(safe, 1),
"необратимо_МиБ": round(risky, 1),
"шагов": len(rows)}, ensure_ascii=False))
if __name__ == "__main__":
main()
PY
fail=0
ok() { printf ' ✓ %s\n' "$1"; }
bad() { printf ' ✗ %s\n' "$1"; fail=1; }
printf '\n═══ Подготовка: создаём расход ═══\n'
docker volume create lab-orphan > /dev/null
docker run --rm -v lab-orphan:/d alpine:3.21 \
sh -c 'dd if=/dev/zero of=/d/data.bin bs=1M count=30 2>/dev/null' > /dev/null 2>&1
docker run --name lab-stopped alpine:3.21 true > /dev/null 2>&1
docker run -d --name lab-logs -e PYTHONUNBUFFERED=1 python:3.13-slim \
python -c "
line = 'L' * 400
for i in range(40000):
print(f'{i:06d} {line}')
" > /dev/null
docker run -d --name lab-writer python:3.13-slim sh -c \
'dd if=/dev/zero of=/tmp/blob.bin bs=1M count=90 2>/dev/null; sleep 200' > /dev/null
sleep 10
printf ' объекты созданы\n'
printf '\n═══ Требование 1: расхождение df и каталога ═══\n'
analysis="$(python3 analyze.py 2>/dev/null)"
echo "$analysis" | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(f\" docker system df суммарно: {d['видно_в_df_МиБ']:.0f} МиБ\")
print(f\" оценка объёма логов: {d['логи']['оценка_МиБ']:.0f} МиБ\")
print(f\" логи в отчёт system df НЕ ВХОДЯТ\")
print(' крупнейшие по логам:')
for c in d['логи']['топ'][:3]:
print(f\" {c['имя']:<20} {c['байт'] / 1024 / 1024:6.1f} МиБ\")
"
root="$(docker info --format '{{.DockerRootDir}}' 2>/dev/null)"
if sudo test -d "$root" 2>/dev/null; then
actual="$(sudo du -sm "$root" 2>/dev/null | cut -f1)"
printf ' фактический размер %s: %s МиБ\n' "$root" "$actual"
du_ok=1
else
printf ' фактический размер каталога: НЕ ИЗМЕРЕН (нужны права root)\n'
du_ok=0
fi
log_mib="$(echo "$analysis" | python3 -c "
import json,sys; print(int(json.load(sys.stdin)['логи']['оценка_МиБ']))")"
[ "$log_mib" -gt 5 ] \
&& ok "логи занимают $log_mib МиБ и не видны в system df (du доступен: $du_ok)" \
|| bad "объём логов: $log_mib МиБ"
printf '\n═══ Требование 2: SIZE против UNIQUE SIZE ═══\n'
echo "$analysis" | python3 -c "
import json, sys
d = json.load(sys.stdin)['образы']
s, u = d['сумма_SIZE_МиБ'], d['сумма_UNIQUE_МиБ']
print(f' сумма колонки SIZE: {s:8.0f} МиБ')
print(f' сумма колонки UNIQUE SIZE: {u:8.0f} МиБ')
print(f' разница (общие слои): {s - u:8.0f} МиБ')
if s > 0:
print(f' доля общих слоёв: {(s - u) / s * 100:.0f} %')
"
printf ' вывод: удаление образа освобождает только UNIQUE SIZE\n'
diff_ok="$(echo "$analysis" | python3 -c "
import json, sys
d = json.load(sys.stdin)['образы']
print(1 if d['сумма_SIZE_МиБ'] > d['сумма_UNIQUE_МиБ'] else 0)
")"
[ "$diff_ok" = "1" ] \
&& ok "сумма SIZE превышает реальный расход из-за общих слоёв" \
|| bad "разница не обнаружена"
printf '\n═══ Требование 3: кто пишет в свой слой ═══\n'
printf ' (docker ps -s обходит файловые системы — это занимает время)\n'
docker ps -s --format '{{.Names}}|{{.Size}}' 2>/dev/null | while IFS='|' read -r nm sz; do
printf ' %-16s %s\n' "$nm" "$sz"
done
writer_size="$(docker ps -s --format '{{.Names}}|{{.Size}}' 2>/dev/null \
| grep '^lab-writer|' | cut -d'|' -f2 | cut -d' ' -f1)"
printf ' lab-writer записал в свой слой: %s\n' "${writer_size:-?}"
case "$writer_size" in
*MB|*GB) ok "container, пишущий в файловую систему, найден: $writer_size" ;;
*) bad "размер writable layer: $writer_size" ;;
esac
printf '\n═══ Требование 4: удаляет ли system prune volumes ═══\n'
docker volume create lab-keep > /dev/null
docker run --rm -v lab-keep:/d alpine:3.21 sh -c 'echo ВАЖНЫЕ-ДАННЫЕ > /d/f.txt' > /dev/null
docker system prune -f > /dev/null 2>&1
if docker volume inspect lab-keep > /dev/null 2>&1; then
body="$(docker run --rm -v lab-keep:/d alpine:3.21 cat /d/f.txt 2>/dev/null)"
printf ' после «docker system prune»: volume жив, содержимое=%s\n' "$body"
survived_plain=1
else
printf ' после «docker system prune»: volume УДАЛЁН\n'
survived_plain=0
fi
docker system prune -f --volumes > /dev/null 2>&1
if docker volume inspect lab-keep > /dev/null 2>&1; then
printf ' после «docker system prune --volumes»: volume ещё жив\n'
survived_volumes=1
docker volume rm lab-keep > /dev/null 2>&1
else
printf ' после «docker system prune --volumes»: volume УДАЛЁН вместе с данными\n'
survived_volumes=0
fi
[ "$survived_plain" -eq 1 ] && [ "$survived_volumes" -eq 0 ] \
&& ok "проверено на настоящем volume: флаг --volumes решает всё" \
|| bad "без флага=$survived_plain с флагом=$survived_volumes"
printf '\n═══ Требование 5: план очистки ═══\n'
plan_out="$(python3 plan.py 2>/dev/null)"
echo "$plan_out" | head -12
plan_json="$(echo "$plan_out" | tail -1)"
steps="$(echo "$plan_json" | python3 -c "import json,sys; print(json.load(sys.stdin)['шагов'])")"
irr="$(echo "$plan_json" | python3 -c "import json,sys; print(json.load(sys.stdin)['необратимо_МиБ'])")"
printf ' шагов в плане: %s, необратимо: %s МиБ\n' "$steps" "$irr"
[ "$steps" -ge 5 ] \
&& ok "план разделяет обратимое и необратимое" \
|| bad "шагов: $steps"
printf '\n═══ Требование 6: автоматические ограничения ═══\n'
cat > daemon-recommended.json <<'JSON'
{
"log-driver": "local",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"builder": {
"gc": {
"enabled": true,
"defaultKeepStorage": "10GB",
"policy": [
{"keepStorage": "10GB", "filter": ["unused-for=168h"]},
{"keepStorage": "20GB", "all": true}
]
}
}
}
JSON
python3 -c "
import json
cfg = json.load(open('daemon-recommended.json'))
print(' предлагаемый /etc/docker/daemon.json:')
opts = cfg['log-opts']
n_containers = 20
per = int(opts['max-size'].rstrip('m')) * int(opts['max-file'])
print(f\" драйвер логов: {cfg['log-driver']} (ротация по умолчанию)\")
print(f\" явная ротация: {opts['max-size']} × {opts['max-file']} = {per} МиБ на container\")
print(f\" бюджет логов на {n_containers} container'ов: {per * n_containers / 1024:.1f} ГиБ\")
gc = cfg['builder']['gc']
print(f\" build cache: включён GC, предел {gc['defaultKeepStorage']}\")
print(f\" правил политики: {len(gc['policy'])}\")
print()
print(' Закрывает две категории, которые растут без присмотра:')
print(' логи (не видны в system df) и build cache (обычно крупнейшая).')
"
printf ' применение: скопировать в /etc/docker/daemon.json, перезапустить daemon\n'
printf ' ВАЖНО: настройка логов действует только на НОВЫЕ container'"'"'ы\n'
valid="$(python3 -c "
import json
try:
json.load(open('daemon-recommended.json'))
print('да')
except Exception:
print('нет')
")"
[ "$valid" = "да" ] \
&& ok "конфигурация валидна и закрывает обе невидимые категории" \
|| bad "конфигурация невалидна"
printf '\n═══ Проверка: очистка по плану ═══\n'
before="$(docker system df --format '{{.Type}} {{.Size}}' 2>/dev/null | tr '\n' ' ')"
printf ' до: %s\n' "$before"
docker rm -f lab-logs lab-writer > /dev/null 2>&1
docker container prune -f > /dev/null 2>&1
docker image prune -f > /dev/null 2>&1
after="$(docker system df --format '{{.Type}} {{.Size}}' 2>/dev/null | tr '\n' ' ')"
printf ' после: %s\n' "$after"
printf ' volume lab-orphan НЕ удалён — требует ручной проверки:\n'
docker volume ls -qf 'dangling=true' 2>/dev/null | grep -c 'lab-orphan' \
| sed 's/^/ осиротевших с именем lab-orphan: /'
ok "безопасные шаги выполнены, необратимый оставлен на решение человека"
printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo " все требования выполнены" || echo " ЕСТЬ ПРОВАЛЫ"
[ "$du_ok" = "0" ] && echo " примечание: размер каталога Docker не измерялся — нет прав root"
docker volume rm lab-orphan > /dev/null 2>&1
docker rm -f lab-stopped > /dev/null 2>&1
cd /tmp && rm -rf /tmp/disklab
exit "$fail"
Ожидаемый вывод:
═══ Подготовка: создаём расход ═══
объекты созданы
═══ Требование 1: расхождение df и каталога ═══
docker system df суммарно: 4218 МиБ
оценка объёма логов: 16 МиБ
логи в отчёт system df НЕ ВХОДЯТ
крупнейшие по логам:
lab-logs 15.6 МиБ
lab-writer 0.0 МиБ
фактический размер каталога: НЕ ИЗМЕРЕН (нужны права root)
✓ логи занимают 16 МиБ и не видны в system df (du доступен: 0)
═══ Требование 2: SIZE против UNIQUE SIZE ═══
сумма колонки SIZE: 528 МиБ
сумма колонки UNIQUE SIZE: 141 МиБ
разница (общие слои): 387 МиБ
доля общих слоёв: 73 %
вывод: удаление образа освобождает только UNIQUE SIZE
✓ сумма SIZE превышает реальный расход из-за общих слоёв
═══ Требование 3: кто пишет в свой слой ═══
(docker ps -s обходит файловые системы — это занимает время)
lab-writer 94.4MB (virtual 219.2MB)
lab-logs 0B (virtual 124.8MB)
lab-writer записал в свой слой: 94.4MB
✓ container, пишущий в файловую систему, найден: 94.4MB
═══ Требование 4: удаляет ли system prune volumes ═══
после «docker system prune»: volume жив, содержимое=ВАЖНЫЕ-ДАННЫЕ
после «docker system prune --volumes»: volume УДАЛЁН вместе с данными
✓ проверено на настоящем volume: флаг --volumes решает всё
═══ Требование 5: план очистки ═══
шаг категория освободит риск
──────────────────────────────────────────────────────────────────────────
1 остановленные container'ы 0 МиБ нет
2 образы без тега — нет
3 build cache старше 72 ч 412 МиБ следующая сборка дольше
4 образы без container'а 3891 МиБ повторное скачивание
5 логи без ротации — ⚠ потеря истории логов
6 осиротевшие volumes 30 МиБ ⚠ ПОТЕРЯ ДАННЫХ
обратимо: 4303 МиБ
необратимо: 30 МиБ — требует проверки перед удалением
шагов в плане: 6, необратимо: 30.0 МиБ
✓ план разделяет обратимое и необратимое
═══ Требование 6: автоматические ограничения ═══
предлагаемый /etc/docker/daemon.json:
драйвер логов: local (ротация по умолчанию)
явная ротация: 10m × 3 = 30 МиБ на container
бюджет логов на 20 container'ов: 0.6 ГиБ
build cache: включён GC, предел 10GB
правил политики: 2
Закрывает две категории, которые растут без присмотра:
логи (не видны в system df) и build cache (обычно крупнейшая).
применение: скопировать в /etc/docker/daemon.json, перезапустить daemon
ВАЖНО: настройка логов действует только на НОВЫЕ container'ы
✓ конфигурация валидна и закрывает обе невидимые категории
═══ Проверка: очистка по плану ═══
до: Images 4.31GB Containers 184.3MB Local Volumes 1.2GB Build Cache 412MB
после: Images 4.31GB Containers 0B Local Volumes 1.2GB Build Cache 412MB
volume lab-orphan НЕ удалён — требует ручной проверки:
осиротевших с именем lab-orphan: 1
✓ безопасные шаги выполнены, необратимый оставлен на решение человека
═══ ИТОГ ═══
все требования выполнены
примечание: размер каталога Docker не измерялся — нет прав root
Все требования выполнены; размер каталога Docker измерить не удалось, и это отмечено.
Требование 2 даёт самое практичное число: 73 % суммарного размера образов — это общие слои. Планирование очистки по колонке SIZE завысило бы ожидаемый выигрыш вчетверо.
Три решения, определяющие качество.
Требование 4 проверено на настоящем volume с настоящим файлом. Можно было сослаться на документацию — но поведение prune менялось между версиями, и утверждение о защите volumes стоит того, чтобы его проверить. Файл с содержимым ВАЖНЫЕ-ДАННЫЕ читается после первой команды и не читается после второй: это доказательство, а не цитата.
План разделяет итог на «обратимо» и «необратимо», а не сортирует по объёму. Сортировка по объёму поставила бы первым шагом удаление образов на 3,9 ГБ. Порядок по риску ставит первыми действия, которые можно выполнить не думая, и оставляет 30 МБ осиротевших volumes на отдельное решение. Тридцать мегабайт данных стоят минуты проверки.
Оценка объёма логов идёт через docker logs, а не через чтение файла. Прямое чтение LogPath требует root и на многих машинах недоступно. Объём, отдаваемый docker logs, — оценка снизу (без метаданных JSON), но она доступна всегда и достаточна, чтобы обнаружить категорию.
Чего решение не делает. Фактический размер каталога Docker не измерен: du требует root, и решение об этом сообщает вместо того, чтобы подставить правдоподобное число. Оценка логов занижена — она не учитывает накладные расходы формата JSON, около 20 %. Ротация в daemon.json предложена, но не применена: изменение конфигурации daemon и его перезапуск выходят за рамки упражнения и требуют отдельного решения. Наконец, docker ps -s выполнялась на пяти container'ах; на сотне она заняла бы минуты, и в регулярном мониторинге её не используют.
Проверка результата
docker system df
docker system df -v | head -20
docker info --format '{{.DockerRootDir}}'
sudo du -sh $(docker info --format '{{.DockerRootDir}}')/*/ | sort -h
docker ps -s --format 'table {{.Names}}\t{{.Size}}'
Сравните сумму из docker system df с выводом du — разница объясняется логами.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
Доверяют docker system df целиком | Выглядит исчерпывающим | Логи в него не входят |
Планируют очистку по колонке SIZE | Естественное число | Освободится только UNIQUE SIZE |
docker system prune -a --volumes «для очистки» | Одна команда решает всё | Удаляет данные безвозвратно |
Считают RECLAIMABLE = 0 признаком чистоты | Число нулевое | Образы удерживают остановленные container'ы |
| Забывают про build cache | Не виден в docker images | Часто крупнейшая категория |
Удаляют образ, ожидая освободить его SIZE | Колонка так называется | Общие слои остаются |
| Не настраивают ротацию логов | Работает и так | Категория растёт без ограничений |
docker ps -s в мониторинге | Даёт нужное число | Обходит файловые системы; медленно |
Фильтр until считают «по последнему использованию» | Логично предположить | Считает от создания |
| Приложение пишет в файловую систему container'а | Работает | Слой растёт; данные теряются при пересоздании |
Контрольные вопросы
На понимание:
- Почему сумма колонки
SIZEвdocker imagesбольше занятого места? - Какая категория расхода не входит в
docker system dfи как её измерить? - Что означает
RECLAIMABLEдля образов и почему бывает равен нулю? - Чем
docker system pruneотличается отdocker system prune --volumes? - Что показывают два числа в колонке
SIZEвыводаdocker ps -s?
На применение:
- Как определить, какой container пишет в свою файловую систему?
- В каком порядке выполнять очистку и почему именно в таком?
- Как настроить систему так, чтобы ручная очистка не требовалась?
На диагностику:
docker system dfпоказывает 12 ГБ, каталог занимает 58 ГБ. Причина?- Удалили образ на 800 МБ, освободилось 20 МБ. Почему?
Краткое резюме
- Шесть категорий расхода;
docker system dfпоказывает четыре. - Логи container'ов в отчёт не входят — их измеряют через
duкаталога. - Колонка
SIZEвключает общие слои; освободится толькоUNIQUE SIZE. RECLAIMABLE= 0 при наличии образов означает, что их удерживают остановленные container'ы.docker system pruneне трогает volumes; флаг--volumesэту защиту снимает.docker volume pruneудаляет данные безвозвратно — восстановления нет.- Build cache часто крупнейшая категория и не виден в
docker images. docker ps -sпоказывает записанное в writable layer — и работает медленно.- Запись в файловую систему container'а вместо volume раздувает слой и теряется при пересоздании.
- Фильтр
untilсчитает от создания, а не от последнего использования. - Порядок очистки — по возрастанию риска, а не по объёму.
- Ротация логов и GC билдера в
daemon.jsonзакрывают обе растущие без присмотра категории.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
Docker: docker system df | https://docs.docker.com/reference/cli/docker/system/df/ | Категории и колонки |
| Docker: prune unused objects | https://docs.docker.com/engine/manage-resources/pruning/ | Поведение каждой команды prune |
Docker: docker system prune | https://docs.docker.com/reference/cli/docker/system/prune/ | Флаг --volumes, фильтры |
Docker: docker builder prune | https://docs.docker.com/reference/cli/docker/buildx/prune/ | Очистка build cache |
| Docker: builder GC | https://docs.docker.com/build/cache/garbage-collection/ | Настройка builder.gc |
Docker: docker ps | https://docs.docker.com/reference/cli/docker/container/ls/#size | Колонка SIZE и virtual |
| Docker: daemon.json | https://docs.docker.com/reference/cli/dockerd/#daemon-configuration-file | Формат конфигурации |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Глубокая диагностика
Главное оглавление