Вы собрали образ у себя на ноутбуке — теперь нужно, чтобы он оказался на сервере. Реестр образов — это именно то место, через которое образ переезжает с одной машины на другую, и откуда CI берёт и кладёт образы автоматически.
Там же кроется главная ловушка: имя с тегом — это не сам образ, а подвижный указатель на него.
Два тега на одном образе ничего не дублируют — в реестре лежат те же 240 МБ. Но повторный push перевешивает latest на новый образ, и два сервера с одним тегом окажутся на разных образах; неподвижны только sha-тег коммита и дайджест.
Что такое реестр образов
Реестр образов (registry) — это хранилище Docker-образов, доступное по сети. Принцип тот же, что у репозитория git: вы отправляете (push) образ в реестр, а потом скачиваете (pull) его на любой машине — локально, на сервере, в Kubernetes.
Самые распространённые реестры:
- Docker Hub (
hub.docker.com) — публичный реестр по умолчанию. Бесплатный для публичных образов; приватные — ограничены на бесплатном тарифе. Когда вы пишетеdocker pull nginx, Docker идёт именно туда. - GitHub Container Registry (
ghcr.io) — реестр от GitHub, удобен, если код уже там. Права управляются через токены GitHub. - Приватные реестры — можно поднять собственный (например, через
registry:2или Harbor) или использовать реестр облачного провайдера (Amazon ECR, Google Artifact Registry, Yandex Container Registry).
Все они работают по единому протоколу — OCI Distribution Spec, поэтому команды docker push / docker pull одинаковы вне зависимости от реестра.
Именование образов и теги
Полное имя образа выглядит так:
<реестр>/<пространство-имён>/<имя>:<тег>
Несколько примеров:
| Имя образа | Где лежит |
|---|---|
nginx:1.27 | Docker Hub, официальный образ |
myuser/myapp:latest | Docker Hub, образ пользователя |
ghcr.io/myorg/backend:v1.4.2 | GitHub Container Registry |
registry.company.ru/team/api:sha-abc1234 | приватный реестр компании |
Если реестр не указан — Docker Hub. Если тег не указан — latest.
Тег — это метка, приклеенная к конкретному образу целиком: к его манифесту, то есть к списку слоёв и настроек. Не к отдельному слою — к набору. Теги изменяемы: latest сегодня и latest через неделю могут указывать на разные образы. Поэтому latest удобен для разработки, но опасен в продакшне.
Тег держится за манифест, а не за отдельный слой; второй тег на тот же манифест добавляет ещё один указатель и ни одного байта данных.
Стратегия тегов
Хаотичное именование — источник головной боли: непонятно, что задеплоено, сложно откатиться, трудно проследить, из какого коммита образ собран.
Три устойчивые стратегии:
1. Семантическое версионирование (semver)
myapp:1.4.2
myapp:1.4
myapp:1
Понятно людям, поддерживает «плавающие» теги (1.4 → всегда последний патч-релиз). Подходит для библиотек и публичных образов.
2. Git SHA коммита
myapp:sha-abc1234f
Однозначно привязывает образ к исходному коду. Невозможно перезаписать случайно. Рекомендуется как основной тег в CI — всегда знаете, из какого коммита собрано.
3. Комбинированный
myapp:1.4.2 # для релизного тега
myapp:sha-abc1234f # для точного отслеживания
Можно ставить оба тега на один образ.
Короткая формула: в продакшне никогда не используйте latest — только версионированный или sha-тег.
Тег — подвижный указатель: даже версионный тег можно перезаписать повторным push. Когда нужно гарантировать, что в кластере работает ровно тот образ, который прошёл тесты, ссылаются на дайджест — registry/myapp@sha256:…: это хеш содержимого, он однозначен и не подменяется. CI получает дайджест после push и записывает его в манифест деплоя; тег остаётся для людей, дайджест — для машин.
docker tag, push и pull
Прежде чем отправить образ в реестр — войдите в него:
docker login ghcr.io -u USERNAME --password-stdin <<< "$GITHUB_TOKEN"
Образ тегируется командой docker tag:
# сначала собираем
docker build -t backend:local .
# добавляем «адрес» для реестра
docker tag backend:local ghcr.io/myorg/backend:sha-abc1234f
docker tag backend:local ghcr.io/myorg/backend:v1.4.2
Отправляем:
docker push ghcr.io/myorg/backend:sha-abc1234f
docker push ghcr.io/myorg/backend:v1.4.2
Скачиваем на другой машине:
docker pull ghcr.io/myorg/backend:sha-abc1234f
Один образ (один набор слоёв) может иметь сколько угодно тегов — это дёшево: теги — просто указатели, данные не дублируются.
Сборка и публикация в CI
Ручная сборка и push — это хорошо для экспериментов. В реальном проекте образ собирается автоматически при каждом пуше в основную ветку или при создании тега релиза.
Общая схема конвейера:
Образ собирается один раз и получает тег с хешем коммита. Дальше по цепочке едет именно он — а не «пересоберём для прода».
Пример конфигурации для GitHub Actions (.github/workflows/build.yml):
name: Сборка и публикация образа
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # нужно для публикации в ghcr.io
steps:
- uses: actions/checkout@v4
- name: Имя образа строчными буквами
run: echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
- name: Поднять buildx
uses: docker/setup-buildx-action@v3
- name: Войти в GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Собрать образ и опубликовать
uses: docker/build-push-action@v6
with:
context: .
push: true
# кэш слоёв между запусками: без этих строк каждая сборка идёт с нуля
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
Два шага в начале выглядят лишними, но без них этот файл не заработает.
Имя строчными буквами. Реестр ghcr.io принимает имена образов только в нижнем регистре. Если организация называется MyOrg, то ${{ github.repository }} даст MyOrg/backend, и push закончится ошибкой. Конструкция ${GITHUB_REPOSITORY,,} — это средство самой оболочки bash: две запятые переводят строку в нижний регистр.
Шаг setup-buildx-action. Кэш type=gha умеет только отдельный сборщик buildx. Без этого шага сборка идёт стандартным драйвером, который выгружать кэш не умеет, и вы получите Cache export is not supported for the docker driver — причём падение будет на самой сборке, а не на кэше.
Тег latest здесь ставится дополнительно к неизменяемому sha-… — как удобная ссылка «последнее из main» для локальной работы. Разворачивать в прод нужно именно по sha-…: он всегда указывает на один и тот же образ.
Теги не пишут руками: docker/metadata-action
В примере выше два тега перечислены вручную, и для одной ветки этого хватает. Как только появляются релизные теги, ветки с задачами и запросы на изменение, список превращается в набор условий — и для этого есть готовый шаг, который считает теги сам:
- name: Посчитать теги и метки
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=sha- # sha-1a2b3c4 — всегда
type=ref,event=branch # main, feature-x — для ветки
type=ref,event=pr # pr-42 — для запроса на изменение
type=semver,pattern={{version}} # 1.4.2 — из тега v1.4.2
type=semver,pattern={{major}}.{{minor}} # 1.4
type=raw,value=latest,enable={{is_default_branch}}
- name: Собрать образ и опубликовать
uses: docker/build-push-action@v6
id: build
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Что это даёт, кроме экономии строк. latest ставится только для основной ветки — самая частая ошибка ручных тегов, когда latest начинает указывать на сборку из ветки с задачей. Из тега v1.4.2 автоматически получаются 1.4.2 и 1.4, то есть работает описанная выше стратегия «двигающийся минорный тег». И тот же шаг заполняет метки OCI (ссылку на исходный код, коммит, версию), о которых говорит статья про промышленные образы.
Развёртывание по дайджесту: где его взять
Выше сказано, что развёртывать надо по дайджесту, потому что тег можно переставить. Осталось показать, откуда его брать: шаг сборки отдаёт его сам.
- name: Показать дайджест
run: echo "${{ steps.build.outputs.digest }}"
# sha256:9f8e7d...
- name: Подставить в манифест и развернуть
run: |
kubectl set image deployment/orders \
orders=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
Обратите внимание на синтаксис: дайджест присоединяется к имени через @, а не через :, и тег при этом не нужен вовсе. Такая ссылка неизменяема по определению — кто бы что ни переставил в реестре, развёрнут будет ровно этот образ.
Если развёртывание идёт не командой, а через репозиторий с манифестами (подход, когда состояние кластера описано в git), то в конвейере тот же дайджест подставляют в файл манифеста и делают коммит — дальше его применяет оператор в кластере. Механика та же, меняется только способ доставки.
Права на образ: почему сервер не может его скачать
Следующий шаг после первой успешной публикации — «образ в реестре есть, а сервер его не тянет». Причина почти всегда в правах, и в ghcr.io она устроена так: пакет по умолчанию приватный, а видимость и доступ настраиваются отдельно от репозитория с кодом.
Три вещи, которые надо сделать.
Дать конвейеру право публиковать. Это строка packages: write в разрешениях задания — без неё push отвечает отказом, хотя вход прошёл успешно.
Решить, приватный образ или публичный. Публичный качается без входа — удобно для примеров и открытого кода. Приватный требует входа с любой машины, включая сервер.
Дать серверу учётные данные. Для приватного образа это docker login на машине или, в Kubernetes, отдельный секрет с доступом к реестру, который указывают в поде (imagePullSecrets). Токен для этого берут узкий: в GitHub — токен с правом только на чтение пакетов, а не личный токен с полными правами. Симптом, по которому это узнаётся сразу: ErrImagePull и в описании пода unauthorized или denied.
И про сам GITHUB_TOKEN из примера: он действует только внутри запуска конвейера и для сервера не годится — на машины его не копируют.
Сканирование в конвейере
Статья про промышленные образы показывает сканеры на локальном образе и обещает встраивание в конвейер. Вот оно, и встраивается оно одним шагом после сборки:
- name: Проверить образ на уязвимости
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: '1'
Три решения, которые тут принимаются осознанно.
Что считать отказом. exit-code: 1 роняет сборку. Падать стоит на CRITICAL и на исправимых HIGH (ignore-unfixed: true), иначе конвейер встанет на уязвимости, которую никто не может исправить, и его отключат.
Сканировать до публикации или после. Проще после (как выше, по дайджесту уже опубликованного образа): образ доступен всем инструментам. Строже — до: собрать локально без push, проверить, и публиковать только прошедшее. Второй вариант правильнее для внешних реестров, первый достаточен для внутренних.
Кто читает отчёт. Кроме отказа сборки полезно выгружать результат в раздел безопасности репозитория (format: sarif плюс шаг загрузки) — тогда история находок видна и без чтения журналов конвейера.
Рядом с этим шагом обычно ставят ещё два, уже не про безопасность образа, а про доверие к нему: --sbom и --provenance в сборке и подпись cosign по тому же дайджесту.
Очистка реестра
Реестр растёт быстрее, чем кажется: тег на каждый коммит плюс кэш слоёв — и через год это сотни гигабайт, за которые платят или упираются в квоту. Уборка — часть конвейера, а не разовая акция.
Правила, которые обычно работают: образы с тегом sha-… хранить месяц, pr-… — удалять после закрытия запроса, релизные (1.4.2) — хранить всегда, кэш сборки — неделю. В GitHub это делает готовый шаг по расписанию, в Yandex Container Registry и Harbor — встроенная политика жизненного цикла образов, и её достаточно настроить один раз.
Одна оговорка: политика должна понимать, что сейчас развёрнуто. Удалённый образ, на который ссылается работающее развёртывание, ломает следующий перезапуск пода, хотя вчера всё работало. Поэтому релизные теги и образы, на которые ссылаются живые среды, исключают из уборки явно.
В реальном проекте рядом обычно идёт следующий шаг — обновление манифеста Kubernetes или вызов kubectl rollout restart, но это уже выходит за пределы Docker как инструмента сборки.
Один образ на все среды
Правило, которое подпись под схемой упоминает мельком, а стоит оно того, чтобы сказать его отдельно: образ собирают один раз и продвигают по средам, не пересобирая.
Соблазн пересобрать выглядит естественно: у тестовой среды другой адрес базы, другой уровень журналирования, другой ключ. Кажется логичным собрать «образ для теста» и «образ для прода». Так делать нельзя, и вот почему.
Пересобранный образ — другой образ. Между двумя сборками изменилось хотя бы содержимое базового образа (21-jre обновляется), версии подтянутых зависимостей, а иногда и код — если кто-то успел смержить. Проверили на тесте одно, в прод поехало другое, и разница невидима: тег тот же, а дайджест другой.
Смысл тестирования исчезает. Все проверки — интеграционные тесты, нагрузочные, ручная приёмка — относятся к конкретному артефакту. Если в прод едет другой артефакт, результаты проверок к нему не относятся, сколько бы их ни было.
Отладка становится гаданием. «На тесте работает, в проде нет» невозможно разобрать, пока нет уверенности, что запущено одно и то же.
Как тогда быть с различиями между средами. Ответ один и старый: различия живут вне образа. Адреса, пароли, ключи, уровни журналирования, включённые возможности — переменные окружения, файлы настроек, монтируемые извне, секреты из хранилища. Внутри образа остаётся только то, что одинаково везде: код, зависимости, среда выполнения. Это и есть принцип «конфигурация отдельно от кода» из «двенадцати факторов», и в контейнерах он не пожелание, а условие работоспособности схемы.
Практически это выглядит так: конвейер собрал образ с тегом sha-1a2b3c4, опубликовал, дальше по этому же дайджесту он разворачивается в тестовую среду, потом в предпромышленную, потом в промышленную — одна ссылка, три развёртывания, разные наборы переменных. Продвижение можно оформить и тегами (app@sha256:… → staging, → production), но тегами именно помечают уже существующий образ, а не собирают новый.
Проверить, что правило соблюдается, можно одним вопросом к своему конвейеру: сколько раз в нём встречается шаг сборки образа? Если больше одного (например, отдельная задача «собрать для прода»), значит в прод едет непроверенное.
Образ помнит, на каком процессоре его собрали
Про это узнают одинаково: собрали образ на ноутбуке с процессором Apple, отправили в реестр, запустили на сервере — и контейнер падает со строчкой exec format error. Внутри лежат машинные команды для другой архитектуры.
У образа есть архитектура, как у обычной программы. Ноутбуки Apple и многие облачные машины — это arm64, а большинство серверов — amd64 (он же x86_64). Образ, собранный под одну, на другой не запустится.
Лечится двумя способами. Первый — собирать в конвейере: раннеры GitHub Actions работают на amd64, и образ сразу получается «серверный». Второй — собрать сразу под обе архитектуры, тогда реестр отдаст каждой машине подходящий вариант:
- name: Собрать образ и опубликовать
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
Локально то же самое делается флагом: docker build --platform linux/amd64 -t myapp:1.0 . — полезно, когда собираете с ноутбука, а запускать будете на сервере.
Многоэтапная сборка в CI
Для многоэтапной сборки CI ничего особенного делать не должен — docker build сам разберётся, и в реестр уйдёт только финальный лёгкий образ без JDK и сборочного хозяйства: 300–350 МБ вместо 700–900. Как устроить Dockerfile, чтобы зависимости кэшировались отдельно от кода, разобрано в статье про многоэтапную сборку.
Глубже: реестры в российских условиях: зеркала, прокси и свой реестррасширенное
docker pull postgres:17 на сервере отвечает ошибкой, а на ноутбуке вчера работал. Список реестров выше устроен так, будто Docker Hub доступен всегда, а с 2024 года прямые обращения к нему из российских сетей блокируются со стороны самого Docker Hub, и это первое, что ломается при развёртывании. Вторая проблема существовала и раньше: у Docker Hub лимит на анонимные pull с одного адреса, а за NAT офиса или облака адрес общий на всех, и в разгар дня сборки падают с toomanyrequests.
Три способа, по нарастающей надёжности. Зеркало. В /etc/docker/daemon.json указывают registry-mirrors со списком зеркал, и Docker ходит за образами Docker Hub туда; публичные зеркала есть у российских облаков и у Google (mirror.gcr.io), и они кэшируют популярные образы. Это чинит pull на машине без правки образов и Dockerfile, но зеркало не гарантирует наличие редкого образа и не покрывает другие реестры: quay.io (Keycloak), gcr.io (distroless), ghcr.io.
Прокси-реестр. Nexus или Harbor внутри компании настраивают как сквозной кэш: он тянет образ из внешнего реестра при первом запросе и хранит у себя, а Dockerfile и конвейеры ссылаются на него: FROM registry.corp/dockerhub/eclipse-temurin:21-jre. Так покрывают все внешние реестры, снимают лимиты и получают образы, которые не пропадут, если внешний источник закроется, ценой ещё одного сервиса в эксплуатации.
Свой реестр для своих образов нужен в любом случае: собранные образы кладут в реестр облака, где стоит кластер, Yandex Container Registry (cr.yandex/<id>/app:1.4.2, вход через yc container registry configure-docker), реестры VK Cloud и Selectel, или в Harbor у себя. Вместе с ним заводят правило: базовые образы, от которых зависят сборки, перетегированы в свой реестр (docker pull, docker tag, docker push один раз, дальше FROM cr.yandex/<id>/base/temurin:21-jre), и внешние реестры в сборке не участвуют вовсе. Дайджест в FROM закрепляет, что перетегированный образ тот самый.
Что проверить на новой машине или в новом конвейере: docker pull базового образа из того реестра, что в Dockerfile; настроен ли registry-mirrors или прокси; есть ли учётные данные для своего реестра в CI (секрет, а не файл config.json в репозитории); и куда ходит Testcontainers, у него для этого своя настройка префикса реестра, о чём статья про основы.
Коротко
- Реестр образов — хранилище, через которое образ переезжает между машинами; самые популярные: Docker Hub, GitHub Container Registry, приватные реестры облачных провайдеров.
- Полное имя образа:
<реестр>/<пространство-имён>/<имя>:<тег>; без реестра — Docker Hub, без тега —latest.docker tagдобавляет новый указатель на уже собранный образ; данные не копируются. - В продакшне используйте конкретный тег (semver или sha коммита), а не
latest— иначе непонятно, что именно задеплоено. Теги считаетdocker/metadata-action:sha-…всегда,latestтолько для основной ветки,1.4.2и1.4из релизного тега, он же заполняет метки OCI. - Конвейер CI:
build → tag → push; следующий шаг — развёртывание (Kubernetes, systemd, Compose на сервере). - В GitHub Actions
GITHUB_TOKENдостаточно для публикации вghcr.io— дополнительные секреты не нужны. Вghcr.ioпакет по умолчанию приватный: конвейеру нужноpackages: write, серверу — вход или секрет реестра с правом только на чтение, иначеErrImagePullсunauthorized. - Docker Hub из России и с общего адреса ненадёжен:
registry-mirrorsвdaemon.json, прокси-реестр Nexus или Harbor для всех внешних источников, свой реестр в облаке для своих образов и перетегированные базовые образы с дайджестом. - Дайджест для развёртывания берут из вывода шага сборки (
steps.build.outputs.digest) и подставляют через@, а не через:. - Сканирование встраивают шагом после сборки с порогом
CRITICALплюс исправимыеHIGHиignore-unfixed; рядом ставят SBOM, провенанс и подпись по дайджесту. - Реестру нужна политика уборки (
sha-…месяц,pr-…после закрытия, релизы навсегда), и она обязана исключать то, на что ссылаются живые среды. - Образ собирают один раз и продвигают по средам по дайджесту: пересборка для прода отправляет туда непроверенный артефакт, а различия сред живут в переменных, не в образе.
Что почитать дальше
- Многоэтапная сборка и слои образа — как устроены слои, почему порядок инструкций влияет на размер и скорость сборки.
- Лучшие практики образов — безопасность, минимальный базовый образ, непривилегированный пользователь.
- Запуск Spring Boot в Docker — полный пример: Dockerfile, конфигурация JVM, профили Spring.