Раздел 3. Практические задания
Задания выполняются в реальной системе. Разбор открывайте только после самостоятельной попытки.
Обозначения: [обяз.] — обязательное, [доп.] — дополнительное, [★] — повышенной сложности, [диаг.] — диагностическое.
Подготовка:
mkdir -p ~/docker-course/03-images
cd ~/docker-course/03-images
docker system df # зафиксируйте исходное состояние
Задание 1. Анатомия образа [обяз.]
Постановка. Загрузите python:3.13-slim. Определите:
- Число слоёв.
- Размер каждого слоя и самый большой из них.
- Долю самого большого слоя в общем размере.
- Число шагов истории и сколько из них создали слой.
Ожидаемый результат. Таблица слоёв, отсортированная по убыванию, с процентами.
Проверка:
docker image inspect python:3.13-slim --format 'слоёв: {{len .RootFS.Layers}}'
docker history python:3.13-slim --format '{{.Size}}' | grep -vc '^0B$'
Разбор
docker pull -q python:3.13-slim
docker history python:3.13-slim --no-trunc --format '{{.Size}}\t{{.CreatedBy}}' \
| python3 -c "
import sys, re
def to_bytes(s):
m = re.match(r'^([\d.]+)\s*([KMGT]?B)\$', s.strip())
if not m: return 0
return int(float(m.group(1)) * {'B':1,'KB':10**3,'MB':10**6,'GB':10**9,'TB':10**12}[m.group(2)])
rows, empty = [], 0
for line in sys.stdin:
size, _, cmd = line.partition('\t')
b = to_bytes(size)
if b: rows.append((b, size.strip(), cmd.strip()[:62]))
else: empty += 1
rows.sort(reverse=True)
total = sum(r[0] for r in rows)
print(f'шагов истории всего: {len(rows) + empty}')
print(f'создали слой: {len(rows)}')
print(f'только конфигурация: {empty}')
print(f'суммарный размер: {total/10**6:.1f} MB\n')
for b, s, cmd in rows:
print(f'{s:>9} {b*100/total:5.1f}% {cmd}')
"
шагов истории всего: 9
создали слой: 3
только конфигурация: 6
суммарный размер: 132.5 MB
87.4MB 65.9% # debian.sh --arch 'amd64' out/ 'trixie' '@1785715200'
40.2MB 30.3% RUN /bin/sh -c set -eux; savedAptMark="$(apt-mark showmanual
4.94MB 3.7% RUN /bin/sh -c set -eux; apt-get update; apt-get install -y
Числа у вас будут другими: образ пересобирается, и с каждой пересборкой меняются и число шагов, и размеры. Приведённые сняты 2026-08-05 с python:3.13-slim на Debian trixie. Устойчиво другое — соотношение.
Главный вывод. Две трети размера — базовый слой Debian, а не сам Python; ещё треть — установка самого интерпретатора. Отсюда следует, что для уменьшения этого образа нужно менять базу, а не оптимизировать команды pip. Это ровно то решение, которое разбирается в разделе 06.
Шесть шагов истории из девяти не создали слоёв: ENV, CMD, WORKDIR изменяют только конфигурацию.
Задание 2. Tag против digest [обяз.]
Постановка. Докажите, что digest не меняется, а тег может быть перепривязан.
- Загрузите образ по тегу, запишите digest.
- Удалите образ локально.
- Загрузите его по digest.
- Убедитесь, что получили то же самое.
- Объясните, почему после шага 3 у образа тег
<none>.
Ожидаемый результат. Совпадение Image ID до и после и объяснение отсутствия тега.
Проверка:
docker images --digests python
Разбор
docker pull -q python:3.13-slim
ID1="$(docker image inspect python:3.13-slim --format '{{.Id}}')"
DIGEST="$(docker image inspect python:3.13-slim --format '{{index .RepoDigests 0}}' | cut -d@ -f2)"
echo "Image ID: $ID1"
echo "digest: $DIGEST"
docker rmi python:3.13-slim > /dev/null
docker pull -q "python@$DIGEST" > /dev/null
ID2="$(docker image inspect "python@$DIGEST" --format '{{.Id}}')"
echo "после загрузки по digest: $ID2"
[ "$ID1" = "$ID2" ] && echo "образ идентичен"
docker images --digests python --format 'table {{.Tag}}\t{{.Digest}}'
Image ID: sha256:c2f1e0d9b8a7c6d5...
digest: sha256:bf503bb2243c5aad...
после загрузки по digest: sha256:c2f1e0d9b8a7c6d5...
образ идентичен
TAG DIGEST
<none> sha256:bf503bb2243c5aad...
Почему тег <none>. Тег — это отдельная локальная запись «имя → образ». При загрузке по digest вы не называли тега, значит и записи нет. Образ полностью работоспособен:
docker run --rm "python@$DIGEST" python --version
Присвоить имя можно вручную:
docker tag "python@$DIGEST" python:3.13-slim
Практическое применение. Именно так работает развёртывание по digest: в production запускают myapp@sha256:..., а тег остаётся справочной информацией в системе сборки. Подробнее — в разделе 14.
Задание 3. Writable layer [обяз.]
Постановка. Создайте container, измените в нём файл, унаследованный из образа, и найдите изменённую копию на host.
- Запустите container из
python:3.13-slim. - Измените файл
/etc/hostnameвнутри container. - Найдите его копию в writable layer на host.
- Убедитесь, что в слое образа оригинал не изменился.
- Удалите файл в container и найдите whiteout-маркер.
Ожидаемый результат. Путь к файлу в upperdir и вывод ls -l для whiteout.
Подсказка
UpperDir доступен через docker inspect <container> --format '{{.GraphDriver.Data.UpperDir}}'. При containerd image store поле может быть пустым — тогда берите upperdir из /proc/<pid>/mountinfo.
Разбор
docker run -d --name cow python:3.13-slim sleep 300 > /dev/null
UPPER="$(docker inspect cow --format '{{.GraphDriver.Data.UpperDir}}')"
if [ -z "$UPPER" ]; then
CPID="$(docker inspect cow --format '{{.State.Pid}}')"
UPPER="$(sudo grep -m1 ' / overlay ' /proc/"$CPID"/mountinfo \
| tr ',' '\n' | awk -F= '/^upperdir/{print $2}')"
fi
echo "upperdir: $UPPER"
echo "--- до изменения: upperdir пуст ---"
sudo ls -A "$UPPER" 2>/dev/null || echo "(пусто)"
echo "--- изменяем файл из образа ---"
docker exec cow sh -c 'echo "# изменено" >> /etc/hostname'
sudo ls -l "$UPPER/etc/hostname"
echo "--- оригинал в слое образа не тронут ---"
docker run --rm python:3.13-slim cat /etc/hostname
echo "--- удаляем файл ---"
docker exec cow rm /etc/hostname
sudo ls -l "$UPPER/etc/hostname"
docker rm -f cow > /dev/null
upperdir: /var/lib/docker/overlay2/9f8e.../diff
--- до изменения: upperdir пуст ---
(пусто)
--- изменяем файл из образа ---
-rw-r--r-- 1 root root 25 Jul 29 16:55 /var/lib/docker/overlay2/9f8e.../diff/etc/hostname
--- оригинал в слое образа не тронут ---
c8f4a1b2d3e5
--- удаляем файл ---
c--------- 1 root root 0, 0 Jul 29 16:56 /var/lib/docker/overlay2/9f8e.../diff/etc/hostname
Что здесь произошло.
Шаг 1: upperdir пуст — container ничего не писал, все файлы видны из слоёв образа.
Шаг 2: файла не было в upperdir, но мы его изменили — сработал copy-up. Файл скопирован из нижнего слоя целиком, затем изменён.
Шаг 3: оригинал в образе не тронут. Любой новый container из того же образа получит исходную версию.
Шаг 4: после rm файл превратился в whiteout — character device 0:0 нулевого размера. Данные в нижнем слое остались, просто скрыты.
Механизм разбирается в уроке 3.2.
Задание 4. Перенос образа [обяз.]
Постановка. Перенесите образ через архив и подтвердите идентичность.
- Сохраните образ в сжатый архив.
- Удалите его локально.
- Восстановите из архива.
- Докажите, что образ идентичен исходному.
- Сравните размер архива до и после сжатия, объясните разницу.
Ожидаемый результат. Совпадение Image ID и объяснение коэффициента сжатия.
Проверка:
docker save alpine:3.21 | gzip | wc -c
docker save alpine:3.21 | wc -c
Разбор
docker pull -q alpine:3.21
ID_BEFORE="$(docker image inspect alpine:3.21 --format '{{.Id}}')"
docker save -o alpine.tar alpine:3.21
gzip -k alpine.tar
ls -lh alpine.tar alpine.tar.gz
docker rmi alpine:3.21 > /dev/null
docker load -i alpine.tar.gz
ID_AFTER="$(docker image inspect alpine:3.21 --format '{{.Id}}')"
echo "до: $ID_BEFORE"
echo "после: $ID_AFTER"
[ "$ID_BEFORE" = "$ID_AFTER" ] && echo "идентично"
rm -f alpine.tar alpine.tar.gz
-rw------- 1 evg evg 8.5M Jul 29 17:02 alpine.tar
-rw------- 1 evg evg 3.4M Jul 29 17:02 alpine.tar.gz
Loaded image: alpine:3.21
до: sha256:b0c1d2e3f4a5...
после: sha256:b0c1d2e3f4a5...
идентично
Почему архив сжимается втрое. Внутри docker save слои хранятся распакованными — как обычные tar-архивы. При загрузке из registry слои приходят сжатыми (gzip), а на диске лежат распакованными; docker save берёт их с диска.
Поэтому при передаче по сети архив нужно сжимать явно:
docker save myapp:1.0 | gzip | ssh remote 'gunzip | docker load'
Почему Image ID совпадает. Image ID — хэш конфигурации образа (урок 3.1), а конфигурация содержит diff_ids всех слоёв. Совпадение ID означает, что и конфигурация, и все слои идентичны. Это надёжнее сравнения размеров.
Задание 5. Слой-виновник размера [доп.]
Постановка. Соберите образ, где временный файл создаётся в одной инструкции и удаляется в другой. Найдите слой, в котором остались данные, и измерьте разницу с правильным вариантом.
Ожидаемый результат. Два образа с разницей в размере и указание слоя с «удалёнными» данными.
Разбор
mkdir -p /tmp/ex5 && cd /tmp/ex5
cat > Dockerfile.bad <<'EOF'
FROM debian:trixie-slim
RUN dd if=/dev/zero of=/tmp/junk bs=1M count=180 status=none
RUN rm /tmp/junk
EOF
cat > Dockerfile.good <<'EOF'
FROM debian:trixie-slim
RUN dd if=/dev/zero of=/tmp/junk bs=1M count=180 status=none && rm /tmp/junk
EOF
docker build -q -f Dockerfile.bad -t ex5:bad . > /dev/null
docker build -q -f Dockerfile.good -t ex5:good . > /dev/null
docker images --format 'table {{.Repository}}:{{.Tag}}\t{{.Size}}' \
| grep -E 'ex5|debian:trixie-slim'
echo
echo "--- где остались данные в ex5:bad ---"
docker history ex5:bad --format '{{.Size}}\t{{.CreatedBy}}' | head -3
REPOSITORY:TAG SIZE
ex5:good 97.2MB
ex5:bad 286MB
debian:trixie-slim 97.2MB
--- где остались данные в ex5:bad ---
0B RUN /bin/sh -c rm /tmp/junk # buildkit
189MB RUN /bin/sh -c dd if=/dev/zero of=/tmp/junk bs=1M count=180 status=none # buil…
97.2MB /bin/sh -c #(nop) ADD file:... in /
Результат. ex5:good совпал по размеру с базовым образом — временный файл не оставил следа. ex5:bad больше на 189 MB, и эти мегабайты будут скачиваться при каждом docker pull.
Слой удаления имеет размер 0B: он содержит только whiteout-маркер.
Убедимся, что файла действительно нет в обоих:
docker run --rm ex5:bad ls /tmp/junk 2>&1 | tail -1
docker run --rm ex5:good ls /tmp/junk 2>&1 | tail -1
ls: cannot access '/tmp/junk': No such file or directory
ls: cannot access '/tmp/junk': No such file or directory
Функционально образы идентичны. Разница только в 189 MB, которые никому не нужны и которые невозможно удалить постфактум.
docker rmi ex5:bad ex5:good > /dev/null
cd /tmp && rm -rf /tmp/ex5
Задание 6. save против export [доп.]
Постановка. Экспортируйте один и тот же образ двумя способами и составьте таблицу того, что сохранилось, а что потерялось.
Сравните минимум по пяти параметрам: число слоёв, Cmd, число переменных Env, записей истории, возможность запуска без указания команды.
Разбор
mkdir -p /tmp/ex6 && cd /tmp/ex6
docker run -d --name src python:3.13-slim sleep 300 > /dev/null
docker save -o via-save.tar python:3.13-slim
docker export -o via-export.tar src
docker import via-export.tar flat:1 > /dev/null
python3 - <<'PY'
import subprocess
def q(img, fmt):
return subprocess.check_output(
['docker','image','inspect',img,'--format',fmt], text=True).strip()
def hist(img):
return subprocess.check_output(
['docker','history','-q',img], text=True).strip().count('\n') + 1
rows = [
('слоёв', '{{len .RootFS.Layers}}'),
('Cmd', '{{.Config.Cmd}}'),
('Entrypoint', '{{.Config.Entrypoint}}'),
('Env, переменных', '{{len .Config.Env}}'),
('WorkingDir', '{{.Config.WorkingDir}}'),
]
print(f"{'ПАРАМЕТР':<20} {'ИСХОДНЫЙ':<28} {'ПОСЛЕ import':<28}")
print('-' * 78)
for name, fmt in rows:
print(f"{name:<20} {q('python:3.13-slim', fmt):<28} {q('flat:1', fmt):<28}")
print(f"{'записей истории':<20} {hist('python:3.13-slim'):<28} {hist('flat:1'):<28}")
PY
echo
echo "--- размеры архивов ---"
ls -lh via-save.tar via-export.tar | awk '{print " " $9 ": " $5}'
echo
echo "--- запуск без указания команды ---"
echo -n "исходный: "; docker run --rm python:3.13-slim python --version
echo -n "после import: "; docker run --rm flat:1 2>&1 | tail -1
docker rm -f src > /dev/null; docker rmi flat:1 > /dev/null
cd /tmp && rm -rf /tmp/ex6
ПАРАМЕТР ИСХОДНЫЙ ПОСЛЕ import
------------------------------------------------------------------------------
слоёв 3 1
Cmd [python3] []
Entrypoint [] []
Env, переменных 4 1
WorkingDir /
записей истории 12 1
--- размеры архивов ---
via-save.tar: 128M
via-export.tar: 126M
--- запуск без указания команды ---
исходный: Python 3.13.9
после import: docker: Error response from daemon: No command specified
Итоговая таблица.
| Параметр | save/load | export/import |
|---|---|---|
| Слои | 3 → 3 | 3 → 1 |
Cmd | сохранён | потерян |
Env | 4 → 4 | 4 → 1 |
| История | 12 → 12 | 12 → 1 |
| Запуск без команды | работает | ошибка |
Единственная оставшаяся переменная Env после import — это PATH, которую Docker подставляет по умолчанию, а не унаследованная из образа.
Вывод. Для переноса образа export/import непригодны. Их область — схлопывание слоёв и снятие среза файловой системы, и в обоих случаях конфигурацию приходится восстанавливать вручную через --change.
Задание 7. Извлечение секрета из слоя [★]
Постановка. Соберите образ, в котором секрет копируется, используется и удаляется следующей инструкцией. Затем извлеките секрет из образа, не запуская container.
Объясните, почему это возможно, и назовите правильное решение.
Задание выполняется на собственной учебной машине с заведомо ненастоящим секретом.
Подсказка 1
Слои внутри docker save — обычные tar-архивы. tar -tf покажет их содержимое.
Подсказка 2
Проверить наличие секрета в метаданных можно быстрее — через docker history --no-trunc.
Разбор
mkdir -p /tmp/ex7 && cd /tmp/ex7
echo "DB_PASSWORD=nEver-Use-Th1s-1n-Real-Life" > secret.env
cat > Dockerfile <<'EOF'
FROM alpine:3.21
COPY secret.env /build/secret.env
RUN . /build/secret.env && echo "используем пароль длиной ${#DB_PASSWORD}" \
&& rm /build/secret.env
EOF
docker build -q -t leaky:1 . > /dev/null
echo "=== 1. В запущенном container файла нет ==="
docker run --rm leaky:1 ls /build/secret.env 2>&1 | tail -1
echo
echo "=== 2. Извлекаем из слоёв ==="
docker save leaky:1 -o leaky.tar
mkdir -p x && tar -xf leaky.tar -C x
found=0
for b in x/blobs/sha256/*; do
if tar -tf "$b" 2>/dev/null | grep -q 'build/secret.env'; then
echo " найден в blob $(basename "$b" | cut -c1-16)..."
echo -n " содержимое: "
tar -xOf "$b" build/secret.env 2>/dev/null
found=1
fi
done
[ "$found" -eq 0 ] && echo " не найден (проверьте формат архива)"
echo
echo "=== 3. Тот же результат без распаковки ==="
docker run --rm -v "$PWD/leaky.tar:/img.tar:ro" alpine:3.21 sh -c '
mkdir -p /x && tar -xf /img.tar -C /x
for b in /x/blobs/sha256/*; do
tar -xOf "$b" build/secret.env 2>/dev/null && break
done
'
cd /tmp && rm -rf /tmp/ex7
docker rmi leaky:1 > /dev/null
=== 1. В запущенном container файла нет ===
ls: /build/secret.env: No such file or directory
=== 2. Извлекаем из слоёв ===
найден в blob 3c4d5e6f7a8b9c0d...
содержимое: DB_PASSWORD=nEver-Use-Th1s-1n-Real-Life
=== 3. Тот же результат без распаковки ===
DB_PASSWORD=nEver-Use-Th1s-1n-Real-Life
Почему это возможно. Инструкция COPY создала слой, содержащий файл. Инструкция RUN ... rm создала следующий слой с whiteout-маркером. Первый слой никуда не делся — он остаётся частью образа, передаётся при docker push и docker pull и доступен любому, у кого есть образ.
Из merged-вида файл исчез. Из образа — нет.
Ещё один канал утечки. Проверьте историю:
docker history --no-trunc leaky:1 | grep -i 'secret\|password' || echo "в истории чисто"
Если бы секрет передавался через ARG или напрямую в RUN, он был бы виден и здесь — без всякой распаковки.
Правильные решения.
| Способ | Как работает | Где разбирается |
|---|---|---|
| BuildKit secret mount | Секрет монтируется только на время выполнения RUN и не попадает ни в один слой | раздел 05 |
| Multi-stage build | Секрет используется в стадии сборки, в финальный образ копируется только результат | раздел 05 |
| Передача при запуске | Секрет вообще не участвует в сборке: файл или переменная окружения при docker run | раздел 11 |
Правильный вариант с secret mount:
# syntax=docker/dockerfile:1
FROM alpine:3.21
RUN --mount=type=secret,id=dbpass \
. /run/secrets/dbpass && echo "длина: ${#DB_PASSWORD}"
docker build --secret id=dbpass,src=secret.env -t safe:1 .
Секрет доступен внутри RUN, но не попадает ни в слой, ни в историю.
Что делать, если утечка уже произошла. Пересборка образа не помогает: старый образ мог быть опубликован и загружен. Единственное надёжное действие — ротация секрета. Считайте скомпрометированным любой секрет, попавший в слой опубликованного образа.
Задание 8. Диагностика: расхождение размеров [диаг.]
Постановка. На машине docker images показывает суммарно около 12 GB, а docker system df — 7 GB занятого места. При этом диск заполнен на 90 %, и Docker занимает 45 GB.
Объясните оба расхождения и составьте план освобождения места с оценкой риска каждого шага.
Разбор
Расхождение 1: 12 GB против 7 GB.
Сумма колонки SIZE в docker images считает каждый образ целиком, включая общие слои. Десять образов на базе python:3.13-slim каждый учитывают её 126 MB, хотя на диске она лежит один раз.
docker system df учитывает дедупликацию и показывает фактический объём.
Проверить:
echo "сумма SIZE:"
docker images --format '{{.Size}}' | python3 -c "
import sys, re
def b(s):
m = re.match(r'^([\d.]+)([KMGT]?B)\$', s.strip())
return float(m.group(1)) * {'B':1,'KB':1e3,'MB':1e6,'GB':1e9,'TB':1e12}[m.group(2)] if m else 0
print(f'{sum(b(l) for l in sys.stdin)/1e9:.2f} GB')
"
echo "фактически:"
docker system df --format '{{.Type}}: {{.Size}}' | head -1
Расхождение 2: 7 GB против 45 GB.
Образы — только одна из четырёх категорий. Полная картина:
docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 38 7 7.104GB 5.882GB (82%)
Containers 24 3 2.417GB 2.401GB (99%)
Local Volumes 12 4 9.882GB 6.104GB (61%)
Build Cache 412 0 25.71GB 25.71GB
Виновник — build cache: 25.7 GB из 45. Это самая частая причина «Docker съел диск», и её пропускают, потому что смотрят только docker images.
Вторая по объёму категория — volumes, 9.9 GB. Из них 6.1 GB не примонтированы ни к одному container — но это не значит, что они не нужны.
План освобождения, по возрастанию риска.
# Шаг 1. Кэш сборки старше недели. Риск: следующая сборка медленнее.
docker builder prune --filter 'until=168h'
# ожидаемое освобождение: ~20 GB
# Шаг 2. Образы без тега. Риск: минимальный.
docker image prune
# ожидаемое освобождение: ~1-2 GB
# Шаг 3. Проверить, что за containers накопились.
docker ps -a --filter status=exited --format 'table {{.Names}}\t{{.Status}}\t{{.Size}}'
# и только затем:
docker container prune
# ожидаемое освобождение: ~2.4 GB
# Риск: теряются writable layers остановленных containers
# Шаг 4. ПЕРЕД удалением volumes — обязательно посмотреть, что это.
docker volume ls --filter dangling=true
for v in $(docker volume ls --filter dangling=true -q); do
mp="$(docker volume inspect "$v" --format '{{.Mountpoint}}')"
printf '%-40s %s\n' "$v" "$(sudo du -sh "$mp" 2>/dev/null | cut -f1)"
done
Шаг 4 — точка принятия решения, а не автоматическое действие. Volume с именем вида myproject_pgdata почти наверняка содержит базу данных остановленного стека. После docker compose down она попадает в список dangling, и docker volume prune удалит её безвозвратно.
Чего делать не следует:
docker system prune -a --volumes # НЕ ДЕЛАТЬ
Эта команда объединяет все четыре шага, включая самый опасный, и не даёт возможности посмотреть на шаг 4. Она освободит 45 GB и может унести с собой данные, которые невозможно восстановить.
Профилактика.
# ограничить рост кэша сборки
docker builder prune --reserved-space 10GB
# ротация логов containers — вторая частая причина
# /etc/docker/daemon.json:
# { "log-driver": "local", "log-opts": { "max-size": "10m", "max-file": "3" } }
Настройка логирования разбиралась в уроке 1.3; подробно тема — в разделе 13.
Задание 9. Дедупликация слоёв [доп.]
Постановка. Соберите три образа на общей базе, каждый со своим уникальным слоем. Покажите, что общие слои хранятся однократно.
Сравните сумму SIZE трёх образов с фактическим приростом по docker system df.
Разбор
mkdir -p /tmp/ex9 && cd /tmp/ex9
docker pull -q python:3.13-slim
BEFORE="$(docker system df --format '{{if eq .Type "Images"}}{{.Size}}{{end}}' | head -1)"
echo "занято образами до сборки: $BEFORE"
for n in a b c; do
cat > "Dockerfile.$n" <<EOF
FROM python:3.13-slim
RUN echo "уникальные данные $n" > /marker-$n.txt
EOF
docker build -q -f "Dockerfile.$n" -t "dedup:$n" . > /dev/null
done
echo
docker images dedup --format 'table {{.Tag}}\t{{.Size}}'
AFTER="$(docker system df --format '{{if eq .Type "Images"}}{{.Size}}{{end}}' | head -1)"
echo
echo "занято образами после сборки: $AFTER"
echo
echo "--- общие слои ---"
python3 - <<'PY'
import subprocess, json
def layers(img):
out = subprocess.check_output(
['docker','image','inspect',img,'--format','{{json .RootFS.Layers}}'])
return json.loads(out)
a, b, c = layers('dedup:a'), layers('dedup:b'), layers('dedup:c')
common = set(a) & set(b) & set(c)
print(f'слоёв в каждом образе: {len(a)}')
print(f'общих для всех трёх: {len(common)}')
print(f'уникальных у каждого: {len(a) - len(common)}')
PY
docker rmi dedup:a dedup:b dedup:c > /dev/null
cd /tmp && rm -rf /tmp/ex9
занято образами до сборки: 1.204GB
TAG SIZE
c 126MB
b 126MB
a 126MB
занято образами после сборки: 1.204GB
--- общие слои ---
слоёв в каждом образе: 4
общих для всех трёх: 3
уникальных у каждого: 1
Результат. Три образа по 126 MB, суммарно «378 MB» по docker images. Фактический прирост — округляется до нуля на уровне гигабайт: уникальные слои содержат по одной строке текста.
Три слоя из четырёх общие и хранятся на диске один раз.
Практическое следствие. Использование одного базового образа во всех сервисах проекта экономит место кратно числу сервисов — и, что важнее, время загрузки: при docker pull общие слои скачиваются один раз.
Это один из аргументов в пользу общей базовой стадии в multi-stage сборках, разбираемой в разделе 05.
Очистка после раздела
# ВНИМАНИЕ: НЕ `docker ps -aq | xargs -r docker rm -f`.
# Такая строка удаляет ВСЕ container'ы на машине, включая чужие:
# базу коллеги, кластер kind, работающий стенд. Проверено дорого —
# при подготовке курса она снесла кластер, поднятый для раздела 18.
# Удаляем только то, что создали в этом разделе.
docker ps -aq --filter 'name=ex' --filter 'name=cow' --filter 'name=demo' \
| xargs -r docker rm -f
docker image prune -f
docker system df
Проверьте, что вернулись к исходному состоянию, зафиксированному в начале раздела.
Критерии завершения
Раздел закрыт, когда выполнены обязательные задания 1–4 и вы можете без подсказок ответить на вопросы из MAIN.md раздела.
Дальше: Quiz 03.
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий раздел → Containers и lifecycle
Главное оглавление