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

3.1. Архитектура image

Цели

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

  • описать, из каких объектов состоит image: index, manifest, config, слои;
  • объяснить, что такое content-addressable storage и почему всё адресуется по хэшу;
  • прочитать манифест и конфигурацию образа локально и из registry;
  • объяснить, откуда берутся значения CMD, ENV, USER при запуске container;
  • найти файлы образа на диске;
  • объяснить разницу между классическим хранилищем и containerd image store в Docker Engine 29.

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

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

ТерминОбъяснение
blobПроизвольный двоичный объект, адресуемый по хэшу своего содержимого
manifestJSON-документ, перечисляющий слои образа и ссылку на его конфигурацию
image configJSON-документ с параметрами запуска и историей сборки
image indexJSON-документ, перечисляющий манифесты для разных платформ. Синоним: manifest list
content-addressable storageХранилище, где адресом объекта служит хэш его содержимого
media typeСтрока, описывающая формат объекта, например application/vnd.oci.image.manifest.v1+json
snapshotterКомпонент containerd, управляющий слоями файловой системы

Теория

Образ — это граф объектов

Слово «образ» обозначает не файл, а связанный набор объектов. Все они хранятся как blob и адресуются по хэшу содержимого.

text
   тег "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

json
{
  "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

json
{
  "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 файла конфигурации:

text
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 infooverlay2overlayfs
Multi-platform образы локальноНе поддерживаются: хранится только текущая платформаПоддерживаются полностью
Хранение слоёвТолько распакованныеСжатые и распакованные
Место на дискеМеньшеБольше, зато push и pull быстрее
Attestations (SBOM, provenance)ОграниченноПолностью
Совместимость с userns-remapЕстьНет

Проверить, что используется:

bash
docker info --format '{{.Driver}}'

Важно при переключении. Образы и containers из прежнего хранилища не удаляются, но становятся невидимыми — они лежат в другом каталоге. Перед сменой хранилища сохраните нужные образы через docker save или опубликуйте их в registry.

Для курса разница проявляется в трёх местах: вывод docker images (урок 3.4), поведение docker save/load (урок 3.5) и расположение файлов на диске.


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

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

  1. CLI просит daemon загрузить образ по имени.
  2. Daemon обращается к registry за токеном (для публичных образов — анонимным).
  3. Запрашивается манифест по тегу: GET /v2/library/python/manifests/3.13-slim.
  4. Если пришёл index, daemon выбирает манифест под текущие os и architecture и запрашивает его.
  5. Из манифеста берётся список слоёв. Для каждого проверяется наличие blob локально.
  6. Отсутствующие слои скачиваются параллельно и распаковываются.
  7. Скачивается config.
  8. Локально создаётся запись, связывающая тег с манифестом.

Строки Already exists в выводе — это шаг 5: слой уже есть от другого образа.

Где физически лежат объекты

При containerd image store:

bash
sudo ls /var/lib/containerd/
text
io.containerd.content.v1.content/     ← blob: манифесты, конфиги, сжатые слои
io.containerd.metadata.v1.bolt/       ← база метаданных
io.containerd.snapshotter.v1.overlayfs/  ← распакованные слои

Blob хранятся по хэшу:

bash
sudo ls /var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/ | head -3

Имя файла и есть его хэш. Это content-addressable storage в буквальном виде.

При классическом хранилище:

text
/var/lib/docker/
├── image/overlay2/
│   ├── imagedb/content/sha256/     ← конфигурации образов
│   ├── layerdb/sha256/             ← метаданные слоёв
│   └── repositories.json           ← связь тегов с образами
└── overlay2/                       ← распакованные слои

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

Просмотр манифеста из registry

Не загружая образ:

bash
docker manifest inspect python:3.13-slim

Для multi-platform образа вы увидите index:

json
{
  "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" }
    }
  ]
}

Список доступных платформ:

bash
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 ''}\")
"
text
linux/amd64
linux/arm64/v8
linux/386
linux/arm/v7
linux/ppc64le
linux/s390x

Записи с os: unknown — это attestations (SBOM и provenance), а не образы для платформ. Они прикрепляются к образу тем же механизмом index.

Манифест для конкретной платформы:

bash
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
"
text
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

Просмотр конфигурации локального образа

bash
docker pull python:3.13-slim
docker image inspect python:3.13-slim --format '{{json .Config}}' | python3 -m json.tool
json
{
    "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 определяет поведение по умолчанию:

bash
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: .*'
text
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

bash
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}')
"
text
1. sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
2. sha256:5b4a392817263544f0e1d2c3b4a5968778695a4b3c2d1e0f9a8b7c6dbf503bb2
3. sha256:0e1d2c3b4a5968778695a4b3c2d1e0f9a8b7c6dbf503bb2243c5aad0aa93544

Это diff_ids — хэши распакованных слоёв. Сравните их с digest слоёв из манифеста выше: значения разные для одних и тех же слоёв. Первые идентифицируют содержимое, вторые — переданные по сети сжатые архивы.

Image ID и его происхождение

bash
docker image inspect python:3.13-slim --format '{{.Id}}'
text
sha256:c2f1e0d9b8a7c6d5e4f3a2b1908877665544332211aabbccddeeff0011223344

Это хэш файла конфигурации. Проверим на практике, что изменение только метаданных меняет ID, не меняя слоёв:

bash
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 "слои идентичны"
text
база:   sha256:c2f1e0d9b8a7c6d5...
с меткой: sha256:7a6b5c4d3e2f1908...
--- слои ---
слои идентичны

Разные ID, одинаковые слои. Дополнительное место на диске — размер нового файла конфигурации, единицы килобайт.

История сборки

bash
docker history python:3.13-slim --no-trunc --format 'table {{.Size}}\t{{.CreatedBy}}' | head -8
text
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.

Файлы на диске

bash
docker info --format 'Storage Driver: {{.Driver}}'

При overlayfs (containerd image store):

bash
sudo ls /var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/ | wc -l
sudo du -sh /var/lib/containerd/

Проверим, что конкретный blob лежит по имени, равному его хэшу:

bash
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 "конфигурация найдена по своему хэшу"

Убедимся, что имя файла действительно равно хэшу содержимого:

bash
sudo sha256sum "/var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/$CFG" \
  | awk -v n="$CFG" '{print ($1==n) ? "хэш совпадает с именем файла" : "РАСХОЖДЕНИЕ"}'
text
хэш совпадает с именем файла

Это и есть content-addressable storage — без метафор.

Дедупликация слоёв

bash
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
text
слоёв в slim:   3
слоёв в alpine: 4
общих:          0

Общих нет — базы разные (Debian и Alpine). А теперь образы с общей базой:

bash
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
text
shared:a — 4 слоёв
shared:b — 4 слоёв
общих:     3
уникальных у каждого: 1

Три общих слоя хранятся на диске один раз. Именно поэтому суммарный размер по docker images больше, чем фактически занятое место.

Уборка

bash
docker rmi labeltest:1 shared:a shared:b python:3.13-alpine 2>/dev/null || true
rm -rf /tmp/labeltest

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

Задание. Напишите скрипт image-anatomy.sh <image>, который выводит полную анатомию образа:

  1. Список платформ из index (если образ multi-platform).
  2. Число слоёв и их размеры, отсортированные по убыванию.
  3. Суммарный сжатый размер (из манифеста) и распакованный (из docker images).
  4. Ключевые параметры конфигурации: Cmd, Entrypoint, User, WorkingDir, число переменных Env.
  5. Число шагов истории и сколько из них создали слой.

Скрипт должен работать и для образа, отсутствующего локально (загружая только метаданные, где это возможно).

Подсказки

Подсказка 1

docker manifest inspect работает без загрузки образа и обращается прямо к registry. docker image inspect требует локального образа.

Подсказка 2

Шаги истории, создавшие слой, отличаются ненулевым размером:

bash
docker history <image> --format '{{.Size}}'
Подсказка 3

Разбирать JSON удобнее в Python, чем шаблонами Go. Передавайте вывод docker ... --format '{{json .}}' в python3 -c.

Решение

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

Показать решение
bash
#!/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)}"

Запуск:

bash
chmod +x image-anatomy.sh
./image-anatomy.sh python:3.13-slim

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

text
=== Образ: 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 мы создаём пользователя явно.

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

bash
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 при добавлении LABELID — хэш конфигурации, а не слоёвЭто нормально; место почти не расходуется
Ожидать multi-platform локально на классическом хранилищеОно хранит только текущую платформуВключить containerd image store
Переключить хранилище и решить, что образы пропалиОни в другом каталоге и не удаленыСохранить через docker save до переключения

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

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

  1. Зачем нужен image index, если есть manifest?
  2. Почему у слоя два разных хэша и чем они различаются?
  3. Почему добавление LABEL меняет Image ID, но почти не увеличивает занятое место?
  4. Откуда Docker берёт команду, если при docker run она не указана?
  5. Почему content-addressable storage делает подмену образа обнаружимой без подписей?

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

  1. Как узнать список платформ образа, не загружая его?
  2. Как посмотреть все переменные окружения, заданные в образе?
  3. Как определить, какое хранилище образов используется в вашей системе?

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

  1. docker images показывает суммарно 12 GB, а docker system df — 7 GB. Объясните расхождение.
  2. После включения containerd image store docker images показывает пустой список. Что произошло и что делать?

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

  1. Образ — граф объектов: image index → manifest → config + слои.
  2. Все объекты адресуются по SHA-256 своего содержимого.
  3. Из этого следуют неизменяемость, проверяемость, дедупликация и кэшируемость.
  4. Manifest содержит только ссылки и размеры, поэтому он маленький.
  5. Config задаёт значения по умолчанию: Cmd, Entrypoint, Env, User, WorkingDir.
  6. Image ID — хэш конфигурации; digest — хэш манифеста.
  7. digest слоёв в манифесте — от сжатых данных, diff_ids в конфигурации — от распакованных.
  8. Инструкции, не создающие файлов, дают записи empty_layer и не занимают места.
  9. Docker Engine 29 по умолчанию использует containerd image store: другой каталог, поддержка multi-platform локально, хранение и сжатых, и распакованных слоёв.
  10. При переключении хранилища прежние образы не удаляются, но становятся невидимыми.

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

ИсточникСсылкаЧто подтверждает
OCI Image Specificationhttps://github.com/opencontainers/image-specСтруктура manifest, config, image index, media types, diff_ids
OCI Image Layer Specificationhttps://github.com/opencontainers/image-spec/blob/main/layer.mdФормат слоёв, сжатие, whiteout
containerd image storehttps://docs.docker.com/engine/storage/containerd/Включение через features.containerd-snapshotter, отличия от классического хранилища, предупреждение о видимости образов при переключении
Docker Engine 29 release noteshttps://docs.docker.com/engine/release-notes/29/containerd image store по умолчанию для новых установок
docker manifest inspecthttps://docs.docker.com/reference/cli/docker/manifest/inspect/Просмотр манифеста и index без загрузки образа
docker image inspecthttps://docs.docker.com/reference/cli/docker/image/inspect/Поля Config, RootFS.Layers, Id, RepoDigests
Docker storage drivershttps://docs.docker.com/engine/storage/drivers/Расположение данных, роль storage driver
Docker build attestationshttps://docs.docker.com/build/metadata/attestations/Записи os: unknown в index как SBOM и provenance

Навигация

Вернуться к разделу
Следующий материал → Слои и copy-on-write
Главное оглавление

Markdown на GitHub ↗