Learning Platform
Глоссарий Troubleshooting
Урок 06.01 · 22 мин
Начальный
dockerimagesocilayersmanifest

Что такое Docker-образ

Когда Data Engineer запускает локальный Airflow или поднимает Postgres для своего DAG’а, он не загружает программу в обычном смысле слова. Он скачивает образ — иммутабельный артефакт, который описывает корневую файловую систему процесса до последнего байта. И в отличие от установки пакетов через apt или pip, образ одинаков на всех машинах: то, что собралось в CI на Ubuntu, будет в точности тем же на macOS с OrbStack, на коллегиной Windows с WSL2 и на production-сервере. Один SHA256 — одно поведение.

В этом уроке разберём, что внутри образа физически лежит, почему он состоит из слоёв и зачем нужен манифест.


Образ это не “архив с программой”

Распространённое заблуждение: образ это zip-архив с приложением. На самом деле образ это набор связанных артефактов:

  • Один или несколько read-only слоёв — каждый слой это tar-архив с фрагментом файловой системы (обычно diff относительно предыдущего слоя)
  • Манифест (manifest) — JSON, описывающий какие слои есть, в каком порядке их применять, какая платформа (linux/amd64, linux/arm64) и где взять конфиг
  • Config (image config) — JSON с метаданными: какая команда выполняется по умолчанию (CMD), какие переменные окружения, рабочая директория, история создания каждого слоя
  • Digest (sha256:…) — криптографический хэш манифеста. Уникально идентифицирует образ. Изменился хоть один байт — изменился digest

Все эти артефакты хранятся в registry (Docker Hub, GHCR, ECR) и адресуются по content-addressable принципу: имя слоя это его собственный хэш. Это не случайность — это краеугольный камень дедупликации и безопасности.

NOTE

Современный формат образа стандартизирован OCI (Open Container Initiative). Спецификация называется image-spec и описывает структуру манифеста, конфига и слоёв. Docker, Podman, containerd, Kubernetes — все говорят на одном языке OCI. Это значит, что образ, собранный Docker, можно запустить через Podman или развернуть в k8s без конвертации.


Анатомия образа: что физически лежит на диске

Возьмём типичный образ для DE — python:3.13-slim — и посмотрим, что мы скачиваем:

$ docker pull python:3.13-slim
3.13-slim: Pulling from library/python
6e909acdb790: Pull complete   # base layer (debian:12-slim rootfs, ~30MB)
4f4fb700ef54: Pull complete   # пустой слой (метаданные)
b1c40a1ec76d: Pull complete   # apt-installed пакеты (~15MB)
8b3e1a3e9b7d: Pull complete   # python runtime (~35MB)
3e2fa3a9c8e8: Pull complete   # pip + setuptools (~5MB)
Digest: sha256:5b7b9c0f6c9e3e8a...
Status: Downloaded newer image for python:3.13-slim

Пять слоёв, каждый ровно одна инструкция Dockerfile, который собирал этот образ. Если ты потом скачаешь python:3.13-slim-bookworm (того же семейства), Docker увидит, что базовый слой Debian уже есть локально, и пропустит его — Pull complete сменится на Already exists. Это и есть layer reuse.

Структура Docker-образа: манифест, конфиг, слои
Manifest ссылается на config и слои по их digest’ам
manifest.jsonsha256:5b7b...Manifest: корневой документ образа. Содержит media-types, ссылки на config и слои (через их digest), платформу. Это то, что подписывается digest'ом образа.
config ref
config.jsoncmd, env, historyImage Config: метаданные. CMD по умолчанию, ENV, WORKDIR, USER, EXPOSE, история сборки (одна запись на слой). Десериализуется при запуске контейнера.
Layer 0: baseСлой 0: базовый rootfs (debian:12-slim). Tar-архив с / -- bin, etc, lib, usr и т.д. Read-only.
diff
Layer 1: aptСлой 1: diff от предыдущего слоя. Содержит изменённые/добавленные файлы. Удалённые файлы помечаются whiteout-маркером.
diff
Layer 2: pythonСлой 2: добавляет Python runtime (/usr/local/bin/python, lib/python3.13). Не дублирует то, что было в base.
diff
Layer 3: pipСлой 3: pip, setuptools, wheel. Финальный read-only слой.

Манифест: чертёж образа

Манифест это короткий JSON, который описывает образ “сверху”. Запросим его руками через registry API:

$ TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/python:pull" | jq -r .token)

$ curl -s -H "Authorization: Bearer $TOKEN" \
    -H "Accept: application/vnd.oci.image.manifest.v1+json" \
    "https://registry-1.docker.io/v2/library/python/manifests/3.13-slim" | jq
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:c0a3...",
    "size": 5147
  },
  "layers": [
    {"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": "sha256:6e90...", "size": 28983291},
    {"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": "sha256:4f4f...", "size": 32},
    {"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": "sha256:b1c4...", "size": 14829003}
  ]
}

Заметь две вещи. Первая: каждый слой адресуется своим digest’ом. Docker daemon берёт этот digest и спрашивает у registry “дай мне blob sha256:6e90…” — registry возвращает tar-архив, daemon проверяет, что хэш совпадает, и кладёт слой в /var/lib/docker/overlay2/. Если хэш не совпал — Docker отвергнет файл. Это защита от подмены.

Вторая: размер каждого слоя известен заранее. Это позволяет Docker показывать прогресс-бар при pull и параллельно скачивать слои.


Иммутабельность: почему хэш это подпись

Содержимое образа полностью определяет его digest. Если ты пересоберёшь образ с другой версией pip, digest изменится. Если изменишь одну переменную в ENV, digest изменится. Если поменяется порядок инструкций в Dockerfile — digest изменится (потому что слои применяются по очереди, и нижний слой влияет на хэш upper-слоёв через chain-id).

Это означает три практических вещи для Data Engineer:

1. image@sha256:... это контракт. Если в проде запущен airflow@sha256:abc123, то на коллегиной машине с тем же digest’ом будет в точности тот же байт-в-байт runtime. Никаких “у меня работает”.

2. Невозможно “тихо” подменить образ. Если кто-то перезальёт library/python:3.13-slim с другим содержимым (например, добавит вредоносный код), digest изменится — твой CI с pinned python@sha256:... упадёт с ошибкой “digest mismatch”, а не молча запустит зловреда.

3. Слои дедуплицируются между образами. python:3.13-slim и python:3.13-slim-bookworm шарят базовый Debian-слой. На диске он лежит один раз. Docker считает количество ссылок и удаляет слой только когда последний образ, который на него ссылался, удалён.

TIP

Когда подписываешь production-deployment, всегда используй digest: image: postgres@sha256:5b7b..., а не image: postgres:16. Тег postgres:16 mutable — мейнтейнеры могут перезалить под этим тегом новый билд с патчами. Digest гарантирует bit-for-bit reproducibility.


VFS — виртуальная файловая система Linux

Образ vs контейнер: класс vs экземпляр

Самая частая путаница новичков: чем образ отличается от контейнера. Используем аналогию из ООП:

ОбразКонтейнер
Класс (template, blueprint)Экземпляр (instance, runtime)
Read-only, иммутабельныйRead-write, изменяемый
Лежит в registry / на дискеСуществует, пока процесс жив
Один образ — много контейнеровКаждый контейнер привязан к одному образу
docker pull postgres:16docker run postgres:16
Размер: фиксирован (по слоям)Размер: образ + writable layer
Не запускаетсяЗапускается, останавливается, удаляется
Образ → Контейнер: один шаблон, много экземпляров
Каждый контейнер получает свой writable-слой поверх общих read-only слоёв образа
Image: postgres:16read-only layersОбраз Postgres 16. Состоит из ~10 слоёв (Debian base, glibc, postgres binaries, init scripts). Все read-only, шарятся между контейнерами.
pg-dev (RW)Контейнер pg-dev. Свой writable-слой /var/lib/docker/overlay2/{id}/diff. Изменения здесь не видны другим контейнерам.
pg-test (RW)Контейнер pg-test. Тот же образ, свой writable-слой. Полностью изолирован от pg-dev.
pg-airflow (RW)Контейнер pg-airflow. Третий экземпляр того же образа.

Когда ты делаешь docker run postgres:16, Docker:

  1. Проверяет, есть ли образ локально (если нет — pull)
  2. Создаёт новый writable-слой поверх read-only слоёв образа
  3. Объединяет всё через overlayfs в один merged view (то, что видит процесс)
  4. Запускает entrypoint из image config

Образ при этом остаётся неизменным. Ты можешь запустить десять контейнеров из одного образа — они будут изолированы друг от друга, потому что у каждого свой writable-слой. Удалишь контейнеры — образ останется лежать на диске, готовый создать новые экземпляры.


Попробуй сам

Скачай маленький образ и посмотри на его внутренности:

# Pull маленького образа (~5MB)
docker pull alpine:3.21

# Посмотреть список локальных образов
docker images
# REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
# alpine       3.21      aded1e1a5b37   2 weeks ago   7.83MB

# Получить digest
docker inspect alpine:3.21 --format '{{.RepoDigests}}'
# [alpine@sha256:21dc6063fd678b478f57c0e13f47560d0...]

# Запустить 3 контейнера из одного образа
docker run -d --name a1 alpine:3.21 sleep 3600
docker run -d --name a2 alpine:3.21 sleep 3600
docker run -d --name a3 alpine:3.21 sleep 3600

# Все три — один образ, три экземпляра
docker ps
# Размер образа на диске НЕ умножается на 3 — слои шарятся
docker system df

# Cleanup
docker rm -f a1 a2 a3

Обрати внимание на docker system df: даже с тремя контейнерами alpine занимает всё те же ~8MB (плюс крошечный writable layer на каждый контейнер). Это сила слоёв.


Проверка знанийKnowledge check
Команда продакт-инженера обнаружила, что docker pull library/python:3.13-slim сегодня даёт другой digest, чем неделю назад. Является ли это нормальным поведением и что это значит для воспроизводимости?
ОтветAnswer
Это абсолютно нормально. Тег :3.13-slim mutable — мейнтейнеры регулярно перезаливают его, обновляя базовый Debian-слой (security patches), pip и т.п. Чтобы зафиксировать ровно тот образ, что работал неделю назад, нужно использовать digest: python@sha256:abc123... Тег это указатель на ID, digest это сам ID. Для production-deployment всегда pin'те digest; для dev можно тегом.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что физически представляет собой Docker-образ согласно OCI image-spec?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5