Главная/Работа с Images/Урок

3.4. Управление images

Цели

После этого материала вы сможете:

  • находить образ, ответственный за расход дискового пространства, и объяснять его размер по слоям;
  • читать docker history и связывать слои с инструкциями Dockerfile;
  • фильтровать и форматировать вывод docker images под конкретную задачу;
  • объяснить, откуда берутся dangling images и почему они накапливаются;
  • безопасно удалять образы, отличая неиспользуемые от нужных;
  • разбирать ошибку conflict: unable to delete … (must be forced) и снимать блокировку, не прибегая к -f;
  • объяснить, что именно удаляет каждый вариант docker system prune, и выполнить предварительный просмотр;
  • учитывать различия вывода в Docker Engine 29 с containerd image store.

Предварительные знания

Ключевые термины

ТерминОбъяснение
dangling imageОбраз без тега, на который никто не ссылается: <none>:<none>
unused imageОбраз с тегом, не используемый ни одним container
intermediate imageПромежуточный образ, созданный при сборке (в классическом builder)
build cacheКэш BuildKit, хранящийся отдельно от образов
reclaimableОбъём, который можно освободить удалением неиспользуемого
filterУсловие отбора объектов в командах Docker: --filter key=value

Теория

Три источника расхода места

Прежде чем что-то удалять, нужно понять, что именно занимает диск. Docker расходует место по четырём независимым категориям:

КатегорияЧто этоРастёт когда
ImagesСлои образовЗагружаете и собираете образы
ContainersWritable layersContainers пишут данные и не удаляются
Local VolumesДанные volumesПриложения хранят состояние
Build CacheКэш BuildKitСобираете образы

Люди обычно чистят первую категорию, а место чаще всего съедают четвёртая и вторая. Поэтому диагностика начинается с docker system df, а не с docker images.

Почему размеры не складываются

Сумма колонки SIZE в docker images почти всегда больше фактически занятого места. Причина — дедупликация слоёв (урок 3.1): десять образов на общей базе делят её слои, и на диске она лежит один раз.

Правило: для оценки занятого места используйте docker system df, а не сложение SIZE.

Откуда берутся образы <none>

Образ без тега появляется в трёх ситуациях, и их полезно различать.

1. Тег перепривязан к новому образу. Вы выполнили docker pull python:3.13-slim, вышла новая сборка — тег переехал, старый образ остался без имени. Это главный источник накопления у тех, кто регулярно обновляется.

2. Пересборка с тем же тегом. docker build -t myapp:dev . — предыдущий myapp:dev теряет тег. При активной разработке это происходит десятки раз в день.

3. Загрузка по digest. docker pull python@sha256:... даёт образ без тега намеренно.

Первые два случая — dangling images, их обычно можно удалять. Третий — нет: образ нужен, просто у него нет имени.

Различить помогает фильтр dangling=true, который отбирает именно образы, не имеющие ни тега, ни ссылок от других образов.

Что показывает docker history

Команда выводит содержимое поля history из конфигурации образа (урок 3.1) вместе с размерами слоёв.

Читается снизу вверх: последняя строка — базовый слой, первая — последняя инструкция.

text
IMAGE          CREATED         CREATED BY                          SIZE
<missing>      2 minutes ago   CMD ["python" "app.py"]             0B      ← последняя
<missing>      2 minutes ago   COPY app.py /app/ # buildkit        1.2kB
<missing>      3 minutes ago   RUN pip install -r requirements.txt 84.3MB
<missing>      3 minutes ago   COPY requirements.txt /app/ # bui…  128B
<missing>      3 weeks ago     CMD ["python3"]                     0B
<missing>      3 weeks ago     ADD file:abc... in /                126MB   ← базовый

<missing> в колонке IMAGE — нормально: это не ошибка. Промежуточные образы существуют только на машине, где выполнялась сборка; в загруженном образе от них остались лишь записи истории.

Строки с размером 0B соответствуют инструкциям, изменившим только конфигурацию.

Различия вывода в Docker Engine 29

С containerd image store вывод docker images изменился:

ИзменениеБылоСтало
ФорматПлоский списокСвёрнутое дерево
Образы без тегаПоказывалисьСкрыты без --all
КолонкиSIZEDISK USAGE, CONTENT SIZE
Усечение имёнДлинные имена обрезалисьНе обрезаются

Различие DISK USAGE и CONTENT SIZE: containerd хранит слои и в сжатом, и в распакованном виде. CONTENT SIZE — объём blob (сжатых), DISK USAGE — фактическое потребление с учётом распакованных снапшотов.

Практический вывод: если вы видите два разных числа вместо одного SIZE, вы работаете на containerd image store. Проверить: docker info --format '{{.Driver}}' даёт overlayfs.

Ловушка: docker images и docker image inspect дают разные числа

Различие не косметическое, и на нём легко ошибиться в четыре раза.

bash
docker images fastapi-basic
echo "--- через --format ---"
docker images fastapi-basic --format '{{.Size}}'
docker image inspect fastapi-basic --format '{{.Size}}' | numfmt --to=si
echo "--- сколько весит перенос ---"
docker save fastapi-basic | wc -c | numfmt --to=si
text
IMAGE                  ID             DISK USAGE   CONTENT SIZE   EXTRA
fastapi-basic:latest   d59b4177e2da        293MB         68.5MB
--- через --format ---
293MB
69M
--- сколько весит перенос ---
69M

{{.Size}} в двух командах означает разное:

КомандаЧто возвращаетДля чего это число
docker images --format '{{.Size}}'DISK USAGEсколько занято на узле
docker image inspect --format '{{.Size}}'CONTENT SIZEсколько качается из registry

Совпадение docker save с inspect подтверждает: inspect считает сжатые blob'ы.

Почему это важно. Требование вида «образ меньше 200 MB» без указания команды становится бессмысленным: один и тот же образ FastAPI — 293 MB или 68.5 MB, смотря чем мерить. Разница — коэффициент сжатия слоёв, для Debian-основы примерно 4×.

Ориентир для сравнения: python:3.13-slim сам по себе — 178 MB на диске при 41 MB контента. То есть на диске у приложения на slim остаётся 22 MB до порога в 200 MB — уложиться нельзя, и порог задавался не про это число.

На классическом graphdriver-хранилище оба числа совпадали, потому что распакованный размер был единственным. Утверждение приведено по документации; на этом стенде проверить нельзя — переключение хранилища требует перезапуска демона с потерей образов.


Внутренний механизм

Что происходит при docker rmi

  1. Docker смотрит, сколько ссылок (тегов) у образа. Если указанная ссылка не последняя, она просто снимается: вывод Untagged, образ остаётся под другими именами, и на этом всё.
  2. Если снимается последняя ссылка, проверяется, есть ли container'ы, созданные из этого образа, — в любом состоянии, включая остановленные. Если есть — отказ с ошибкой conflict.
  3. Container'ов нет: ссылка снимается (Untagged), удаляется и запись об образе (Deleted).
  4. Для каждого слоя проверяется, ссылаются ли на него другие образы. Слои без ссылок удаляются с диска.

Шаг 4 объясняет, почему удаление образа на 1.5 GB может освободить 40 MB: остальные слои переиспользуются.

Шаги 1 и 3 объясняют разницу между Untagged и Deleted в выводе — их постоянно путают.

Шаг 2 — источник самой частой ошибки удаления; ей посвящён следующий подраздел.

Ссылки от остановленных containers

Ссылку на образ держит любой существующий container, а не только запущенный. Остановленный, завершившийся с ошибкой, отработавший секунду и вышедший — все они продолжают ссылаться на свой образ, потому что хранят writable layer и конфигурацию, а те привязаны к образу.

Отсюда самая частая ошибка удаления. Команда

bash
docker rmi alpine:latest

отвечает

text
Error response from daemon: conflict: unable to delete alpine:latest (must be forced) - container 60686429323c is using its referenced image 28bd5fe8b56d

при том, что docker ps пуст: container завершился и виден только с -a.

Четыре формулировки одного отказа

Текст сообщения зависит от двух вещей: чем адресован образ (тегом или ID) и работает ли container. Проверено на Docker 29.7.1 с containerd image store:

КомандаContainerСообщение после conflict:
docker rmi repo:tagостановленunable to delete repo:tag (must be forced) - container <ctr> is using its referenced image <img>
docker rmi repo:tagработаетто же самое
docker rmi <image-id>остановленunable to delete <img> (must be forced) - image is being used by stopped container <ctr>
docker rmi <image-id>работаетunable to delete <img> (cannot be forced) - image is being used by running container <ctr>

Смысл несёт скобка:

  • must be forced — daemon отказывается, но флаг -f отказ снимет. Это констатация механики, а не рекомендация: что именно сделает -f, разбирается ниже.
  • cannot be forced — образ используется работающим container'ом, и -f не поможет. Container придётся остановить или удалить.

Разница объясняется тем, что удаление по тегу и удаление по ID — разные операции. По тегу Docker снимает ссылку; сделать это он может всегда, образ просто останется без имени, поэтому нужно только согласие пользователя. По ID удаляется сам образ — а вытащить его из-под работающего процесса нельзя ни при каком флаге.

Когда конфликта не возникает

Если у образа есть другой тег, docker rmi по любому из них проходит без ошибки: снятие одной ссылки не оставляет образ безымянным, и container ничего не теряет.

bash
docker tag alpine:latest spare:1   # у образа стало две ссылки
docker rmi spare:1                 # Untagged: spare:1 — конфликта нет

Конфликт возникает только при попытке удалить последнюю ссылку на образ, у которого есть container.

На классическом хранилище (graphdriver) первая формулировка выглядит иначе: conflict: unable to remove repository reference "myapp:1.0" (must force) - container 3f2a1b4c5d6e is using its referenced image 8f7e6d5c4b3a. Смысл тот же. Здесь и далее приводится вывод containerd image store — умолчания Docker Engine 29; на этом стенде другое хранилище проверить нельзя.


Команды и примеры

Начинать с общей картины

bash
docker system df
text
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          24        6         8.412GB   5.903GB (70%)
Containers      11        3         184.2MB   131.7MB (71%)
Local Volumes   7         3         2.104GB   1.312GB (62%)
Build Cache     183       0         4.271GB   4.271GB

Читается так:

КолонкаЗначение
TOTALВсего объектов
ACTIVEИспользуются: образ имеет container, volume примонтирован
SIZEЗанято с учётом дедупликации
RECLAIMABLEОсвободится при удалении неиспользуемого

В этом примере суммарно 15 GB, из которых освободить можно 11.5 GB. Больше всего — build cache, а не образы.

Детализация:

bash
docker system df -v

Выводит построчно каждый образ, container, volume и запись кэша. На загруженной машине вывод длинный — направляйте в файл или в less.

Анализ размера конкретного образа

bash
docker pull -q python:3.13-slim
mkdir -p /tmp/mgmt && cd /tmp/mgmt

cat > requirements.txt <<'EOF'
fastapi==0.141.1
uvicorn[standard]==0.52.0
EOF

cat > app.py <<'EOF'
print("hello")
EOF

cat > Dockerfile <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
EOF

docker build -q -t sizedemo:1 . > /dev/null
docker history sizedemo:1 --format 'table {{.Size}}\t{{.CreatedBy}}' --no-trunc | head -8
text
SIZE      CREATED BY
0B        CMD ["python" "app.py"]
12.3kB    COPY app.py . # buildkit
51.9MB    RUN /bin/sh -c pip install --no-cache-dir -r requirements.txt # buildkit
12.3kB    COPY requirements.txt . # buildkit
8.19kB    WORKDIR /app
0B        CMD ["python3"]
16.4kB    RUN /bin/sh -c set -eux;  for src in idle3 pip3 pydoc3 python3 ...

Сразу видно распределение: 51.9 MB добавили зависимости, остальное — базовый образ.

Обратите внимание на COPY app.py . — 12.3 kB при файле в тридцать байт. Слой не бывает меньше блока файловой системы, и десяток мелких COPY обойдётся дороже, чем кажется по размеру исходников.

Отсортируем слои по убыванию размера — это самый быстрый способ найти виновника раздувания:

bash
docker history sizedemo:1 --no-trunc --format '{{.Size}}\t{{.CreatedBy}}' \
  | python3 -c "
import sys, re

def to_bytes(s):
    s = s.strip()
    m = re.match(r'^([\d.]+)\s*([KMGT]?B)$', s)
    if not m:
        return 0
    n, unit = float(m.group(1)), m.group(2)
    return int(n * {'B':1,'KB':10**3,'MB':10**6,'GB':10**9,'TB':10**12}[unit])

rows = []
for line in sys.stdin:
    size, _, cmd = line.partition('\t')
    b = to_bytes(size)
    if b:
        rows.append((b, size.strip(), cmd.strip()[:70]))

rows.sort(reverse=True)
total = sum(r[0] for r in rows)
print(f'Слоёв с данными: {len(rows)}, суммарно {total/10**6:.1f} MB\n')
for b, s, cmd in rows:
    print(f'{s:>10}  {b*100/total:5.1f}%  {cmd}')
"
text
Слоёв с данными: 4, суммарно 184.4 MB

    87.4MB   47.4%  # debian.sh --arch 'amd64' out/ 'trixie' '@1785715200'
    51.9MB   28.1%  RUN /bin/sh -c pip install --no-cache-dir -r requirements.txt # buildk
    40.2MB   21.8%  RUN /bin/sh -c set -eux;   savedAptMark="$(apt-mark showmanual)";  apt
    4.94MB    2.7%  RUN /bin/sh -c set -eux;  apt-get update;  apt-get install -y --no-ins

Вывод для этого образа: 70 % размера — база Debian и установка самого интерпретатора, 28 % — зависимости. Оптимизировать нужно выбор базы, а не команды pip.

Числа сняты 2026-08-05; при следующей пересборке python:3.13-slim они изменятся. Смотреть нужно на доли, а не на абсолютные значения. Это ровно то решение, которое разбирается в разделе 06.

Фильтры и форматирование

bash
# только образы конкретного repository
docker images python

# образы без тега
docker images --filter 'dangling=true'

# созданные после указанного образа
docker images --filter 'since=python:3.13-slim'

# по метке
docker images --filter 'label=maintainer=course@example.com'

# только идентификаторы (для передачи в другие команды)
docker images -q

Полезные форматы:

bash
# компактная таблица с датой
docker images --format 'table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}'

# самые большие образы
docker images --format '{{.Size}}\t{{.Repository}}:{{.Tag}}' | sort -hr | head -10
text
1.24GB	tensorflow/tensorflow:latest
612MB	postgres:17
168MB	sizedemo:1
178MB	python:3.13-slim
97.2MB	debian:trixie-slim
8.31MB	alpine:3.21

Флаг sort -h понимает суффиксы KB, MB, GB — это то, что нужно.

Untagged против Deleted

bash
docker tag sizedemo:1 sizedemo:copy
docker rmi sizedemo:copy
text
Untagged: sizedemo:copy

Только Untagged — образ остался, у него есть другой тег.

bash
docker rmi sizedemo:1
text
Untagged: sizedemo:1
Deleted: sha256:7a6b5c4d3e2f19081726354453627180...
Deleted: sha256:1e0f9a8b7c6d5e4f3a2b190877665544...

Теперь и Untagged, и Deleted — тегов не осталось, образ и его уникальные слои удалены. Слои базового образа не удалены: на них ссылается python:3.13-slim.

Образ занят container

Разбор целиком: воспроизведение, чтение сообщения, поиск виновника, снятие блокировки.

bash
docker build -q -t sizedemo:2 . > /dev/null
docker run -d --name busy sizedemo:2 sleep 300 > /dev/null
docker rmi sizedemo:2
text
Error response from daemon: conflict: unable to delete sizedemo:2 (must be forced) - container a1b2c3d4e5f6 is using its referenced image 8f7e6d5c4b3a

Сообщение разбирается по частям:

ФрагментЧто означает
conflictКласс ошибки: запрошенное действие противоречит текущему состоянию. Это не сбой и не нехватка прав
unable to delete sizedemo:2То, что вы указали в команде. Здесь — ссылка (тег), а не образ
(must be forced)Отказ снимается флагом -f. Прежде чем его применять, дочитайте до следующего подраздела
container a1b2c3d4e5f6Усечённый до 12 символов ID мешающего container'а
its referenced image 8f7e6d5c4b3aID самого образа

Шаг 1. Посмотреть на container. Его идентификатор уже есть в сообщении — подставьте его в фильтр id (ниже он берётся из переменной, чтобы блок можно было выполнить как есть):

bash
CTR=$(docker ps -aq --filter ancestor=sizedemo:2)    # в жизни — ID из текста ошибки
docker ps -a --filter id="$CTR" --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}'
text
CONTAINER ID   NAMES   IMAGE         STATUS
a1b2c3d4e5f6   busy    sizedemo:2    Up 12 seconds

Флаг -a обязателен: без него команда ничего не найдёт, если container остановлен, — а в большинстве реальных случаев он именно остановлен.

Найти сразу все container'ы, держащие образ, удобнее фильтром ancestor:

bash
docker ps -a --filter ancestor=sizedemo:2 --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}'

Фильтр принимает и тег, и ID образа, и любой другой его тег. Две особенности стоит знать заранее:

  • container'ы, созданные из производных образов (собранных FROM sizedemo:2), этот фильтр не находит — проверено на стенде, хотя документация обещает обратное. Ищите такие образы отдельно;
  • в колонке IMAGE вместо имени может стоять ID. Это значит, что тега, из которого container был создан, больше нет: его переприсвоили или сняли.

Шаг 2. Решить судьбу container'а. Ответ на вопрос «нужен ли он» даёт docker ps -a: имя, статус и время выхода обычно достаточно, чтобы понять, это остаток эксперимента или чей-то рабочий стенд. Если container нужен — образ удалять нельзя, и правильный итог диагностики может быть «оставить как есть».

Шаг 3. Удалить container, затем образ.

bash
docker rm busy

Для работающего container'а docker rm откажет:

text
Error response from daemon: cannot remove container "busy": container is running: stop the container before removing or force remove

Тогда либо docker stop busy && docker rm busy, либо одной командой:

bash
docker rm -f busy

docker rm -f — это остановка (SIGKILL) плюс удаление. Флаг -f здесь безопасен по смыслу: он форсирует удаление container'а, что вы и хотите, — в отличие от docker rmi -f, который форсирует совсем другое.

bash
docker rmi sizedemo:2
text
Untagged: sizedemo:2
Deleted: sha256:8f7e6d5c4b3a...

Обе строки — образ действительно удалён.

Если мешающих container'ов много и все они мусорные, помогает адресная уборка вместо ручного перебора:

bash
docker ps -aq --filter ancestor=sizedemo:2 | xargs -r docker rm -f
docker rmi sizedemo:2

docker container prune удаляет все остановленные container'ы машины, а не только мешающие. Для снятия одной блокировки это слишком широкий инструмент.

Почему -f не решение

Соблазн очевиден: сообщение само упоминает forced. Вот что происходит на самом деле.

bash
docker build -q -t sizedemo:2 . > /dev/null
docker run -d --name busy sizedemo:2 sleep 300 > /dev/null
IMG=$(docker images -q sizedemo:2)    # запомним ID: после снятия тега имени не останется
docker rmi -f sizedemo:2
text
Untagged: sizedemo:2

Строки Deleted нет. Тег снят, образ остался на диске без имени:

bash
docker images --filter 'dangling=true' --format '{{.ID}} {{.Repository}}:{{.Tag}} {{.Size}}'
text
8f7e6d5c4b3a <none>:<none> 168MB

Обычный docker images его не покажет: в Docker 29 образы без тега скрыты, пока не указан --all или фильтр dangling=true.

Итог хуже исходного состояния по трём причинам:

  1. Место не освободилось. Ровно та задача, ради которой обычно и удаляют образ, не решена.
  2. Образ потерял имя. Теперь непонятно, что это за <none> и можно ли его трогать.
  3. Очистка его не заберёт. Пока container существует, образ остаётся используемым:
bash
docker image prune -f
text
Total reclaimed space: 0B

Освободить место всё равно можно только через удаление container'а — то есть через тот шаг, который -f позволил пропустить.

Отдельный случай — docker rmi -f по ID образа при остановленном container'е. Здесь образ действительно удаляется:

bash
docker stop busy > /dev/null
docker rmi -f "$IMG"
text
Deleted: sha256:8f7e6d5c4b3a...

Container при этом остаётся работоспособным: его файловая система уже развёрнута снапшоттером, и docker start поднимает его как ни в чём не бывало (проверено). Но образа, из которого он создан, больше нет — воспроизвести container на другой машине или пересоздать его после docker rm уже не из чего. Так теряют единственную копию образа, которого нет в registry.

Правило: docker rmi -f уместен только тогда, когда вы удаляете мусор и заранее знаете, что container'ов быть не должно. Как реакция на conflict он неверен всегда.

Накопление dangling images

Воспроизведём главный источник мусора — пересборку с одним тегом:

bash
for i in 1 2 3; do
    echo "print('сборка $i')" > app.py
    docker build -q -t rebuilt:dev . > /dev/null
done

docker images --filter 'dangling=true' --format 'table {{.ID}}\t{{.Size}}\t{{.CreatedSince}}'
text
IMAGE ID       SIZE      CREATED
3e2f1908a7b6   168MB     10 seconds ago
2f1908a7b6c5   168MB     18 seconds ago

Две сборки потеряли тег. Колонка SIZE показывает виртуальный размер — фактически уникальных данных там килобайты, но записи копятся, и при активной разработке их становятся сотни.

Сколько места они реально занимают:

bash
docker system df -v 2>/dev/null | grep -A5 'Images space usage' | head -6

Безопасное удаление именно dangling:

bash
docker image prune
text
WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] y
Deleted Images:
deleted: sha256:3e2f1908a7b6...
deleted: sha256:2f1908a7b6c5...

Total reclaimed space: 62.14kB

docker image prune без флагов удаляет только образы без тега. Это самая безопасная из команд очистки: удаляется то, на что и так ничто не ссылается.

Предварительный просмотр перед удалением

Ни одна команда prune не имеет флага dry-run. Поэтому смотреть нужно заранее, отдельными командами.

bash
echo "=== Будет удалено при docker image prune ==="
docker images --filter 'dangling=true' --format '{{.ID}}\t{{.Size}}'

echo
echo "=== Дополнительно удалится при docker image prune -a ==="
docker images --format '{{.Repository}}:{{.Tag}}\t{{.ID}}' | while IFS=$'\t' read -r name id; do
    if ! docker ps -aq --filter "ancestor=$id" | grep -q .; then
        echo "  $name"
    fi
done

Второй блок перечисляет образы с тегами, не используемые ни одним container. Именно их удалит -a, и именно здесь чаще всего теряют нужное.

Опасные варианты очистки

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

КомандаЧто удаляетВосстановимо?
docker image pruneОбразы без тегаПересборкой или загрузкой из registry
docker image prune -aВсе образы, не используемые container'ами, включая теговыеТолько если они есть в registry
docker container pruneВсе остановленные containersНет: writable layers теряются
docker volume pruneVolumes, не примонтированные ни к одному containerНет. Данные пропадают безвозвратно
docker builder pruneКэш сборкиВосстанавливается пересборкой (медленно)
docker system pruneОстановленные containers, неиспользуемые сети, dangling images, build cacheЧастично
docker system prune -a --volumesВсё перечисленное плюс все неиспользуемые образы и все неиспользуемые volumesНет для volumes

Самая опасная — последняя. Типичный сценарий потери данных: разработчик остановил стек на ночь через docker compose down, утром выполнил docker system prune -a --volumes для освобождения места — и вместе с мусором удалил volume с базой данных, потому что она была не запущена и потому «не используется».

Безопасная последовательность:

bash
# 1. Посмотреть, что есть
docker system df

# 2. Убедиться, что важные volumes не пропадут
docker volume ls
docker volume ls --filter dangling=true    # именно эти удалит volume prune

# 3. Начать с безопасного.
#    Обе команды спрашивают подтверждение [y/N] — это здесь к месту.
#    Для скриптов добавьте -f, но тогда и проверять придётся заранее.
docker image prune
docker builder prune

# 4. Проверить результат
docker system df

В подавляющем большинстве случаев шага 3 достаточно: build cache и dangling images дают основной объём.

Очистка build cache

bash
docker builder prune -f --filter 'until=168h'

Удаляет записи кэша старше недели. Флаг --filter until= доступен и у image prune, и у system prune — это самый практичный способ чистить регулярно, не теряя актуального.

Ограничить рост кэша заранее:

bash
docker builder prune -f --reserved-space 10GB

Флаг назывался --keep-storage; в Docker 29 старое имя ещё принимается, но печатает Flag --keep-storage has been deprecated.

Постоянное ограничение задаётся в конфигурации BuildKit; подробно — в разделе 13.

Уборка после урока

bash
docker rm -f busy 2>/dev/null || true
docker rmi rebuilt:dev sizedemo:1 sizedemo:2 2>/dev/null || true
docker image prune -f
rm -rf /tmp/mgmt
docker system df

Практическое упражнение

Задание. Напишите скрипт docker-space.sh, который выполняет безопасный аудит дискового пространства Docker.

Скрипт должен:

  1. Показать общую картину по четырём категориям с указанием освобождаемого объёма.
  2. Перечислить пять самых больших образов.
  3. Отдельно показать dangling images: количество и объём.
  4. Перечислить образы с тегами, не используемые ни одним container, — то есть то, что удалит prune -a.
  5. Показать volumes, не примонтированные ни к одному container, с явным предупреждением о невосстановимости.
  6. Вывести рекомендованную последовательность команд очистки, отсортированную по возрастанию риска.

Скрипт не должен ничего удалять.

Подсказки

Подсказка 1

docker system df --format '{{json .}}' даёт структурированный вывод, который проще разбирать, чем таблицу.

Подсказка 2

Образ используется, если существует хотя бы один container (в любом состоянии) с этим ancestor:

bash
docker ps -aq --filter "ancestor=<id>"
Подсказка 3

Volumes без container'ов отбираются фильтром dangling=true:

bash
docker volume ls --filter dangling=true -q

Решение

Сначала выполните задание самостоятельно.

Показать решение
bash
#!/usr/bin/env bash
# docker-space.sh — безопасный аудит дискового пространства Docker.
# Ничего не удаляет.
set -uo pipefail

docker info > /dev/null 2>&1 || { echo "Нет связи с Docker daemon." >&2; exit 1; }

echo "═══ 1. Общая картина ═══"
docker system df | sed 's/^/  /'

echo
echo "═══ 2. Пять самых больших образов ═══"
docker images --format '{{.Size}}\t{{.Repository}}:{{.Tag}}' \
  | grep -v '<none>:<none>' \
  | sort -hr | head -5 \
  | awk -F'\t' '{printf "  %-10s %s\n", $1, $2}'

echo
echo "═══ 3. Dangling images (без тега) ═══"
dangling_ids="$(docker images --filter 'dangling=true' -q)"
if [ -z "$dangling_ids" ]; then
    echo "  нет"
else
    count="$(echo "$dangling_ids" | grep -c .)"
    echo "  штук: $count"
    docker images --filter 'dangling=true' \
        --format '  {{.ID}}  {{.Size}}  создан {{.CreatedSince}}' | head -10
    [ "$count" -gt 10 ] && echo "  ... и ещё $((count - 10))"
    echo
    echo "  Безопасно удаляются: docker image prune"
fi

echo
echo "═══ 4. Образы с тегами, не используемые container'ами ═══"
echo "  (именно это дополнительно удалит 'docker image prune -a')"
unused=0
while IFS=$'\t' read -r name id size; do
    [ "$name" = "<none>:<none>" ] && continue
    if ! docker ps -aq --filter "ancestor=$id" 2>/dev/null | grep -q .; then
        printf '  %-46s %s\n' "$name" "$size"
        unused=$((unused + 1))
    fi
done < <(docker images --format '{{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}')
[ "$unused" -eq 0 ] && echo "  нет"

echo
echo "═══ 5. Volumes без container'ов ═══"
vols="$(docker volume ls --filter dangling=true -q)"
if [ -z "$vols" ]; then
    echo "  нет"
else
    echo "$vols" | while read -r v; do
        [ -z "$v" ] && continue
        mp="$(docker volume inspect "$v" --format '{{.Mountpoint}}' 2>/dev/null)"
        sz="$(sudo du -sh "$mp" 2>/dev/null | cut -f1)"
        printf '  %-44s %s\n' "$v" "${sz:-?}"
    done
    echo
    echo "  ВНИМАНИЕ: 'docker volume prune' удалит их БЕЗВОЗВРАТНО."
    echo "  Остановленный стек не значит «ненужный»: база данных"
    echo "  после 'compose down' попадает именно в этот список."
fi

echo
echo "═══ 6. Рекомендуемый порядок очистки (по возрастанию риска) ═══"
cat <<'EOF'
  1. docker builder prune --filter 'until=168h'
       кэш сборки старше недели. Риск: замедление следующей сборки.

  2. docker image prune
       только образы без тега. Риск: минимальный.

  3. docker container prune
       остановленные containers. Риск: теряются их writable layers.
       Проверьте: docker ps -a

  4. docker image prune -a
       все образы без container'ов, включая теговые.
       Риск: придётся заново загружать или собирать. См. пункт 4 выше.

  5. docker volume prune
       ОПАСНО И НЕОБРАТИМО. Сначала сделайте backup нужных volumes.

  Команду 'docker system prune -a --volumes' не используйте:
  она объединяет все шаги, включая самый опасный, без разбора.
EOF

Запуск:

bash
chmod +x docker-space.sh
./docker-space.sh

Ожидаемый вывод (сокращённо):

text
═══ 1. Общая картина ═══
  TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
  Images          24        6         8.412GB   5.903GB (70%)
  Containers      11        3         184.2MB   131.7MB (71%)
  Local Volumes   7         3         2.104GB   1.312GB (62%)
  Build Cache     183       0         4.271GB   4.271GB

═══ 2. Пять самых больших образов ═══
  1.24GB     tensorflow/tensorflow:latest
  612MB      postgres:17
  168MB      sizedemo:1
  178MB      python:3.13-slim
  97.2MB     debian:trixie-slim

═══ 3. Dangling images (без тега) ═══
  штук: 2
  3e2f1908a7b6  168MB  создан 10 seconds ago
  2f1908a7b6c5  168MB  создан 18 seconds ago

  Безопасно удаляются: docker image prune

═══ 4. Образы с тегами, не используемые container'ами ═══
  (именно это дополнительно удалит 'docker image prune -a')
  tensorflow/tensorflow:latest                   1.24GB
  debian:trixie-slim                             97.2MB

═══ 5. Volumes без container'ов ═══
  myproject_pgdata                             612M

  ВНИМАНИЕ: 'docker volume prune' удалит их БЕЗВОЗВРАТНО.
  ...

Что важно в этом решении.

Пункт 5 — главная его ценность. Volume myproject_pgdata выглядит «неиспользуемым» просто потому, что стек остановлен. Автоматическая очистка удалит базу данных. Скрипт показывает это явно, до того как что-то произойдёт.

Пункт 4 отвечает на вопрос, который обычно задают уже после prune -a: «а что именно удалилось?». Список удобно просмотреть заранее и решить, готовы ли вы заново загружать эти образы.

Порядок в пункте 6 не случаен: build cache восстанавливается сам, образы — из registry, а volumes не восстанавливаются никак.

Проверка результата

bash
docker system df
docker images --filter 'dangling=true' -q | wc -l

Убедитесь, что можете ответить: какая категория занимает у вас больше всего места и какая команда освободит её с наименьшим риском.

Типичные ошибки

ОшибкаПричинаИсправление
Складывать SIZE из docker imagesКажется, что это фактический объёмДедупликация слоёв; использовать docker system df
Чистить образы, когда место занял build cacheНачинают с самого заметногоНачинать с docker system df и смотреть, где объём
docker rmi -f при занятом образеБыстрый способ обойти ошибкуУдалить container; -f оставит образ на диске без тега
Читать (must be forced) как совет применить -fСкобка выглядит как подсказкаЭто описание механики отказа. Правильный ход — удалить container
Искать мешающий container через docker psКажется, что «занимать» образ может только запущенныйСсылку держит любой container; смотреть с -a
Пытаться снять (cannot be forced) флагом -fФлаг помог в прошлый разЭто другой отказ: образ у работающего container'а. Сначала docker stop или docker rm -f
docker container prune, чтобы удалить один мешающий containerКоманда под рукойОна удалит все остановленные container'ы машины; удалять нужно адресно по ID
docker system prune -a --volumes для освобождения местаОдна команда вместо разбораVolumes невосстановимы. Чистить по шагам, начиная с безопасного
Считать остановленный volume ненужным«Не используется» ≠ «не нужен»После compose down база данных попадает в dangling
Путать Untagged и DeletedОбе строки в одном выводеUntagged — снят тег; Deleted — удалён образ
Удивляться <missing> в docker historyВыглядит как ошибкаПромежуточные образы существуют только на машине сборки
Ожидать флаг dry-run у pruneОн был бы логиченЕго нет; смотреть заранее через images --filter и volume ls

Контрольные вопросы

На понимание:

  1. Почему сумма SIZE всех образов больше, чем показывает docker system df?
  2. Откуда берутся образы <none>:<none> и всегда ли их можно удалять?
  3. Чем Untagged отличается от Deleted в выводе docker rmi?
  4. Почему удаление образа на 1.5 GB может освободить всего 40 MB?
  5. Что означает <missing> в колонке IMAGE вывода docker history?

На применение:

  1. Как найти слой, дающий основной вклад в размер образа?
  2. Как узнать, какой container мешает удалить образ?
  3. Как посмотреть, что именно удалит docker volume prune, до его запуска?

На диагностику:

  1. Диск заполнен, docker images показывает 3 GB, но Docker занимает 40 GB. Где искать остальное?
  2. После docker system prune -a --volumes пропали данные PostgreSQL, хотя стек был просто остановлен. Что произошло и как было бы правильно?
  3. docker rmi alpine:latest отказал с conflict: … (must be forced), а docker ps не показывает ни одного container'а. Где искать виновника и как снять блокировку?
  4. Чем (must be forced) отличается от (cannot be forced) и почему одна и та же ситуация даёт разный текст при удалении по тегу и по ID?

Краткое резюме

  1. Диагностика места начинается с docker system df, а не с docker images.
  2. Место расходуется по четырём независимым категориям; чаще всего лидируют build cache и containers.
  3. Сумма SIZE не равна занятому месту из-за дедупликации слоёв.
  4. Образы <none> появляются при перепривязке тега, пересборке и загрузке по digest.
  5. docker image prune без флагов — самая безопасная очистка: удаляет только образы без тега.
  6. -a дополнительно удаляет теговые образы без container'ов — здесь чаще всего теряют нужное.
  7. Ошибка conflict: unable to delete … (must be forced) означает, что на образ ссылается container — чаще всего давно остановленный и невидимый без docker ps -a. Лечится удалением container'а.
  8. (cannot be forced) — отдельный случай: образ занят работающим container'ом, и никакой флаг этого не изменит.
  9. docker rmi -f при занятом образе не освобождает место, а создаёт <none>.
  10. docker volume prune необратим; остановленный стек попадает в список dangling.
  11. У команд prune нет dry-run — проверять нужно заранее отдельными командами.
  12. В Docker Engine 29 вывод docker images стал древовидным, образы без тега скрыты без --all, добавлены колонки DISK USAGE и CONTENT SIZE.

Официальные источники

ИсточникСсылкаЧто подтверждает
docker images referencehttps://docs.docker.com/reference/cli/docker/image/ls/Фильтры dangling, since, label, форматирование вывода
docker history referencehttps://docs.docker.com/reference/cli/docker/image/history/Формат вывода, значение <missing>
docker rmi referencehttps://docs.docker.com/reference/cli/docker/image/rm/Поведение при нескольких тегах, конфликт с containers, флаг -f
docker image prunehttps://docs.docker.com/reference/cli/docker/image/prune/Различие между удалением dangling и -a, фильтр until
docker system prunehttps://docs.docker.com/reference/cli/docker/system/prune/Что именно удаляется, флаги -a и --volumes
docker system dfhttps://docs.docker.com/reference/cli/docker/system/df/Категории, колонка RECLAIMABLE, флаг -v
docker builder prunehttps://docs.docker.com/reference/cli/docker/builder/prune/Очистка кэша сборки, --reserved-space (бывш. --keep-storage), фильтры
Docker Engine 29 release noteshttps://docs.docker.com/engine/release-notes/29/Древовидный вывод docker images, скрытие untagged без --all, отказ от усечения имён

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Save, load, export, import
Главное оглавление

Markdown на GitHub ↗