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

Раздел 3. Практические задания

Задания выполняются в реальной системе. Разбор открывайте только после самостоятельной попытки.

Обозначения: [обяз.] — обязательное, [доп.] — дополнительное, [★] — повышенной сложности, [диаг.] — диагностическое.

Подготовка:

bash
mkdir -p ~/docker-course/03-images
cd ~/docker-course/03-images
docker system df    # зафиксируйте исходное состояние

Задание 1. Анатомия образа [обяз.]

Постановка. Загрузите python:3.13-slim. Определите:

  1. Число слоёв.
  2. Размер каждого слоя и самый большой из них.
  3. Долю самого большого слоя в общем размере.
  4. Число шагов истории и сколько из них создали слой.

Ожидаемый результат. Таблица слоёв, отсортированная по убыванию, с процентами.

Проверка:

bash
docker image inspect python:3.13-slim --format 'слоёв: {{len .RootFS.Layers}}'
docker history python:3.13-slim --format '{{.Size}}' | grep -vc '^0B$'
Разбор
bash
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}')
"
text
шагов истории всего: 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 не меняется, а тег может быть перепривязан.

  1. Загрузите образ по тегу, запишите digest.
  2. Удалите образ локально.
  3. Загрузите его по digest.
  4. Убедитесь, что получили то же самое.
  5. Объясните, почему после шага 3 у образа тег <none>.

Ожидаемый результат. Совпадение Image ID до и после и объяснение отсутствия тега.

Проверка:

bash
docker images --digests python
Разбор
bash
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}}'
text
Image ID: sha256:c2f1e0d9b8a7c6d5...
digest:   sha256:bf503bb2243c5aad...
после загрузки по digest: sha256:c2f1e0d9b8a7c6d5...
образ идентичен
TAG       DIGEST
<none>    sha256:bf503bb2243c5aad...

Почему тег <none>. Тег — это отдельная локальная запись «имя → образ». При загрузке по digest вы не называли тега, значит и записи нет. Образ полностью работоспособен:

bash
docker run --rm "python@$DIGEST" python --version

Присвоить имя можно вручную:

bash
docker tag "python@$DIGEST" python:3.13-slim

Практическое применение. Именно так работает развёртывание по digest: в production запускают myapp@sha256:..., а тег остаётся справочной информацией в системе сборки. Подробнее — в разделе 14.


Задание 3. Writable layer [обяз.]

Постановка. Создайте container, измените в нём файл, унаследованный из образа, и найдите изменённую копию на host.

  1. Запустите container из python:3.13-slim.
  2. Измените файл /etc/hostname внутри container.
  3. Найдите его копию в writable layer на host.
  4. Убедитесь, что в слое образа оригинал не изменился.
  5. Удалите файл в container и найдите whiteout-маркер.

Ожидаемый результат. Путь к файлу в upperdir и вывод ls -l для whiteout.

Подсказка

UpperDir доступен через docker inspect <container> --format '{{.GraphDriver.Data.UpperDir}}'. При containerd image store поле может быть пустым — тогда берите upperdir из /proc/<pid>/mountinfo.

Разбор
bash
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
text
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. Перенос образа [обяз.]

Постановка. Перенесите образ через архив и подтвердите идентичность.

  1. Сохраните образ в сжатый архив.
  2. Удалите его локально.
  3. Восстановите из архива.
  4. Докажите, что образ идентичен исходному.
  5. Сравните размер архива до и после сжатия, объясните разницу.

Ожидаемый результат. Совпадение Image ID и объяснение коэффициента сжатия.

Проверка:

bash
docker save alpine:3.21 | gzip | wc -c
docker save alpine:3.21 | wc -c
Разбор
bash
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
text
-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 берёт их с диска.

Поэтому при передаче по сети архив нужно сжимать явно:

bash
docker save myapp:1.0 | gzip | ssh remote 'gunzip | docker load'

Почему Image ID совпадает. Image ID — хэш конфигурации образа (урок 3.1), а конфигурация содержит diff_ids всех слоёв. Совпадение ID означает, что и конфигурация, и все слои идентичны. Это надёжнее сравнения размеров.


Задание 5. Слой-виновник размера [доп.]

Постановка. Соберите образ, где временный файл создаётся в одной инструкции и удаляется в другой. Найдите слой, в котором остались данные, и измерьте разницу с правильным вариантом.

Ожидаемый результат. Два образа с разницей в размере и указание слоя с «удалёнными» данными.

Разбор
bash
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
text
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-маркер.

Убедимся, что файла действительно нет в обоих:

bash
docker run --rm ex5:bad  ls /tmp/junk 2>&1 | tail -1
docker run --rm ex5:good ls /tmp/junk 2>&1 | tail -1
text
ls: cannot access '/tmp/junk': No such file or directory
ls: cannot access '/tmp/junk': No such file or directory

Функционально образы идентичны. Разница только в 189 MB, которые никому не нужны и которые невозможно удалить постфактум.

bash
docker rmi ex5:bad ex5:good > /dev/null
cd /tmp && rm -rf /tmp/ex5

Задание 6. save против export [доп.]

Постановка. Экспортируйте один и тот же образ двумя способами и составьте таблицу того, что сохранилось, а что потерялось.

Сравните минимум по пяти параметрам: число слоёв, Cmd, число переменных Env, записей истории, возможность запуска без указания команды.

Разбор
bash
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
text
ПАРАМЕТР             ИСХОДНЫЙ                     ПОСЛЕ 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/loadexport/import
Слои3 → 33 → 1
Cmdсохранёнпотерян
Env4 → 44 → 1
История12 → 1212 → 1
Запуск без командыработаетошибка

Единственная оставшаяся переменная Env после import — это PATH, которую Docker подставляет по умолчанию, а не унаследованная из образа.

Вывод. Для переноса образа export/import непригодны. Их область — схлопывание слоёв и снятие среза файловой системы, и в обоих случаях конфигурацию приходится восстанавливать вручную через --change.


Задание 7. Извлечение секрета из слоя [★]

Постановка. Соберите образ, в котором секрет копируется, используется и удаляется следующей инструкцией. Затем извлеките секрет из образа, не запуская container.

Объясните, почему это возможно, и назовите правильное решение.

Задание выполняется на собственной учебной машине с заведомо ненастоящим секретом.

Подсказка 1

Слои внутри docker save — обычные tar-архивы. tar -tf покажет их содержимое.

Подсказка 2

Проверить наличие секрета в метаданных можно быстрее — через docker history --no-trunc.

Разбор
bash
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
text
=== 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-вида файл исчез. Из образа — нет.

Ещё один канал утечки. Проверьте историю:

bash
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:

dockerfile
# syntax=docker/dockerfile:1
FROM alpine:3.21
RUN --mount=type=secret,id=dbpass \
    . /run/secrets/dbpass && echo "длина: ${#DB_PASSWORD}"
bash
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 учитывает дедупликацию и показывает фактический объём.

Проверить:

bash
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.

Образы — только одна из четырёх категорий. Полная картина:

bash
docker system df
text
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 — но это не значит, что они не нужны.

План освобождения, по возрастанию риска.

bash
# Шаг 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 удалит её безвозвратно.

Чего делать не следует:

bash
docker system prune -a --volumes    # НЕ ДЕЛАТЬ

Эта команда объединяет все четыре шага, включая самый опасный, и не даёт возможности посмотреть на шаг 4. Она освободит 45 GB и может унести с собой данные, которые невозможно восстановить.

Профилактика.

bash
# ограничить рост кэша сборки
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.

Разбор
bash
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
text
занято образами до сборки: 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.


Очистка после раздела

bash
# ВНИМАНИЕ: НЕ `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
Главное оглавление

Markdown на GitHub ↗