Сборка образов в CI/CD

Как автоматически собирать и публиковать образы из пайплайна вместо ручного docker build на ноутбуке.

CI/CD — автоматический конвейер: на каждый push в репозиторий система сама собирает образ, прогоняет проверки и публикует артефакт в реестр, без участия человека.

Ручная сборка на ноутбуке плоха тем, что она невоспроизводима и зависит от человека: забыл тег, собрал не из той ветки, на машине другая версия инструментов. CI/CD убирает этот фактор. Каждый push запускает свежую сборку в чистом окружении, образ тегируется детерминированно по коммиту, и в реестр уезжает ровно то, что прошло проверки. Разберём это на двух самых распространённых системах — GitHub Actions и GitLab CI.

Зачем это на практике

Пайплайн — это гарантия, что образ в реестре соответствует конкретному состоянию кода. Появился новый коммит — появился новый образ с тегом этого коммита. Откатиться к прошлому деплою = взять образ прошлого коммита. Никаких «а кто и когда это собирал»: сборка задокументирована логами CI и привязана к git-истории.

Сборка и push в GitHub Actions

В GitHub Actions пайплайн описывается YAML-файлом в .github/workflows/. Готовые actions берут на себя логин в реестр и сборку с кэшем.

name: build-image
on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/acme/api:sha-${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Здесь secrets.GITHUB_TOKEN — автоматический токен пайплайна, его не нужно заводить руками. Тег sha-${{ github.sha }} привязывает образ к конкретному коммиту. Строки cache-from/cache-to с type=gha включают кэш слоёв — о нём ниже.

Сборка и push в GitLab CI

В GitLab пайплайн описывается в .gitlab-ci.yml. Реестр и токен GitLab подставляет через встроенные переменные CI_REGISTRY*.

build:
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"

Переменная $CI_REGISTRY_IMAGE — это адрес репозитория образов проекта, а $CI_COMMIT_SHORT_SHA — короткий хеш коммита. Сервис docker:dind (Docker-in-Docker) даёт демон Docker внутри job, чтобы было где собирать.

Кэш слоёв в CI

Главная боль CI-сборки: каждый запуск стартует в чистом окружении, без локального кэша слоёв. Без кэша Docker заново выполняет apt install, pip install, npm ci при каждой сборке — это минуты впустую. Решение — внешний кэш: Docker сохраняет слои в реестр или в хранилище CI и переиспользует их в следующем запуске.

В GitHub Actions это делают строки cache-from/cache-to: type=gha (показаны выше) — слои кладутся в кэш самого GitHub. Универсальный способ для любого CI — registry-кэш: слои хранятся в реестре рядом с образом.

docker buildx build \
  --cache-from type=registry,ref=ghcr.io/acme/api:buildcache \
  --cache-to type=registry,ref=ghcr.io/acme/api:buildcache,mode=max \
  -t ghcr.io/acme/api:sha-abc123 --push .

Чтобы кэш реально срабатывал, помогает грамотный порядок инструкций в Dockerfile: сначала копировать файлы зависимостей и ставить их, и только потом — исходники. Тогда правка кода не инвалидирует слой с установленными зависимостями.

Тегирование по коммиту и версии

В CI обычно навешивают несколько тегов на один образ — разные теги служат разным целям.

ТегИсточникЗачем
sha-a1b2c3dхеш коммитаточная привязка к коду, иммутабелен — для деплоя и отката
1.4.2git-тег релизачеловекочитаемая версия релиза
mainимя ветки«последний с этой ветки» — для тестовых сред

Связка коммит-тегов и версий устроена так: сборка из ветки main получает теги sha-… и main, а когда вы ставите git-тег v1.4.2, отдельный триггер собирает образ с тегом 1.4.2. В прод деплоят иммутабельный sha-… или 1.4.2, но никогда — main или latest (см. урок про реестры).

Build matrix: одна сборка — много вариантов

Build matrix — механизм CI, который запускает одну и ту же сборку параллельно с разными параметрами. Классический случай — мультиплатформенные образы (под amd64 и arm64, чтобы образ работал и на обычных серверах, и на ARM/Apple Silicon).

      - name: Build multi-arch
        uses: docker/build-push-action@v6
        with:
          push: true
          platforms: linux/amd64,linux/arm64
          tags: ghcr.io/acme/api:sha-${{ github.sha }}

Так из одного описания получается образ, который Docker сам отдаст в нужной архитектуре при pull. Matrix также используют, чтобы собрать вариации под разные версии рантайма или с разными build-аргументами — параллельно и из одного конфига.

Как это работает под капотом

За современной CI-сборкой стоит BuildKit — движок сборки Docker нового поколения (он же мотор у docker buildx и build-push-action). BuildKit строит из Dockerfile граф зависимостей шагов и выполняет независимые ветки параллельно, а результаты — слои, адресуемые по хешу содержимого, — умеет экспортировать во внешний кэш (cache-to) и импортировать обратно (cache-from). При следующем запуске BuildKit сверяет хеши входов каждого шага: если ничего не поменялось, он не пересобирает слой, а тянет готовый из кэша. Мультиплатформенность он реализует через эмуляцию (QEMU) или нативные раннеры под каждую архитектуру, собирая отдельный образ на платформу и связывая их единым манифест-листом под одним тегом.

Частые ошибки

  • Сборка без кэша. Каждый пайплайн заново качает и ставит зависимости — минуты на ветер. Включите cache-from/cache-to.
  • Деплой по main/latest. Эти теги плавающие. В прод — только иммутабельный sha- или версия.
  • Секреты в YAML открытым текстом. Пароль реестра должен жить в secrets/CI-переменных, а не в коммите.
  • Плохой порядок в Dockerfile. COPY . . до установки зависимостей сбивает кэш на каждом изменении кода.
  • Забыли права на push. В GitHub Actions без permissions: packages: write публикация в GHCR упадёт.

Итоги

  • CI/CD собирает и публикует образ автоматически на каждый push, в чистом воспроизводимом окружении.
  • GitHub Actions использует docker/build-push-action, GitLab CI — docker build/push со встроенными CI_REGISTRY*.
  • Кэш слоёв (type=gha или registry-кэш) экономит минуты на каждой сборке; помогает правильный порядок Dockerfile.
  • Тегируйте по коммиту (sha-…) и версии; в прод — иммутабельные теги.
  • Build matrix собирает мультиплатформенные образы (amd64+arm64) из одного конфига.
Проверьте себя
1. Зачем в CI-сборке настраивают кэш слоёв (cache-from / cache-to)?
AЧтобы скрыть образ от посторонних
BКаждый запуск CI стартует в чистом окружении без локального кэша, и без внешнего кэша зависимости ставятся заново каждый раз
CЧтобы образ автоматически сканировался на CVE
DКэш обязателен, иначе docker push не работает
2. Каким тегом из CI правильнее всего деплоить в прод?
Amain — он всегда самый свежий
Blatest — стандарт для продакшена
Csha-<хеш коммита> или конкретной версией — иммутабельный тег, привязанный к коду
DТегом с именем разработчика
3. Что позволяет сделать build matrix в контексте Docker-образов?
AЗашифровать слои образа
BСобрать один и тот же образ параллельно под разные параметры, например под платформы amd64 и arm64
CУдалить старые образы из реестра
DЗаменить Dockerfile на docker-compose