3.1. Архитектура image
Цели
После этого материала вы сможете:
- описать, из каких объектов состоит image: index, manifest, config, слои;
- объяснить, что такое content-addressable storage и почему всё адресуется по хэшу;
- прочитать манифест и конфигурацию образа локально и из registry;
- объяснить, откуда берутся значения
CMD,ENV,USERпри запуске container; - найти файлы образа на диске;
- объяснить разницу между классическим хранилищем и containerd image store в Docker Engine 29.
Предварительные знания
- Раздел 02. Основы Containerization, особенно терминология;
- понимание, что такое хэш SHA-256;
- базовое чтение JSON.
Ключевые термины
| Термин | Объяснение |
|---|---|
blob | Произвольный двоичный объект, адресуемый по хэшу своего содержимого |
manifest | JSON-документ, перечисляющий слои образа и ссылку на его конфигурацию |
image config | JSON-документ с параметрами запуска и историей сборки |
image index | JSON-документ, перечисляющий манифесты для разных платформ. Синоним: manifest list |
content-addressable storage | Хранилище, где адресом объекта служит хэш его содержимого |
media type | Строка, описывающая формат объекта, например application/vnd.oci.image.manifest.v1+json |
snapshotter | Компонент containerd, управляющий слоями файловой системы |
Теория
Образ — это граф объектов
Слово «образ» обозначает не файл, а связанный набор объектов. Все они хранятся как blob и адресуются по хэшу содержимого.
тег "python:3.13-slim"
│
▼
┌─────────────────────────────┐
│ image index │ выбор платформы
│ (manifest list) │
├─────────────────────────────┤
│ linux/amd64 → sha256:aaa │
│ linux/arm64 → sha256:bbb │
│ linux/386 → sha256:ccc │
└──────────┬──────────────────┘
│ для нашей платформы
▼
┌─────────────────────────────┐
│ manifest (sha256:aaa) │
├─────────────────────────────┤
│ config: sha256:cfg... │──┐
│ layers: │ │
│ sha256:l1... (29 MB) │ │
│ sha256:l2... ( 3 MB) │ │
│ sha256:l3... (12 MB) │ │
└─────────────────────────────┘ │
▼
┌───────────────────────────────┐
│ config (sha256:cfg...) │
├───────────────────────────────┤
│ Cmd, Entrypoint, Env, User │
│ WorkingDir, ExposedPorts │
│ rootfs.diff_ids │
│ history[] │
└───────────────────────────────┘
Три уровня, у каждого своя задача:
| Объект | Отвечает на вопрос |
|---|---|
| index | Какой манифест взять для моей архитектуры? |
| manifest | Из каких слоёв собирается файловая система и где конфигурация? |
| config | Что и как запускать в собранной файловой системе? |
Не у каждого образа есть index. Образ, собранный под одну платформу, может состоять сразу из манифеста. Официальные образы почти всегда multi-platform, поэтому index у них есть.
Content-addressable storage
Все объекты адресуются по SHA-256 своего содержимого. Из этого следуют четыре свойства, на которых держится вся работа с образами.
Неизменяемость. Изменение хотя бы одного байта меняет хэш. Значит, объект с данным хэшем всегда содержит одно и то же — по всему миру, на любой машине.
Проверяемость. Скачав blob, можно вычислить его хэш и сравнить с ожидаемым. Подмена обнаруживается автоматически, без подписей.
Дедупликация. Два образа, использующие один базовый слой, ссылаются на один blob. На диске он лежит один раз. Именно поэтому десять образов на базе python:3.13-slim занимают не в десять раз больше места.
Кэшируемость. При docker pull Docker сначала получает манифест, смотрит список хэшей слоёв и скачивает только те, которых нет локально. Отсюда сообщения Already exists в выводе.
Что внутри manifest
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:c2f1e0d9...",
"size": 6521
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:a1b2c3d4...",
"size": 29154321
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:e5f6a7b8...",
"size": 3187654
}
]
}
Обратите внимание: манифест не содержит данных, только ссылки и размеры. Он маленький — единицы килобайт. Поэтому docker pull начинается быстро: сначала скачивается манифест, и только потом становится понятен объём работы.
size в манифесте — размер сжатого слоя. Именно эти числа складываются в объём загрузки по сети. Распакованный слой обычно в 2–4 раза больше.
Что внутри config
{
"architecture": "amd64",
"os": "linux",
"config": {
"Env": [
"PATH=/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin",
"PYTHON_VERSION=3.13.9"
],
"Cmd": ["python3"],
"WorkingDir": "/",
"User": "",
"ExposedPorts": {},
"Labels": {}
},
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:bf503bb2...",
"sha256:5b4a3928..."
]
},
"history": [
{
"created": "2026-06-14T02:11:07Z",
"created_by": "/bin/sh -c #(nop) ADD file:abc... in / "
},
{
"created": "2026-06-14T02:11:08Z",
"created_by": "/bin/sh -c #(nop) CMD [\"bash\"]",
"empty_layer": true
}
]
}
Три части:
config — то, что становится значениями по умолчанию при docker run. Когда вы запускаете docker run python:3.13-slim без команды, выполняется python3 — потому что так записано в Cmd. Все инструкции Dockerfile, не создающие файлов (ENV, CMD, ENTRYPOINT, USER, WORKDIR, EXPOSE, LABEL), попадают именно сюда.
rootfs.diff_ids — хэши распакованных слоёв, в отличие от digest в манифесте (хэши сжатых). Два разных набора хэшей для одних и тех же слоёв — источник путаницы. Причина в том, что сжатие не детерминировано: один и тот же tar может дать разные gzip-файлы, поэтому для идентификации содержимого нужен хэш распакованных данных.
history — список шагов сборки. Именно его показывает docker history. Записи с "empty_layer": true соответствуют инструкциям, не создавшим слой.
Откуда берётся Image ID
Image ID — это SHA-256 файла конфигурации:
Image ID = sha256(config.json)
Из этого следует практическое наблюдение, которое иначе выглядит странно: изменение только метаданных меняет Image ID, не меняя ни одного слоя. Добавьте LABEL в Dockerfile — получите новый образ с новым ID, но все слои будут переиспользованы, и на диске почти ничего не добавится.
И наоборот: digest манифеста — это SHA-256 самого манифеста, то есть он зависит и от конфигурации, и от списка слоёв. Разницу между Image ID и digest разбирали в уроке 2.7.
Два хранилища в Docker Engine 29
Начиная с Engine 29 для новых установок по умолчанию используется containerd image store. Он приходит на смену классическому хранилищу образов Docker.
| Классическое хранилище | containerd image store | |
|---|---|---|
| Где данные | /var/lib/docker/image, /var/lib/docker/overlay2 | /var/lib/containerd |
Значение Storage Driver в docker info | overlay2 | overlayfs |
| Multi-platform образы локально | Не поддерживаются: хранится только текущая платформа | Поддерживаются полностью |
| Хранение слоёв | Только распакованные | Сжатые и распакованные |
| Место на диске | Меньше | Больше, зато push и pull быстрее |
| Attestations (SBOM, provenance) | Ограниченно | Полностью |
Совместимость с userns-remap | Есть | Нет |
Проверить, что используется:
docker info --format '{{.Driver}}'
Важно при переключении. Образы и containers из прежнего хранилища не удаляются, но становятся невидимыми — они лежат в другом каталоге. Перед сменой хранилища сохраните нужные образы через
docker saveили опубликуйте их в registry.
Для курса разница проявляется в трёх местах: вывод docker images (урок 3.4), поведение docker save/load (урок 3.5) и расположение файлов на диске.
Внутренний механизм
Что происходит при docker pull
- CLI просит daemon загрузить образ по имени.
- Daemon обращается к registry за токеном (для публичных образов — анонимным).
- Запрашивается манифест по тегу:
GET /v2/library/python/manifests/3.13-slim. - Если пришёл index, daemon выбирает манифест под текущие
osиarchitectureи запрашивает его. - Из манифеста берётся список слоёв. Для каждого проверяется наличие blob локально.
- Отсутствующие слои скачиваются параллельно и распаковываются.
- Скачивается config.
- Локально создаётся запись, связывающая тег с манифестом.
Строки Already exists в выводе — это шаг 5: слой уже есть от другого образа.
Где физически лежат объекты
При containerd image store:
sudo ls /var/lib/containerd/
io.containerd.content.v1.content/ ← blob: манифесты, конфиги, сжатые слои
io.containerd.metadata.v1.bolt/ ← база метаданных
io.containerd.snapshotter.v1.overlayfs/ ← распакованные слои
Blob хранятся по хэшу:
sudo ls /var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/ | head -3
Имя файла и есть его хэш. Это content-addressable storage в буквальном виде.
При классическом хранилище:
/var/lib/docker/
├── image/overlay2/
│ ├── imagedb/content/sha256/ ← конфигурации образов
│ ├── layerdb/sha256/ ← метаданные слоёв
│ └── repositories.json ← связь тегов с образами
└── overlay2/ ← распакованные слои
Команды и примеры
Просмотр манифеста из registry
Не загружая образ:
docker manifest inspect python:3.13-slim
Для multi-platform образа вы увидите index:
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:a1b2c3...",
"size": 1571,
"platform": { "architecture": "amd64", "os": "linux" }
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:d4e5f6...",
"size": 1571,
"platform": { "architecture": "arm64", "os": "linux", "variant": "v8" }
}
]
}
Список доступных платформ:
docker manifest inspect python:3.13-slim \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for m in d.get('manifests', []):
p = m['platform']
if p['os'] == 'unknown':
continue
variant = p.get('variant', '')
print(f\"{p['os']}/{p['architecture']}{'/' + variant if variant else ''}\")
"
linux/amd64
linux/arm64/v8
linux/386
linux/arm/v7
linux/ppc64le
linux/s390x
Записи с os: unknown — это attestations (SBOM и provenance), а не образы для платформ. Они прикрепляются к образу тем же механизмом index.
Манифест для конкретной платформы:
docker manifest inspect --verbose python:3.13-slim \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
data = data if isinstance(data, list) else [data]
for entry in data:
if entry.get('Descriptor', {}).get('platform', {}).get('architecture') == 'amd64':
m = entry['SchemaV2Manifest']
print('config digest:', m['config']['digest'])
print('слоёв:', len(m['layers']))
total = sum(l['size'] for l in m['layers'])
print(f'сжатый размер: {total/1048576:.1f} MB')
for i, l in enumerate(m['layers'], 1):
print(f\" {i}. {l['digest'][:26]}... {l['size']/1048576:8.2f} MB\")
break
"
config digest: sha256:c2f1e0d9b8a7...
слоёв: 3
сжатый размер: 44.6 MB
1. sha256:bf503bb2243c5aad0aa9... 28.99 MB
2. sha256:5b4a392817263544f0e1... 3.31 MB
3. sha256:0e1d2c3b4a596877869 5... 12.32 MB
Просмотр конфигурации локального образа
docker pull python:3.13-slim
docker image inspect python:3.13-slim --format '{{json .Config}}' | python3 -m json.tool
{
"Hostname": "",
"Env": [
"PATH=/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin",
"LANG=C.UTF-8",
"PYTHON_VERSION=3.13.9",
"PYTHON_SHA256=1f2e3d4c5b6a798867564534231201ffeeddccbbaa99887766554433221100ff"
],
"Cmd": ["python3"],
"WorkingDir": "/",
"Entrypoint": null,
"User": "",
"ExposedPorts": {},
"Labels": {}
}
Вывод сокращён: полная структура содержит ещё около десятка полей.
Проверим утверждение о том, что Cmd определяет поведение по умолчанию:
docker image inspect python:3.13-slim --format 'Cmd: {{.Config.Cmd}}'
docker run --rm python:3.13-slim python --version
docker run --rm python:3.13-slim --version 2>&1 | grep -o 'exec: .*'
Cmd: [python3]
Python 3.13.14
exec: "--version": executable file not found in $PATH
Три строки показывают связь config с поведением — и одну ловушку.
Cmd из конфигурации выполняется, когда аргументов нет. Как только аргумент передан, он Cmd не дополняет, а заменяет целиком: во второй команде выполнился не «python3 плюс --version», а ровно python --version.
Третья строка — та же подмена, доведённая до отказа. --version стал командой, и container не запустился: исполняемого файла с таким именем нет. Дописывание аргументов к готовой команде — свойство ENTRYPOINT, а не Cmd, и у python:3.13-slim ENTRYPOINT пуст.
Механизм разбирается подробно в разделе 05.
Слои и diff_ids
docker image inspect python:3.13-slim --format '{{json .RootFS.Layers}}' \
| python3 -c "
import json, sys
for i, l in enumerate(json.load(sys.stdin), 1):
print(f'{i}. {l}')
"
1. sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
2. sha256:5b4a392817263544f0e1d2c3b4a5968778695a4b3c2d1e0f9a8b7c6dbf503bb2
3. sha256:0e1d2c3b4a5968778695a4b3c2d1e0f9a8b7c6dbf503bb2243c5aad0aa93544
Это diff_ids — хэши распакованных слоёв. Сравните их с digest слоёв из манифеста выше: значения разные для одних и тех же слоёв. Первые идентифицируют содержимое, вторые — переданные по сети сжатые архивы.
Image ID и его происхождение
docker image inspect python:3.13-slim --format '{{.Id}}'
sha256:c2f1e0d9b8a7c6d5e4f3a2b1908877665544332211aabbccddeeff0011223344
Это хэш файла конфигурации. Проверим на практике, что изменение только метаданных меняет ID, не меняя слоёв:
mkdir -p /tmp/labeltest && cd /tmp/labeltest
cat > Dockerfile <<'EOF'
FROM python:3.13-slim
LABEL maintainer="course@example.com"
EOF
docker build -q -t labeltest:1 .
docker image inspect python:3.13-slim --format 'база: {{.Id}}'
docker image inspect labeltest:1 --format 'с меткой: {{.Id}}'
echo "--- слои ---"
diff <(docker image inspect python:3.13-slim --format '{{json .RootFS.Layers}}') \
<(docker image inspect labeltest:1 --format '{{json .RootFS.Layers}}') \
&& echo "слои идентичны"
база: sha256:c2f1e0d9b8a7c6d5...
с меткой: sha256:7a6b5c4d3e2f1908...
--- слои ---
слои идентичны
Разные ID, одинаковые слои. Дополнительное место на диске — размер нового файла конфигурации, единицы килобайт.
История сборки
docker history python:3.13-slim --no-trunc --format 'table {{.Size}}\t{{.CreatedBy}}' | head -8
SIZE CREATED BY
0B CMD ["python3"]
0B ENV PYTHON_SHA256=...
12.3MB RUN /bin/sh -c set -eux; wget -O python.tar.xz ...
0B ENV PYTHON_VERSION=3.13.9
3.31MB RUN /bin/sh -c set -eux; apt-get update; apt-get install -y ...
0B ENV LANG=C.UTF-8
29MB /bin/sh -c #(nop) ADD file:abc123... in /
Строки с размером 0B — записи с empty_layer: true: они изменили только конфигурацию. Строки с ненулевым размером соответствуют реальным слоям. Подробно docker history разбирается в уроке 3.4.
Файлы на диске
docker info --format 'Storage Driver: {{.Driver}}'
При overlayfs (containerd image store):
sudo ls /var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/ | wc -l
sudo du -sh /var/lib/containerd/
Проверим, что конкретный blob лежит по имени, равному его хэшу:
CFG="$(docker image inspect python:3.13-slim --format '{{.Id}}' | cut -d: -f2)"
sudo ls -l "/var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/$CFG" 2>/dev/null \
&& echo "конфигурация найдена по своему хэшу"
Убедимся, что имя файла действительно равно хэшу содержимого:
sudo sha256sum "/var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/$CFG" \
| awk -v n="$CFG" '{print ($1==n) ? "хэш совпадает с именем файла" : "РАСХОЖДЕНИЕ"}'
хэш совпадает с именем файла
Это и есть content-addressable storage — без метафор.
Дедупликация слоёв
docker pull python:3.13-slim
docker pull python:3.13-alpine
python3 - <<'PY'
import subprocess, json
def layers(img):
out = subprocess.check_output(
['docker', 'image', 'inspect', img, '--format', '{{json .RootFS.Layers}}'])
return set(json.loads(out))
a, b = layers('python:3.13-slim'), layers('python:3.13-alpine')
print(f'слоёв в slim: {len(a)}')
print(f'слоёв в alpine: {len(b)}')
print(f'общих: {len(a & b)}')
PY
слоёв в slim: 3
слоёв в alpine: 4
общих: 0
Общих нет — базы разные (Debian и Alpine). А теперь образы с общей базой:
cd /tmp/labeltest
cat > Dockerfile.a <<'EOF'
FROM python:3.13-slim
RUN echo "a" > /a.txt
EOF
cat > Dockerfile.b <<'EOF'
FROM python:3.13-slim
RUN echo "b" > /b.txt
EOF
docker build -q -f Dockerfile.a -t shared:a .
docker build -q -f Dockerfile.b -t shared:b .
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 = layers('shared:a'), layers('shared:b')
print(f'shared:a — {len(a)} слоёв')
print(f'shared:b — {len(b)} слоёв')
print(f'общих: {len(set(a) & set(b))}')
print(f'уникальных у каждого: {len(set(a) ^ set(b)) // 2}')
PY
shared:a — 4 слоёв
shared:b — 4 слоёв
общих: 3
уникальных у каждого: 1
Три общих слоя хранятся на диске один раз. Именно поэтому суммарный размер по docker images больше, чем фактически занятое место.
Уборка
docker rmi labeltest:1 shared:a shared:b python:3.13-alpine 2>/dev/null || true
rm -rf /tmp/labeltest
Практическое упражнение
Задание. Напишите скрипт image-anatomy.sh <image>, который выводит полную анатомию образа:
- Список платформ из index (если образ multi-platform).
- Число слоёв и их размеры, отсортированные по убыванию.
- Суммарный сжатый размер (из манифеста) и распакованный (из
docker images). - Ключевые параметры конфигурации:
Cmd,Entrypoint,User,WorkingDir, число переменныхEnv. - Число шагов истории и сколько из них создали слой.
Скрипт должен работать и для образа, отсутствующего локально (загружая только метаданные, где это возможно).
Подсказки
Подсказка 1
docker manifest inspect работает без загрузки образа и обращается прямо к registry. docker image inspect требует локального образа.
Подсказка 2
Шаги истории, создавшие слой, отличаются ненулевым размером:
docker history <image> --format '{{.Size}}'
Подсказка 3
Разбирать JSON удобнее в Python, чем шаблонами Go. Передавайте вывод docker ... --format '{{json .}}' в python3 -c.
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/usr/bin/env bash
# image-anatomy.sh — полная анатомия образа.
set -euo pipefail
IMG="${1:?Использование: $0 <image>}"
echo "=== Образ: $IMG ==="
echo
echo "--- 1. Платформы (из registry) ---"
docker manifest inspect "$IMG" 2>/dev/null | python3 -c "
import json, sys
try:
d = json.load(sys.stdin)
except Exception:
print(' метаданные недоступны'); raise SystemExit
if 'manifests' in d:
for m in d['manifests']:
p = m.get('platform', {})
if p.get('os') == 'unknown':
continue
v = p.get('variant', '')
print(f\" {p.get('os')}/{p.get('architecture')}{'/'+v if v else ''}\")
extra = sum(1 for m in d['manifests']
if m.get('platform', {}).get('os') == 'unknown')
if extra:
print(f' (+{extra} attestation-записей: SBOM и provenance)')
else:
print(' образ под одну платформу (index отсутствует)')
" || echo " недоступно"
# Загрузить, если образа нет локально
if ! docker image inspect "$IMG" > /dev/null 2>&1; then
echo
echo " образа нет локально — загружаю..."
docker pull -q "$IMG" > /dev/null
fi
echo
echo "--- 2. Слои ---"
docker history "$IMG" --format '{{.Size}}\t{{.CreatedBy}}' --no-trunc \
| python3 -c "
import sys
rows = []
for line in sys.stdin:
size, _, cmd = line.partition('\t')
rows.append((size.strip(), cmd.strip()[:64]))
real = [r for r in rows if r[0] not in ('0B', '0')]
print(f' всего шагов истории: {len(rows)}')
print(f' из них создали слой: {len(real)}')
print()
for size, cmd in real:
print(f' {size:>10} {cmd}')
"
echo
echo "--- 3. Размеры ---"
compressed="$(docker manifest inspect "$IMG" 2>/dev/null | python3 -c "
import json, sys
d = json.load(sys.stdin)
if 'layers' in d:
print(sum(l['size'] for l in d['layers']))
else:
print(0)
" 2>/dev/null || echo 0)"
if [ "${compressed:-0}" -gt 0 ]; then
printf ' сжатый (загрузка по сети): %.1f MB\n' \
"$(awk -v c="$compressed" 'BEGIN{print c/1048576}')"
fi
printf ' распакованный (на диске): %s\n' \
"$(docker images "$IMG" --format '{{.Size}}' | head -1)"
echo
echo "--- 4. Конфигурация ---"
docker image inspect "$IMG" --format '{{json .Config}}' | python3 -c "
import json, sys
c = json.load(sys.stdin)
def show(k, v):
print(f' {k:<14} {v if v not in (None, [], {}, \"\") else \"(не задано)\"}')
show('Entrypoint', c.get('Entrypoint'))
show('Cmd', c.get('Cmd'))
show('User', c.get('User'))
show('WorkingDir', c.get('WorkingDir'))
show('ExposedPorts', list((c.get('ExposedPorts') or {}).keys()))
print(f\" {'Env':<14} {len(c.get('Env') or [])} переменных\")
print(f\" {'Labels':<14} {len(c.get('Labels') or {})} меток\")
"
echo
echo "--- 5. Идентификаторы ---"
printf ' Image ID (хэш конфигурации): %s\n' \
"$(docker image inspect "$IMG" --format '{{.Id}}')"
digest="$(docker image inspect "$IMG" --format '{{if .RepoDigests}}{{index .RepoDigests 0}}{{end}}')"
printf ' Digest (хэш манифеста): %s\n' "${digest:-отсутствует (образ не из registry)}"
Запуск:
chmod +x image-anatomy.sh
./image-anatomy.sh python:3.13-slim
Ожидаемый вывод (сокращённо):
=== Образ: python:3.13-slim ===
--- 1. Платформы (из registry) ---
linux/amd64
linux/arm64/v8
linux/386
linux/arm/v7
linux/ppc64le
linux/s390x
(+6 attestation-записей: SBOM и provenance)
--- 2. Слои ---
всего шагов истории: 12
из них создали слой: 3
29MB /bin/sh -c #(nop) ADD file:abc123... in /
12.3MB RUN /bin/sh -c set -eux; wget -O python.tar.xz ...
3.31MB RUN /bin/sh -c set -eux; apt-get update; apt-get install ...
--- 3. Размеры ---
сжатый (загрузка по сети): 44.6 MB
распакованный (на диске): 126MB
--- 4. Конфигурация ---
Entrypoint (не задано)
Cmd ['python3']
User (не задано)
WorkingDir /
ExposedPorts []
Env 4 переменных
Labels 0 меток
--- 5. Идентификаторы ---
Image ID (хэш конфигурации): sha256:c2f1e0d9b8a7c6d5...
Digest (хэш манифеста): python@sha256:bf503bb2243c...
Что важно заметить в результате.
Из 12 шагов истории только 3 создали слои — остальные девять изменили лишь конфигурацию и не занимают места.
Сжатый размер 44.6 MB против распакованных 126 MB: разница почти втрое. Первое число определяет время загрузки по сети, второе — место на диске. Когда говорят «образ весит столько-то», обычно имеют в виду второе, а платят за первое.
Поле User пустое — образ запускается от root. Это относится ко всем официальным образам Python и является одной из причин, по которой в разделе 06 мы создаём пользователя явно.
Проверка результата
docker pull -q alpine:3.21
docker image inspect alpine:3.21 --format 'слоёв: {{len .RootFS.Layers}}'
docker manifest inspect alpine:3.21 | grep -c '"digest"'
Убедитесь, что можете объяснить: почему число diff_ids в конфигурации и число digest в манифесте совпадает, а сами значения — нет.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
| Считать образ одним файлом | Слово «образ» это подразумевает | Образ — граф объектов: index, manifest, config, слои |
| Путать сжатый и распакованный размер | docker images показывает распакованный, манифест — сжатый | Различать: по сети идёт сжатый, на диске лежит распакованный |
Складывать SIZE всех образов для оценки места | Общие слои хранятся однократно | Использовать docker system df |
Путать diff_ids и digest слоёв | Оба вида sha256:... | diff_ids — распакованные, digest в манифесте — сжатые |
Удивляться новому Image ID при добавлении LABEL | ID — хэш конфигурации, а не слоёв | Это нормально; место почти не расходуется |
| Ожидать multi-platform локально на классическом хранилище | Оно хранит только текущую платформу | Включить containerd image store |
| Переключить хранилище и решить, что образы пропали | Они в другом каталоге и не удалены | Сохранить через docker save до переключения |
Контрольные вопросы
На понимание:
- Зачем нужен image index, если есть manifest?
- Почему у слоя два разных хэша и чем они различаются?
- Почему добавление
LABELменяет Image ID, но почти не увеличивает занятое место? - Откуда Docker берёт команду, если при
docker runона не указана? - Почему content-addressable storage делает подмену образа обнаружимой без подписей?
На применение:
- Как узнать список платформ образа, не загружая его?
- Как посмотреть все переменные окружения, заданные в образе?
- Как определить, какое хранилище образов используется в вашей системе?
На диагностику:
docker imagesпоказывает суммарно 12 GB, аdocker system df— 7 GB. Объясните расхождение.- После включения containerd image store
docker imagesпоказывает пустой список. Что произошло и что делать?
Краткое резюме
- Образ — граф объектов: image index → manifest → config + слои.
- Все объекты адресуются по SHA-256 своего содержимого.
- Из этого следуют неизменяемость, проверяемость, дедупликация и кэшируемость.
- Manifest содержит только ссылки и размеры, поэтому он маленький.
- Config задаёт значения по умолчанию:
Cmd,Entrypoint,Env,User,WorkingDir. - Image ID — хэш конфигурации; digest — хэш манифеста.
digestслоёв в манифесте — от сжатых данных,diff_idsв конфигурации — от распакованных.- Инструкции, не создающие файлов, дают записи
empty_layerи не занимают места. - Docker Engine 29 по умолчанию использует containerd image store: другой каталог, поддержка multi-platform локально, хранение и сжатых, и распакованных слоёв.
- При переключении хранилища прежние образы не удаляются, но становятся невидимыми.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| OCI Image Specification | https://github.com/opencontainers/image-spec | Структура manifest, config, image index, media types, diff_ids |
| OCI Image Layer Specification | https://github.com/opencontainers/image-spec/blob/main/layer.md | Формат слоёв, сжатие, whiteout |
| containerd image store | https://docs.docker.com/engine/storage/containerd/ | Включение через features.containerd-snapshotter, отличия от классического хранилища, предупреждение о видимости образов при переключении |
| Docker Engine 29 release notes | https://docs.docker.com/engine/release-notes/29/ | containerd image store по умолчанию для новых установок |
| docker manifest inspect | https://docs.docker.com/reference/cli/docker/manifest/inspect/ | Просмотр манифеста и index без загрузки образа |
| docker image inspect | https://docs.docker.com/reference/cli/docker/image/inspect/ | Поля Config, RootFS.Layers, Id, RepoDigests |
| Docker storage drivers | https://docs.docker.com/engine/storage/drivers/ | Расположение данных, роль storage driver |
| Docker build attestations | https://docs.docker.com/build/metadata/attestations/ | Записи os: unknown в index как SBOM и provenance |
Навигация
Вернуться к разделу
Следующий материал → Слои и copy-on-write
Главное оглавление