Прямой ответ
Тег образа — не «версия по умолчанию», а указатель на слой-снимок, и почти все проблемы деплоя «образ не тот» растут из вольного обращения с указателями. Рабочая стратегия из четырёх правил. (1) Immutable-теги на каждый билд: myapp:1.4.2 (semver — версионирование) и/или myapp:sha-a1b2c3d (git short-sha — уникален на коммит, прослеживаемость до строки кода — «какой код в этом образе» отвечает мгновенно); тег не перезаписывается никогда (пересборка того же кода — новый тег; «перетегировали 1.4.2 после фикса» — инцидент-расследование превращается в ад — два разных образа под одним именем). (2) latest — антипаттерн в проде (мутабельный указатель: деплой latest = «разверни что-нибудь» — откат невозможен (на что? latest уже другой), кеш-ноды тянут разное, «работало вчера» непроверяемо — latest законен только как «свежий dev-билд» в разработке; прод-манифесты/helm-values — только immutable-теги — конфиг-дисциплина); (3) продвижение, не пересборка: образ собирается один раз (CI), теги-указатели двигаются по окружениям (retag/stage→prod — crane copy/registry-репликация — один и тот же набор байтов проходит dev→stage→prod — «в проде пересобрали» нарушает саму идею тестирования — то, что проверено на стейдже, должно быть бит-в-бит в проде — деплой-стратегии); (4) digest как абсолютная истина (myapp@sha256:abc... — контент-адрес — не меняется никогда; K8s-манифесты критичных сервисов — по digest (тег могут перетегировать, digest — нет), в логах/инвентаризации — digest рядом с тегом — «что реально крутится» проверяемо — kubectl get pods -o jsonpath по imageID). Инфраструктура: registry с retention-политиками (GHCR/Harbor/ECR — правила «хранить N тегов/последние M дней/только теги с префиксом v*» — без очистки registry пухнет сотнями гигабайтов — CI-стоимость растёт молча) и сканированием уязвимостей (trivy/Grype-скан каждого пуша — аудит-дисциплина в контейнерном варианте — критичные CVE блокируют продвижение в prod — политика, не пожелание).
Теги: схемы и семантика
Схемы тегирования (комбинируются — один образ, несколько указателей): semver (1.4.2 — релизы; minor-ветки 1.4.x — для патч-линий — мутабельный «указатель ветки» ок для библиотек-образов, не для деплоя), git-sha (sha-a1b2c3d — каждый CI-билд — полная прослеживаемость; короткий sha — читаем, полный — в аннотациях), branch (main — «свежий с main» — dev-среда; мутабельность осознанна и ограничена средой), дата (2026.10.21 — nightly-сборки — календарная сортировка), окружение (staging/prod — мутабельные указатели «текущее в среде» — удобны обзору, но деплой идёт не по ним, а по immutable (указатель двигается после успешного деплоя — как отражение факта, не команда)); latest — по остаточному принципу (удобство docker pull без тега — держать = дубликат последнего релиза, осознавая риски — часть команд удаляет latest вовсе — «pull без тега не работает» лучше «pull непредсказуемого»); multi-arch теги (manifest list — 1.4.2 покрывает amd64/arm64 — локальная сборка на arm-ноутбуке тянет свой arch одним тегом — buildx --platform — прозрачность обязательна в 2026); аннотации/labels внутри образа (org.opencontainers.image.revision=sha, version, created — OCI-стандарт — docker inspect отвечает «что за образ» без внешних систем — docker-практика — LABEL в последнем слое бесплатен). Анти-схемы: «тег = дата пуша в продакшен» (две сборки в день — коллизия/перезапись), «версия в теге не соответствует версии в коде» (app --version говорит одно, тег другое — доверие к обоим падает — версия из сборки — single source — CI ставит оба из одного места), «dev-теги в прод-манифестах» (feature-xyz-v3 в helm-values прода — веточная гигиена — фича-ветки не деплоятся в prod, мержатся).
Registry: выбор, очистка, безопасность
Варианты: GHCR (GitHub Container Registry — привязка к репо/правам — бесплатный старт для CI на Actions — GITHUB_TOKEN из коробки, retention-правила), Harbor (self-hosted — проекты/RBAC/репликация/сканирование trivy/подписи — «серьёзный» уровень для компаний — K8s-деплой (helm)), managed (ECR/GCR/ACR — облачная интеграция/IAM), Docker Hub (публичные образы; приватные — лимиты — rate-limit на pull — CI-пайплайны на хабе упираются в лимиты анонимных пуллов — зеркалирование базовых образов в свой registry — практика против «базовый образ пропал/замедлился»); базовые образы — пиннить по digest (FROM python:3.12.6-slim@sha256:... — неизменяемость основы — republish тега не меняет вашу сборку — multistage-дисциплина). Очистка (retention): правила по репозиторию («хранить: все v*-теги релизов, последние 30 sha-билдов, удалять старше 90 дней» — Harbor/GHCR/ECR умеют нативно; без правил — терабайты мёртвых слоёв — деньги и замедление списков; untagged-образы — удалять агрессивнее (нет указателя — никто не ссылается — мусор по определению); GC registry — после удаления тегов слои физически чистятся garbage-коллектором — «удалил, место не вернулось» — GC не запущен — знать механику). Безопасность: приватность по умолчанию (публичный образ «для теста» с секретом в слое — классика утечек — слои вечны, удаление тега не чистит контент — docker-секреты: build-secrets через --mount, не COPY), сканирование CVE на пуш (trivy CI-шаг — критичные — блокировка тега/продвижения), подписи образов (cosign/sigstore — «образ собран нашим CI» проверяемо при деплое — admission-контроллер в K8s валидирует подпись — supply chain (аудит) замыкается), доступ — least privilege (CI-робот — push в свои репо, прод-кластер — pull-only; «все со всеми паролями registry» — ротация станет инцидентом).
Практический сценарий: от latest-деплоя к прослеживаемому конвейеру
Исходник: «деплоим latest» (docker-compose pull && up на сервере — стек); инцидент: пятничный баг в проде, «откатите» — откатывать не на что (latest перезаписан тремя билдами после — какой был «до» — неизвестно); образы в Docker Hub, 180 ГБ (очистки нет), базовый образ — FROM python:3.12 без пина. Реформа (две недели фоново): (1) CI (Actions) — сборка на каждый merge в main: теги sha-<short> + main + (на релиз-тегах git) vX.Y.Z; push в GHCR (GITHUB_TOKEN — ноль новых секретов); labels OCI (revision/version/date); (2) продвижение: стейдж-деплой — по sha-тегу автоматически; прод — ручной approval с тем же sha (crane copy stage→prod-указатель; helm-values прода обновляется PR'ом с immutable-тегом — чарты — деплой-артефакт в git — «что в проде» читается из репозитория); (3) откат — смена тега в values на предыдущий sha — минуты, проверено учебно (откат на стейдже — 3 минуты до «как было» — вместо «пересобираем старый код, надеемся, что он тот же»); (4) latest — оставлен как alias последнего релиза (не используется в деплоях — только «попробовать руками»); (5) retention GHCR: релизы v* — вечно, sha-билды — 60 дней, untagged — 7 (180 ГБ → 34 ГБ за первый GC — экономика сразу); (6) trivy-скан на пуш (критичные CVE — джоб падает — две находки первого дня: CVE в базовом слое (обновление базы) и в забытой dev-зависимости в prod-образе (multistage вычистил — образ −110 МБ — бонус оптимизации); (7) базовый образ — пин по digest + dependabot-обновления (обновления); (8) K8s-манифесты критичных сервисов — imageID по digest (тег-перезапись не подменит прод — belt-and-suspenders к immutable-тегам). Итог квартала: инцидент «что крутится в проде» — закрыт классом (ответ в git-values + digest в кластере), откаты — рутина минут, «пятница-деплой» перестал быть лотереей (откат предсказуем — деплой-культура), место registry под контролем; извлечённое правило в ADR: «в проде — только immutable-теги/digest, продвижение копированием, latest — не деплой-артефакт».
FAQ
Сколько тегов на один образ — нормально? Нормально несколько указателей на один digest: sha-тег (прослеживаемость) + semver (релиз) + окруженческий указатель (prod — как отражение) — 2–3 тега типичны; «тег на каждый чих» (per-branch×per-date×per-env — десятки мутабельных) — шум и путаница; правило: тег отвечает на вопрос («что за код» — sha, «какой релиз» — semver, «что сейчас в среде» — env-указатель) — лишние вопросы — лишние теги; digest под всеми — один и тот же образ, указатели бесплатны (слои дедуплицированы — теги — байты метаданных).
Можно ли перетегировать образ (1.4.2 → 1.4.3 без пересборки)?
Технически — да (retag/copy — новый тег на существующий digest — продвижение stage→prod ровно это и делает — легитимно); семантически для semver — осторожно: 1.4.3 должен соответствовать коду версии 1.4.3 (тег на digest другого кода — ложь в метаданных — инвентаризация/расследования сойдут с ума); правило: semver-теги ставит сборка соответствующего кода, копии двигают окруженческие/промо-указатели (prod/staging), не версии; «обнаружили баг в 1.4.2 — перетегировали исправленную сборку как 1.4.2» — запрещённый приём (исправление = 1.4.3 — semver — перезапись релиза ломает доверие и откаты).
Нужен ли свой (self-hosted) registry? Малым — нет (GHCR/managed закрывают: цена/retention/сканирование из коробки — ноль обслуживания); self-hosted Harbor оправдан при: on-prem/air-gapped (образы не покидают периметр), десятках команд (проекты/RBAC/квоты/репликация между ЦОД — централизованная политика), больших объёмах (трафик pull из облака — деньги; локальный registry + репликация нужного — экономия), требованиях подписей/сканирования своей политикой (Harbor-политики на продвижение — gate-контроль); «Harbor потому что серьёзно» при команде 5 человек и GHCR-потребностях — операционная стоимость на пустом месте (инфраструктурный выбор — та же логика «масштаб диктует»).
Как узнать, какой код в работающем контейнере, прямо сейчас?
Цепочка проверок: docker inspect <container> → Config.Image + imageID (digest) → сравнение с registry (skopeo inspect/GHCR-страница — какой тег на этом digest); внутри образа — OCI-labels (org.opencontainers.image.revision → git sha — если CI их ставит — ставьте), версия приложения (app --version — из сборки — пакетирование — version из метаданных, не хардкод); в K8s — kubectl describe pod (Image + ImageID) — digest против values-тега — расхождение = дрейф (кто-то обновил руками — policy-запрет ручных правок — GitOps-подход); «ответ занял 10 минут раскопок» — сигнал: labels/version/immutable-теги не внедрены — час настройки CI закрывает вопрос навсегда.
Источники: документация Docker Hub, Harbor, GitHub Container Registry — ссылки в шапке.