3.3. Tags и digests
Цели
После этого материала вы сможете:
- объяснить, почему tag не является идентификатором версии, и показать это экспериментально;
- объяснить разницу между digest манифеста и digest index у multi-platform образов;
- управлять pull policy и точно знать, когда Docker обращается к registry;
- закрепить версию образа по digest в
Dockerfileи вcompose.yaml; - объяснить, почему
docker pullможет не обновить образ, и заставить его обновиться; - выбрать стратегию закрепления версий под конкретную задачу.
Предварительные знания
- 3.1. Архитектура image — index, manifest, config;
- 2.7. Терминология Docker — структура имени образа.
Ключевые термины
| Термин | Объяснение |
|---|---|
tag | Изменяемая метка, указывающая на манифест или index внутри repository |
digest | Неизменяемый идентификатор: SHA-256 содержимого манифеста или index |
pull policy | Правило, определяющее, когда обращаться к registry за образом |
pinning | Закрепление конкретной версии образа |
mutable tag | Тег, который может быть перепривязан к другому образу |
immutable tag | Тег, перезапись которого запрещена настройками registry |
floating tag | Подвижный тег, намеренно указывающий на «текущую» версию: latest, stable, 3 |
Теория
Тег — это указатель, а не имя версии
В registry тег хранится как запись «имя → digest». Владелец repository может изменить эту запись в любой момент.
до обновления после обновления
───────────── ────────────────
3.13-slim ──► sha256:aaa... 3.13-slim ──► sha256:bbb...
latest ──► sha256:aaa... latest ──► sha256:bbb...
(sha256:aaa... всё ещё существует,
но на него ничто не указывает)
Это происходит регулярно и по хорошим причинам: выходит патч безопасности в базовом образе, и мейнтейнер пересобирает 3.13-slim, чтобы пользователи получили исправление. Версия Python осталась 3.13, но набор слоёв изменился.
Практическое следствие, которое трудно переоценить:
Утверждение «в production и в staging один и тот же образ, тег
v2.1» ничего не гарантирует, если между развёртываниями прошло время.
Digest гарантирует содержимое
Digest вычисляется из содержимого манифеста. Его нельзя перепривязать: изменение содержимого даёт другой digest.
python@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
Такая ссылка означает ровно один набор байтов. На любой машине, в любой момент времени.
Цена: нечитаемость. Из строки не понять, что это Python 3.13. Отсюда стандартная практика — писать оба:
FROM python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
Docker использует digest; тег остаётся для человека. Если тег и digest противоречат друг другу, приоритет у digest.
Два digest у multi-platform образа
Тонкость, которая регулярно сбивает с толку.
У multi-platform образа два уровня (урок 3.1):
тег "python:3.13-slim"
│
▼
index (digest: sha256:INDEX...) ← этот digest в RepoDigests
├── manifest amd64 (sha256:AMD...) ← этот digest у образа на amd64
└── manifest arm64 (sha256:ARM...)
| Digest | Что идентифицирует | Где встречается |
|---|---|---|
| index digest | Весь multi-platform образ | docker pull (строка Digest:), RepoDigests, docker images --digests |
| manifest digest | Образ для одной платформы | docker manifest inspect, ссылки внутри index |
Для закрепления версии используется index digest: он работает на любой архитектуре, и Docker сам выберет нужный манифест. Закрепление по manifest digest привяжет вас к одной платформе — иногда это нужно, но обычно нет.
Pull policy: когда Docker идёт в registry
Три политики, задаваемые флагом --pull у docker run и ключом pull_policy в Compose:
| Значение | Поведение |
|---|---|
missing (по умолчанию) | Обращаться к registry, только если образа нет локально |
always | Обращаться всегда, проверяя актуальность |
never | Никогда не обращаться; при отсутствии образа — ошибка |
Значение по умолчанию объясняет самое частое недоразумение:
docker run myapp:latest
Если myapp:latest уже загружен, Docker не проверит, не появилась ли новая версия. Он запустит то, что лежит локально, даже если в registry давно другой образ.
Именно поэтому в CI и в скриптах развёртывания добавляют явный docker pull или --pull=always.
Что делает docker pull при уже загруженном образе
docker pull python:3.13-slim при наличии образа локально:
- Запрашивает манифест по тегу.
- Сравнивает полученный digest с локальным.
- Совпал — выводит
Image is up to dateи завершается. - Не совпал — скачивает недостающие слои и перепривязывает тег локально.
Шаг 4 важен: старый образ не удаляется. Он остаётся на диске без тега — становится dangling image. Отсюда постепенное заполнение диска у тех, кто регулярно обновляет образы. Разбирается в уроке 3.4.
Стратегии закрепления
| Стратегия | Пример | Воспроизводимость | Удобство | Когда применять |
|---|---|---|---|---|
| Без тега | python | нет | высокое | Разовые эксперименты |
| Floating tag | python:3 | низкая | высокое | Локальные пробы |
| Minor-версия | python:3.13 | средняя | высокое | Разработка |
| Полная версия | python:3.13.9-slim | выше средней | среднее | Разработка, тесты |
| Версия + digest | python:3.13-slim@sha256:... | полная | низкое | Production, CI, базовые образы |
| Только digest | python@sha256:... | полная | очень низкое | Автоматика |
Практическая рекомендация курса:
- базовые образы в
Dockerfile— тег плюс digest, обновляется осознанно (можно автоматизировать через Renovate или Dependabot); - свои образы при развёртывании — по digest, полученному из сборки;
- локальная разработка — тег, читаемость важнее.
Обоснование: Dockerfile определяет, что попадёт в ваш артефакт. Если базовый образ подменится незаметно, вы получите другой артефакт при том же коде — и это обнаружится в самый неподходящий момент.
Внутренний механизм
Как вычисляется digest
Digest — это sha256 от байтов JSON-документа манифеста, ровно в том виде, в котором он хранится в registry.
Отсюда неочевидное следствие: даже семантически идентичные манифесты дадут разные digest, если различаются форматированием или порядком ключей. Поэтому манифесты передаются и хранятся байт в байт — их нельзя «переформатировать».
При загрузке Docker вычисляет хэш полученного документа и сверяет с запрошенным. Несовпадение — ошибка, загрузка прерывается. Это защита от повреждения и подмены, работающая без подписей.
Почему локальные образы иногда не имеют digest
docker build -t myapp:1.0 .
docker image inspect myapp:1.0 --format '{{.RepoDigests}}'
[]
Digest появляется только после взаимодействия с registry. Собранный локально образ ещё не имеет манифеста в том виде, в котором он будет опубликован, — точнее, у него нет привязки к repository в registry. После docker push digest появится.
Это объясняет, почему в CI digest берут из вывода docker push или из метаданных build-push-action, а не из локального inspect до публикации.
Команды и примеры
Тег и digest локально
docker pull python:3.13-slim
3.13-slim: Pulling from library/python
Digest: sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
Status: Downloaded newer image for python:3.13-slim
Строка Digest: — это index digest.
docker images python --digests --format 'table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}'
REPOSITORY TAG DIGEST IMAGE ID
python 3.13-slim sha256:bf503bb2243c5aad... c2f1e0d9b8a7
Три разных идентификатора в одной строке. Ещё раз, чем они отличаются:
| Поле | Что это | Область |
|---|---|---|
TAG | Изменяемый указатель | Registry |
DIGEST | Хэш index | Registry, неизменяем |
IMAGE ID | Хэш конфигурации | Локальная машина |
Загрузка по digest
DIGEST="$(docker image inspect python:3.13-slim \
--format '{{index .RepoDigests 0}}' | cut -d@ -f2)"
echo "$DIGEST"
docker rmi python:3.13-slim
docker pull "python@$DIGEST"
sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
...
Status: Downloaded newer image for python@sha256:bf503bb2...
Проверим, что получили то же самое:
docker images --digests | grep python
python <none> sha256:bf503bb2243c5aad... c2f1e0d9b8a7 ...
Обратите внимание: TAG теперь <none>. Образ загружен по digest и не имеет тега. Он полностью работоспособен:
docker run --rm "python@$DIGEST" python --version
Python 3.13.9
Присвоим ему тег для удобства:
docker tag "python@$DIGEST" python:3.13-slim
Демонстрация изменяемости тега
Воспроизведём ситуацию, когда под одним тегом оказываются разные образы. Используем локальный registry.
docker run -d --name reg -p 5000:5000 registry:2
sleep 2
mkdir -p /tmp/tagdemo && cd /tmp/tagdemo
# Версия 1
cat > Dockerfile <<'EOF'
FROM alpine:3.21
RUN echo "версия 1" > /version.txt
EOF
docker build -q -t localhost:5000/app:v1 . > /dev/null
docker push -q localhost:5000/app:v1
D1="$(docker image inspect localhost:5000/app:v1 \
--format '{{index .RepoDigests 0}}' | cut -d@ -f2)"
echo "digest версии 1: $D1"
Теперь пересоберём под тем же тегом:
cat > Dockerfile <<'EOF'
FROM alpine:3.21
RUN echo "версия 2 — другое содержимое" > /version.txt
EOF
docker build -q -t localhost:5000/app:v1 . > /dev/null
docker push -q localhost:5000/app:v1
D2="$(docker image inspect localhost:5000/app:v1 \
--format '{{index .RepoDigests 0}}' | cut -d@ -f2)"
echo "digest версии 2: $D2"
digest версии 1: sha256:1a2b3c4d5e6f...
digest версии 2: sha256:7g8h9i0j1k2l...
Тег v1 не изменился, а digest — изменился. Проверим, что теперь получит новый пользователь:
docker rmi localhost:5000/app:v1 > /dev/null
docker pull -q localhost:5000/app:v1 > /dev/null
docker run --rm localhost:5000/app:v1 cat /version.txt
версия 2 — другое содержимое
Тот, кто загрузил v1 вчера, и тот, кто загрузит сегодня, получат разные образы под одним тегом.
А по digest — всегда одно и то же:
docker run --rm "localhost:5000/app@$D1" cat /version.txt
docker run --rm "localhost:5000/app@$D2" cat /version.txt
версия 1
версия 2 — другое содержимое
Первый образ никуда не делся: он существует и доступен по своему digest. Потерялся только указатель на него.
Именно на этом свойстве строится откат в разделе 14.
Pull policy на практике
# образ есть локально — registry не опрашивается
docker run --rm localhost:5000/app:v1 cat /version.txt
# принудительная проверка актуальности
docker run --rm --pull=always localhost:5000/app:v1 cat /version.txt
# запрет обращения к registry
docker rmi localhost:5000/app:v1 > /dev/null
docker run --rm --pull=never localhost:5000/app:v1 cat /version.txt
docker: Error response from daemon: No such image: localhost:5000/app:v1
--pull=never полезен, когда нужно гарантировать использование локально собранного образа и исключить случайную загрузку из registry.
В Compose то же самое задаётся на уровне сервиса:
services:
api:
image: localhost:5000/app:v1
pull_policy: always
Проверка обновления без загрузки
docker pull python:3.13-slim
Повторно:
docker pull python:3.13-slim
3.13-slim: Pulling from library/python
Digest: sha256:bf503bb2243c5aad...
Status: Image is up to date for python:3.13-slim
Image is up to date означает: digest в registry совпал с локальным, скачивать нечего. Обмен занял килобайты.
Узнать digest в registry, не загружая образ:
docker manifest inspect python:3.13-slim -v \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
d = d if isinstance(d, list) else [d]
print('digest в registry:', d[0]['Descriptor']['digest'])
" 2>/dev/null || docker buildx imagetools inspect python:3.13-slim | head -3
Сравнение с локальным:
docker image inspect python:3.13-slim --format 'локально: {{index .RepoDigests 0}}'
Закрепление в Dockerfile
cd /tmp/tagdemo
DIGEST="$(docker image inspect python:3.13-slim \
--format '{{index .RepoDigests 0}}' | cut -d@ -f2)"
cat > Dockerfile.pinned <<EOF
# Базовый образ закреплён по digest.
# Тег оставлен для читаемости; при расхождении приоритет у digest.
FROM python:3.13-slim@${DIGEST}
WORKDIR /app
CMD ["python", "-c", "print('закреплённая сборка')"]
EOF
cat Dockerfile.pinned
docker build -q -f Dockerfile.pinned -t pinned:1 . > /dev/null
docker run --rm pinned:1
# Базовый образ закреплён по digest.
# Тег оставлен для читаемости; при расхождении приоритет у digest.
FROM python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
WORKDIR /app
CMD ["python", "-c", "print('закреплённая сборка')"]
закреплённая сборка
Теперь сборка воспроизводима: даже если мейнтейнеры перепривяжут тег 3.13-slim, ваша сборка возьмёт прежний образ.
Проверка приоритета digest над тегом:
cat > Dockerfile.conflict <<EOF
FROM python:3.9-slim@${DIGEST}
CMD ["python", "--version"]
EOF
docker build -q -f Dockerfile.conflict -t conflict:1 . > /dev/null
docker run --rm conflict:1
Python 3.13.9
В теге написано 3.9, а получили 3.13 — digest победил. Это иллюстрирует, почему тег рядом с digest носит справочный характер и должен поддерживаться в актуальном состоянии вручную.
Digest в Compose
services:
api:
image: python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
Просмотр всех платформ и их digest
docker buildx imagetools inspect python:3.13-slim
Name: docker.io/library/python:3.13-slim
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
Manifests:
Name: docker.io/library/python:3.13-slim@sha256:a1b2c3d4e5f6...
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/amd64
Name: docker.io/library/python:3.13-slim@sha256:b2c3d4e5f6a7...
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/arm64/v8
...
Верхний Digest: — index digest, тот, что используется для закрепления. Ниже — manifest digest по платформам.
Уборка
docker rm -f reg
docker rmi pinned:1 conflict:1 registry:2 2>/dev/null || true
docker images --filter 'reference=localhost:5000/*' -q | xargs -r docker rmi -f
rm -rf /tmp/tagdemo
Практическое упражнение
Задание. Напишите скрипт pin-check.sh <dockerfile>, который проверяет закрепление базовых образов в Dockerfile и помогает его выполнить.
Скрипт должен:
- Найти все инструкции
FROM(учитывая multi-stage иAS). - Для каждой определить: закреплён ли образ по digest, используется ли floating tag, отсутствует ли тег вовсе.
- Для незакреплённых образов получить актуальный digest из registry.
- Вывести готовые строки
FROMс digest, которые можно скопировать вDockerfile. - Вернуть ненулевой exit code, если найден хотя бы один незакреплённый образ, — чтобы скрипт годился для CI.
Учтите: стадии multi-stage могут ссылаться друг на друга (FROM builder AS final) — такие ссылки закреплять не нужно.
Подсказки
Подсказка 1
Признак закрепления — наличие @sha256: в имени образа.
Подсказка 2
Имена стадий собираются из FROM ... AS <name>. Если очередной FROM ссылается на уже известное имя стадии, это внутренняя ссылка, а не внешний образ.
Подсказка 3
Получить digest, не загружая образ:
docker buildx imagetools inspect <image> --format '{{println .Manifest.Digest}}'
Запасной вариант — загрузить и взять из RepoDigests.
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/usr/bin/env bash
# pin-check.sh — проверка закрепления базовых образов в Dockerfile.
set -uo pipefail
FILE="${1:?Использование: $0 <dockerfile>}"
[ -r "$FILE" ] || { echo "Файл не читается: $FILE" >&2; exit 2; }
declare -A STAGES=()
unpinned=0
get_digest() {
local img="$1" d=""
d="$(docker buildx imagetools inspect "$img" \
--format '{{println .Manifest.Digest}}' 2>/dev/null | head -1)"
if [ -z "$d" ]; then
docker pull -q "$img" > /dev/null 2>&1 \
&& d="$(docker image inspect "$img" \
--format '{{if .RepoDigests}}{{index .RepoDigests 0}}{{end}}' \
2>/dev/null | cut -d@ -f2)"
fi
printf '%s' "$d"
}
echo "=== Проверка: $FILE ==="
echo
while read -r _ image rest; do
[ -z "${image:-}" ] && continue
# Имя стадии из "AS <name>"
stage=""
if [[ "$rest" =~ [Aa][Ss][[:space:]]+([A-Za-z0-9_.-]+) ]]; then
stage="${BASH_REMATCH[1]}"
fi
# Ссылка на предыдущую стадию — не внешний образ
if [ -n "${STAGES[$image]:-}" ]; then
printf ' [—] %-42s ссылка на стадию, закрепление не требуется\n' "$image"
[ -n "$stage" ] && STAGES["$stage"]=1
continue
fi
[ -n "$stage" ] && STAGES["$stage"]=1
# Уже закреплён
if [[ "$image" == *"@sha256:"* ]]; then
printf ' [+] %-42s закреплён\n' "${image%%@*}"
continue
fi
unpinned=$((unpinned + 1))
name="${image%%:*}"
tag="${image##*:}"
[ "$tag" = "$image" ] && tag="latest (не указан)"
printf ' [!] %-42s НЕ закреплён (тег: %s)\n' "$image" "$tag"
digest="$(get_digest "$image")"
if [ -n "$digest" ]; then
printf ' → FROM %s@%s\n' "$image" "$digest"
else
printf ' → digest получить не удалось (нет доступа к registry?)\n'
fi
done < <(grep -iE '^[[:space:]]*FROM[[:space:]]' "$FILE")
echo
if [ "$unpinned" -eq 0 ]; then
echo "Все внешние образы закреплены по digest."
exit 0
else
echo "Не закреплено образов: $unpinned"
echo "Скопируйте предложенные строки FROM в $FILE."
exit 1
fi
Проверка на multi-stage файле:
mkdir -p /tmp/pintest && cd /tmp/pintest
cat > Dockerfile <<'EOF'
FROM python:3.13-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir build
FROM builder AS tester
RUN echo "тесты"
FROM python:3.13-slim@sha256:0000000000000000000000000000000000000000000000000000000000000000 AS runtime
COPY --from=builder /app /app
EOF
chmod +x pin-check.sh
./pin-check.sh Dockerfile; echo "exit code: $?"
Ожидаемый вывод:
=== Проверка: Dockerfile ===
[!] python:3.13-slim НЕ закреплён (тег: 3.13-slim)
→ FROM python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251
[—] builder ссылка на стадию, закрепление не требуется
[+] python:3.13-slim закреплён
Не закреплено образов: 1
Скопируйте предложенные строки FROM в Dockerfile.
exit code: 1
Разбор логики.
Скрипт различает три случая, и это главное в задании:
- внешний образ без digest — требует закрепления;
- внешний образ с digest — в порядке;
- ссылка на стадию (
FROM builder AS tester) — не образ, закреплять нечего.
Третий случай легко пропустить, и тогда скрипт будет пытаться загрузить из registry образ с именем builder.
Ненулевой exit code делает скрипт пригодным для CI: шаг проверки упадёт, если кто-то добавит незакреплённый базовый образ.
Как поддерживать закрепление в актуальном состоянии. Закрепив digest, вы перестаёте получать обновления безопасности базового образа автоматически. Это осознанный компромисс: обновление становится явным изменением в репозитории, которое проходит ревью и тесты. На практике обновление автоматизируют инструментами вроде Renovate или Dependabot, которые создают pull request с новым digest.
Проверка результата
docker pull -q alpine:3.21
docker image inspect alpine:3.21 --format 'digest: {{index .RepoDigests 0}}'
docker image inspect alpine:3.21 --format 'image ID: {{.Id}}'
Убедитесь, что различаете эти два значения и можете объяснить, какое из них использовать для закрепления версии.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
| Расчёт на неизменность тега | Тег выглядит как версия | Тег — изменяемый указатель. Для гарантии использовать digest |
docker run без обновления образа | Pull policy по умолчанию — missing | Явный docker pull или --pull=always |
| Закрепление по manifest digest вместо index | Оба выглядят одинаково | Использовать index digest из RepoDigests — он работает на всех платформах |
| Ожидание digest у локально собранного образа | Digest появляется после взаимодействия с registry | Брать digest из вывода docker push |
Развёртывание по latest | Кажется, что это «текущая версия» | Развёртывать по digest или по неизменяемому тегу |
Тег и digest в FROM разошлись | Обновили digest, забыли тег | Обновлять обе части; помнить, что приоритет у digest |
Заполнение диска после серии docker pull | Старые образы теряют тег, но остаются | Периодически удалять dangling images — урок 3.4 |
| Закрепили digest и забыли про обновления | Патчи безопасности перестали приходить | Автоматизировать обновление digest через Renovate или Dependabot |
Контрольные вопросы
На понимание:
- Почему один и тот же тег в разное время может указывать на разные образы?
- Чем index digest отличается от manifest digest и какой использовать для закрепления?
- Почему
docker run myapp:latestможет запустить устаревший образ? - Что происходит со старым образом, когда
docker pullполучает новую версию под тем же тегом? - Почему у локально собранного образа поле
RepoDigestsпустое?
На применение:
- Как узнать digest образа в registry, не загружая его?
- Как закрепить базовый образ в
Dockerfileтак, чтобы строка оставалась читаемой? - Как гарантировать, что в container запустится именно локально собранный образ?
На диагностику:
- Сборка одного и того же коммита вчера и сегодня дала образы разного размера. Что проверить в первую очередь?
- В
FROMуказан тег3.9-slim, аpython --versionв container выводит 3.13.9. Как это возможно?
Краткое резюме
- Тег — изменяемый указатель «имя → digest», который владелец repository может перепривязать.
- Digest вычисляется из содержимого и неизменен: тот же digest означает те же байты.
- У multi-platform образа два digest: index (для всего образа) и manifest (для платформы).
- Для закрепления используется index digest — он работает на любой архитектуре.
- Pull policy по умолчанию —
missing: при наличии образа локально registry не опрашивается. --pull=alwaysзаставляет проверить актуальность,--pull=neverзапрещает обращение.- При обновлении тега старый образ остаётся на диске без тега и становится dangling.
- В
FROMпишут и тег, и digest: тег для человека, digest для гарантии. - При расхождении тега и digest приоритет у digest.
- Закрепление по digest отключает автоматические обновления — их нужно автоматизировать отдельно.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| docker pull reference | https://docs.docker.com/reference/cli/docker/image/pull/ | Загрузка по digest, поведение при уже загруженном образе, Image is up to date |
| docker run reference | https://docs.docker.com/reference/cli/docker/container/run/ | Флаг --pull и значения missing, always, never |
| docker images reference | https://docs.docker.com/reference/cli/docker/image/ls/ | Флаг --digests, различие колонок DIGEST и IMAGE ID |
| docker buildx imagetools inspect | https://docs.docker.com/reference/cli/docker/buildx/imagetools/inspect/ | Просмотр index и манифестов по платформам, digest каждого уровня |
| Dockerfile reference: FROM | https://docs.docker.com/reference/dockerfile/#from | Синтаксис image:tag@digest, приоритет digest |
| Building best practices | https://docs.docker.com/build/building/best-practices/ | Рекомендация закреплять базовые образы по digest |
| Compose services: pull_policy | https://docs.docker.com/reference/compose-file/services/ | Управление pull policy в Compose |
| OCI Image Specification | https://github.com/opencontainers/image-spec | Определение digest, image index, привязка тегов |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Управление images
Главное оглавление