Когда в проекте много сервисов, один хороший Dockerfile уже не решает все проблемы. В этот момент на первый план выходят две темы: ускорение сборки через golden builder image и аккуратная публикация образов в registry.
Если базовый Dockerfile ещё не выстроен, начните со статьи про multi-stage сборку, pinning и runtime-безопасность. А если нужна именно дисциплина вокруг CI, secrets, proxy и HEALTHCHECK, рядом есть отдельный материал про production-сборку Docker-образов.
В статье
- Когда нужен golden builder image
- Что важно не перепутать
- CI-паттерн для builder image
- Как логиниться в registry
- Тегирование и push
- Перенос образов между registry и через архив
- Копирование через
skopeo
Когда нужен golden builder image
Golden builder image особенно уместен, если:
- У вас много сервисов на одном стеке и они собираются одинаковым набором инструментов.
- CI тормозит из-за повторной установки SDK, компиляторов, package managers и системных зависимостей.
- Нужны стабильные и воспроизводимые сборки с одинаковым builder-окружением между репозиториями.
Для небольшого проекта с одним-двумя сервисами это может быть лишним оверхедом: отдельный образ, отдельный pipeline и сопровождение не всегда окупаются.
Что важно не перепутать
- Golden builder image используется только на этапе сборки, а не как runtime-образ.
- Образ должен быть пинован по версиям и регулярно обновляться, включая security patches.
- Для него нужен отдельный pipeline: сборка, сканирование, подпись и публикация.
- Ускорение достигается за счёт предустановленных инструментов и кэшей, но секреты нельзя запекать внутрь образа.
Пример Dockerfile для прикладного сервиса:
# Пинуем golden builder по версии и digest — гарантия воспроизводимости
FROM registry.company.local/platform/go-builder:1.26.3@sha256:<digest> AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server ./cmd/server
# Runtime-стадия минимальна — toolchain остался в builder
FROM alpine:3.23
RUN apk --no-cache add ca-certificates && addgroup -S app && adduser -S app -G app
COPY --from=builder /src/server /usr/local/bin/server
USER app
CMD ["server"]Важно не превращать golden image в “помойку” из инструментов. Он должен быть минимальным, версионируемым и обновляться по расписанию.
CI-паттерн для golden builder image
Обычно это отдельный репозиторий или отдельная директория с Dockerfile builder-образа:
name: golden-builder
on:
push:
branches: [main]
paths:
- '.github/workflows/golden-builder.yml'
- 'build-images/go-builder/**'
schedule:
- cron: '0 3 * * 1' # еженедельная пересборка по понедельникам — подхватывает security patches
# Keyless-подпись cosign работает через OIDC: GitHub выдаёт короткоживущий токен,
# для чего job'у обязательно нужно право id-token: write. Без него cosign sign упадёт.
permissions:
contents: read
id-token: write
jobs:
build-scan-sign-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Compute image refs
id: refs
run: |
REPO=registry.company.local/platform/go-builder
# Неизменяемый revision-тег на КАЖДУЮ пересборку: еженедельный cron не должен
# повторно пушить один и тот же :1.26.3 — это либо упрётся в tag immutability,
# либо оставит тег изменяемым. run_number монотонно растёт от сборки к сборке.
echo "repo=$REPO" >> "$GITHUB_OUTPUT"
echo "rev=$REPO:1.26.3-r${{ github.run_number }}" >> "$GITHUB_OUTPUT"
echo "float=$REPO:1.26.3" >> "$GITHUB_OUTPUT"
- name: Build builder image
run: docker build -t ${{ steps.refs.outputs.rev }} ./build-images/go-builder
- name: Scan builder image # блокируем публикацию при уязвимостях
run: trivy image --exit-code 1 --severity HIGH,CRITICAL ${{ steps.refs.outputs.rev }}
- name: Push builder image
id: push
run: |
# revision-тег публикуется ровно один раз (его и делаем immutable в registry),
# 1.26.3 — плавающий указатель на «последнюю сборку под Go 1.26.3»:
docker push ${{ steps.refs.outputs.rev }}
docker tag ${{ steps.refs.outputs.rev }} ${{ steps.refs.outputs.float }}
docker push ${{ steps.refs.outputs.float }}
# digest берём ИЗ registry: docker inspect .RepoDigests может расходиться
# с реальным manifest digest, imagetools же спрашивает сам registry:
DIGEST=$(docker buildx imagetools inspect ${{ steps.refs.outputs.rev }} --format '{{.Manifest.Digest}}')
echo "ref=${{ steps.refs.outputs.repo }}@$DIGEST" >> "$GITHUB_OUTPUT"
- name: Install cosign
uses: sigstore/cosign-installer@v3
# Keyless: без --key cosign берёт OIDC-токен (id-token: write выше) и пишет запись
# в прозрачный лог Rekor. Подписываем по digest — он неизменяем и одинаков во всех registry:
- name: Sign builder image (keyless, OIDC)
run: cosign sign --yes ${{ steps.push.outputs.ref }}Схема тегов здесь двухуровневая: 1.26.3-r<N> — неизменяемая ревизия конкретной сборки (её и защищают от перезаписи в registry), а 1.26.3 — плавающий указатель на последнюю ревизию под этот Go. Потребители, которым важна воспроизводимость, пинуются на ревизию или прямо на digest (go-builder:1.26.3-r<N>@sha256:…), а не на плавающий 1.26.3.
После этого прикладные репозитории используют образ только на этапе сборки и не дублируют установку toolchain в каждом CI job.
Docker Hub не обязателен
Рабочая схема одинакова для любых registry: Harbor, Nexus, GitLab Registry, GHCR, ECR и других. Важна не конкретная платформа, а дисциплина публикации и именования образов.
Как логиниться в registry
Базовый вариант:
# --password-stdin: пароль не попадает в историю shell и ps
echo "$REGISTRY_TOKEN" | docker login registry.company.local -u "$REGISTRY_USER" --password-stdinПароль или токен передавайте через stdin, а не в явном виде в командной строке.
Примеры:
echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GITHUB_USER" --password-stdin
echo "$HARBOR_TOKEN" | docker login registry.company.local -u "$HARBOR_USER" --password-stdinТегирование и push
# Version-тег — конкретная версия (по соглашению не перезаписываем; сам по себе тег НЕ immutable)
docker tag app:multi registry.company.local/team/app:1.4.2
# Плавающий тег — указывает на текущий рекомендуемый образ
docker tag app:multi registry.company.local/team/app:stable
docker push registry.company.local/team/app:1.4.2
docker push registry.company.local/team/app:stableПрактика простая: version-тег, например 1.4.2 или commit sha, плюс удобный плавающий тег вроде stable или latest.
Важная оговорка: сам по себе version-тег не становится immutable — по умолчанию любой docker push с тем же тегом переустановит его на другой образ. Настоящую неизменяемость дают два механизма, и лучше применять оба:
- Запрет перезаписи тегов на стороне registry (tag immutability). Это отдельная настройка, а не поведение по умолчанию: в Harbor — immutable tag rules, в AWS ECR —
imageTagMutability: IMMUTABLE, в GHCR/GCR Artifact Registry — соответствующие политики. После включения повторныйpushтого же тега registry отклонит. - Адресация по digest. Тег — это подвижная ссылка, а
app@sha256:…указывает на конкретный контент и не меняется, что бы ни делали с тегами. Для деплоя и подписи (cosign sign … @sha256:…) опирайтесь на digest, а тег держите лишь как человекочитаемый ярлык.
Если образ публикуется в несколько registry, сохраняйте единый version-тег, но источником истины держите именно digest — он одинаков во всех registry для одного и того же образа.
Как перенести образ из одного registry в другой
Самый понятный вариант — через локальный pull, tag и push:
# Простой перенос: pull → переименование → push в другой registry
docker pull registry-a.local/team/app:1.4.2
docker tag registry-a.local/team/app:1.4.2 registry-b.local/team/app:1.4.2
docker push registry-b.local/team/app:1.4.2Это рабочий вариант, когда нужен простой способ забрать и переложить образ без отдельного инструмента репликации.
Как сохранить образ в архив и перенести в другое окружение
# Экспорт в tar-архив — для переноса на изолированные площадки
docker save registry-a.local/team/app:1.4.2 -o app_1.4.2.tar
# На целевом хосте: загрузка из архива
docker load -i app_1.4.2.tar
docker tag registry-a.local/team/app:1.4.2 registry-b.local/team/app:1.4.2
docker push registry-b.local/team/app:1.4.2Подход полезен для изолированных контуров, air-gapped окружений или ручного переноса между площадками.
Копирование между registry без локальной загрузки
Для больших образов и автоматизации удобнее использовать skopeo:
# Прямое копирование между registry — без промежуточного docker pull/push
skopeo copy docker://registry-a.local/team/app:1.4.2 docker://registry-b.local/team/app:1.4.2Это особенно удобно, когда нужно синхронизировать registry без прогонки образа через локальный Docker daemon.
Вывод
Golden builder image и нормальная работа с registry становятся полезны не в момент “первого контейнера”, а когда проект начинает расти. Если сервисов мало, это может быть лишней сложностью. Но для командной разработки и длинных CI-сборок такие практики быстро окупаются.
Чтобы эти практики действительно работали, их лучше строить поверх уже нормальной базы: multi-stage Dockerfile и production-процесса со сканированием, secrets и healthcheck.
Комментарии