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

Теги и digest’ы: mutable label vs immutable id

Каждый раз, когда ты пишешь docker pull postgres:16 или image: redis:7 в docker-compose, ты используешь тег. Тег это удобно: его легко читать, он несёт смысл (postgres:16, python:3.13-slim, airflow:2.10). Проблема в том, что тег это всего лишь mutable указатель — мейнтейнер образа может в любой момент перенаправить его на другой набор слоёв.

В этом уроке разбираемся, почему image:tag не даёт никаких гарантий воспроизводимости, что такое digest и почему в production-пайплайнах Data Engineer всегда pin’ит через image@sha256:....


Тег это not-immutable label

Когда ты делаешь docker pull postgres:16, Docker идёт в registry, говорит “дай мне манифест для тега 16 репозитория library/postgres” и получает в ответ ссылку на конкретный manifest digest. В registry хранится таблица:

postgres:16            → sha256:abc123 (текущий manifest)
postgres:16.6          → sha256:abc123 (тот же, что 16)
postgres:16.6-bookworm → sha256:abc123
postgres:latest        → sha256:abc123

Когда мейнтейнеры PostgreSQL выпускают patch-релиз 16.7, они пересобирают образ и перезаливают теги:

postgres:16            → sha256:def456  (теперь это 16.7)
postgres:16.7          → sha256:def456
postgres:16.7-bookworm → sha256:def456
postgres:latest        → sha256:def456
postgres:16.6          → sha256:abc123  (этот ОБЫЧНО остаётся, но не гарантировано)

То есть тег postgres:16 через неделю может указывать на другой манифест — и поведение твоего приложения может измениться. Поменялась версия libc, поправили баг, добавили security patch — теоретически может приплыть что угодно.

WARNING

Worst case реальной истории: в 2019 году тег python:3.7-alpine после обновления базового Alpine стал ломать установку numpy через pip — потому что в alpine 3.10 убрали один из system libs. Команды, у которых был запиннен python:3.7-alpine в CI, утром обнаружили красные сборки на коммитах, которые накануне были зелёными. Никто их код не менял — поменялся образ под mutable тегом.


Digest: контент-адресуемое имя

Криптографические хэши и целостность данных

В отличие от тега, digest это SHA256-хэш манифеста. Манифест это байты — изменился хоть один байт (порядок слоёв, mediaType, любая мета) — digest изменится. Это значит:

  • Один и тот же digest всегда указывает на один и тот же контент. Навсегда. Бит-в-бит.
  • Невозможно сделать так, чтобы под тем же digest’ом оказался другой образ — это нарушит криптографию SHA256.
  • Если registry получает запрос library/python@sha256:abc..., он возвращает ровно ту версию, которую ты запросил, даже если тег python:3.13-slim уже давно перенаправлен на другой digest.
# Pin через digest
docker pull python@sha256:5b7b9c0f6c9e3e8a4f0d8b6a1e2c3d4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1

# Это будет ВСЕГДА бит-в-бит один и тот же образ
# Даже если тег python:3.13-slim сменили на новый билд

Как получить digest

Есть несколько способов:

# 1. После pull -- Docker выводит digest строкой Digest:
docker pull postgres:16
# 16: Pulling from library/postgres
# Digest: sha256:5d3e8b7f0a2c1e4b6d9f8a7c5e3b1f0d9c8b7a6e5d4c3b2a1f0e9d8c7b6a5f4e3
# Status: Image is up to date for postgres:16
# docker.io/library/postgres:16

# 2. docker inspect (показывает RepoDigests -- хэш образа в виде, как он лежит в registry)
docker inspect postgres:16 --format '{{index .RepoDigests 0}}'
# postgres@sha256:5d3e8b7f...

# 3. docker manifest inspect (без pull)
docker manifest inspect postgres:16 | grep -A1 "manifests" | head -10
# Для multi-arch образов покажет manifest list со списком архитектур и их digest'ов

# 4. Прямо в коде compose
# docker-compose.yml:
# services:
#   db:
#     image: postgres@sha256:5d3e8b7f0a2c1e4b6d9f8a7c5e3b1f0d9c8b7a6e5d4c3b2a1f0e9d8c7b6a5f4e3
Тег vs Digest: mutable указатель vs content-addressable id
Тег может смещаться во времени, digest — никогда
postgres:16time T0Утро понедельника. Тег указывает на manifest sha256:abc....
resolve
sha256:abc...manifestКонкретный манифест с конкретными слоями.
postgres:16time T1Через неделю мейнтейнеры выпустили 16.7. Тот же тег теперь указывает на новый manifest.
resolve
sha256:def...другой manifestНовый манифест, новые слои. Контент изменился -- digest другой.
postgres@sha256:abc...any timeDigest всегда указывает на один и тот же манифест. Иммутабельно по криптографии SHA256.
resolve
sha256:abc...тот же манифестГарантированно тот же контент. Если в registry удалили этот digest -- pull упадёт с 404, но не вернёт другие данные.

Спектр стабильности тегов

Не все теги одинаково “плавающие”. У хороших образов есть convention:

ТегЧто значитМеняется?
latestСамая свежая стабильная сборкаЧасто (каждый релиз)
16 или python:3Major versionЧасто (каждый patch)
16.6 или python:3.13Minor versionИзредка (security patches)
16.6.1 или python:3.13.0Patch versionПочти никогда
16-bookworm-20250115Конкретный билд с датойНикогда (если соблюдают convention)
image@sha256:abc...DigestНикогда (immutable by crypto)

Чем “уже” версия — тем стабильнее тег. Но даже python:3.13.0 теоретически может быть перезалит (мейнтейнер исправил критический баг). Только digest даёт криптографическую гарантию.


Стратегии pinning для Data Engineer

В DE-практике встречается три подхода:

1. Pin через digest везде где можно. Production, CI-pipeline, конфиги Airflow для PythonOperator с custom image. Гарантирует bit-for-bit воспроизводимость на годы вперёд. Минус: некрасиво, требует обновлять руками.

services:
  postgres:
    image: postgres@sha256:5d3e8b7f0a2c1e4b6d9f8a7c5e3b1f0d9c8b7a6e5d4c3b2a1f0e9d8c7b6a5f4e3

2. Pin через semver-теги + monitoring. postgres:16.6.1-bookworm, redis:7.4.0. Удобно читать, изменяется только по security-update. Совмещается с automated dependency bot’ом (Renovate, Dependabot), который видит новые версии в registry и предлагает PR с обновлением.

services:
  postgres:
    image: postgres:16.6.1-bookworm   # ок для prod при наличии monitoring

3. Floating tag (:latest, :16). Только для local dev и quick prototypes. Никогда в проде, никогда в CI, который должен быть воспроизводим.

services:
  postgres:
    image: postgres:16   # OK для docker-compose.dev.yml, не для prod
TIP

Распространённый workflow: в dev используется postgres:16 (всегда свежий), в стейджинге postgres:16.6.1-bookworm, в проде postgres@sha256:5d3e.... Перед промоушеном в прод resolve тега → digest и pin’ишь. Это позволяет балансировать удобство и воспроизводимость.


Почему :latest опасен

:latest это просто convention — это не что-то особенное в Docker, это просто тег с именем latest, который мейнтейнеры по соглашению используют как alias для “самой свежей стабильной сборки”. Проблемы:

1. Семантика непредсказуема. Что значит latest для apache/airflow? Последний стабильный 2.x? Или новый 3.x? Это зависит от мейнтейнера. У некоторых образов latest это вообще dev-build (например, mongo:latest долго был довольно experimental).

2. Невозможно понять, что у тебя запущено. docker ps покажет app:latest, но это может быть build от прошлой недели или сегодняшний. Нельзя сказать “верните мне ту же версию, что была вчера” — она уже перезатёрта.

3. В compose файлах нет default pull_policy. До Docker Compose v2.30 при docker compose up локально кэшированный образ не перетягивался даже если в registry лежала новая версия. То есть :latest на двух машинах в один момент времени мог означать разное.

4. Кэш CI ломается непредсказуемо. В CI обычно docker pull image:latest сразу перед билдом. Сегодня собралось — завтра упало, потому что image:latest получил breaking change в какой-нибудь системной либе.

Правило простое: в production-конфигах и CI — никогда :latest. В DE-практике часто блокируется policy-инструментами типа Hadolint:

DL3007 warning: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag

Resolve тега в digest для compose

Полезный трюк — resolve тега в digest перед production deploy:

# Получить digest текущего :16 тега
docker pull postgres:16
docker inspect postgres:16 --format '{{index .RepoDigests 0}}'
# postgres@sha256:5d3e8b7f0a2c1e4b6d9f8a7c5e3b1f0d9c8b7a6e5d4c3b2a1f0e9d8c7b6a5f4e3

# Скопировать в production compose
# image: postgres@sha256:5d3e8b7f0a2c1e4b6d9f8a7c5e3b1f0d9c8b7a6e5d4c3b2a1f0e9d8c7b6a5f4e3

# Через 3 месяца хочешь обновить
docker pull postgres:16             # подтянет новый
docker inspect postgres:16 --format '{{index .RepoDigests 0}}'
# postgres@sha256:новый_digest
# вписываешь в compose, тестируешь, выкатываешь

В крупных DE-командах этот процесс автоматизирован: Renovate-bot мониторит теги в registry, открывает PR с обновлением digest’а, запускает CI, мерджится по approval.


Попробуй сам

Сравни поведение тега и digest’а:

# Pull через тег
docker pull alpine:3.21

# Получи digest
ALPINE_DIGEST=$(docker inspect alpine:3.21 --format '{{index .RepoDigests 0}}')
echo $ALPINE_DIGEST
# alpine@sha256:21dc6063fd678b478f57c0e13f47560d0...

# Pull через digest -- получишь ровно тот же image id
docker pull $ALPINE_DIGEST

# Посмотри: оба тега и digest указывают на один image
docker images alpine
# REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
# alpine       3.21      aded1e1a5b37   3 weeks ago    7.83MB
# alpine       <none>    aded1e1a5b37   3 weeks ago    7.83MB    (через digest)

# А теперь представь, что мейнтейнер обновил alpine:3.21
# Тег теперь укажет на новый digest, а pull через старый digest вернёт прежний

Проверка знанийKnowledge check
Команда деплоит сервис в прод с image: redis:7. На CI сборка прошла зелёной, тесты прошли, deploy успешен. Через два дня production падает: Redis запустился с другим default-конфигом и приложение получает ConnectionRefusedError. Что нужно изменить в pipeline?
ОтветAnswer
Pin образ через digest или, в крайнем случае, через точный semver-тег с patch-версией. Использование redis:7 (major-only) означает, что между CI и prod-deploy в registry мог быть перезалит тег, и на проде запустилась минорная версия с другим поведением. Правильный workflow: в production-compose всегда image: redis@sha256:... (resolve digest на стадии CI и pin), или хотя бы image: redis:7.4.1-alpine. Дополнительно стоит включить Renovate/Dependabot для автоматизированного обновления digest'ов через PR с прохождением полного CI.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чем отличается image:tag от image@sha256:digest в плане иммутабельности?

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

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

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

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