Learning Platform
Глоссарий Troubleshooting
Урок 19.02 · 25 мин
Средний
dockercigithub-actionsbuildxregistries

GitHub Actions: build + push Docker-образа

В прошлом уроке мы научились запускать тесты в CI. Следующий шаг — собрать Docker-образ из проверенного кода и запушить его в registry, откуда его потом скачают prod-серверы или k8s-кластер.

GitHub Actions предоставляет готовые official actions от Docker Inc. для этого. В этом уроке: setup buildx, login в GHCR, build-push с кэшированием слоёв через gha (GitHub Actions cache), tagging-стратегия для DE-образов.


Какие official actions нужны

Docker Inc. поддерживает четыре основных action:

  • docker/setup-buildx-action — устанавливает Buildx (BuildKit-frontend для docker build). Без него не работают advanced features: cache-from, multi-platform, secrets.
  • docker/login-action — логин в registry (Docker Hub, GHCR, ECR, GAR). Поддерживает password, token, OIDC.
  • docker/build-push-action — собственно сборка и пуш. Обёртка над docker buildx build с интеграциями GHA.
  • docker/metadata-action — генерирует теги и labels из git-context (branch, tag, sha).

Это базовая четвёрка. Иногда добавляют docker/setup-qemu-action для multi-arch.


Минимальный workflow

# .github/workflows/docker.yml
name: build-and-push

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write   # для пуша в GHCR

    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

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

      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}/etl-job
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=sha,prefix=sha-,format=short

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Разберём построчно:

  • permissions.packages: write — workflow получает право пушить в GitHub Packages (включая GHCR).
  • setup-buildx-action — поднимает Buildx-сервер в Docker (контейнер buildx_buildkit_default).
  • login-action с ${{ secrets.GITHUB_TOKEN }} — встроенный токен, работает без настройки secrets. Логинимся в ghcr.io как ${{ github.actor }} (юзер, который запустил workflow).
  • metadata-action — генерирует теги. Подробнее ниже.
  • build-push-actionpush: true только если не PR (на PR — только build, без push).
  • cache-from/to: type=gha — кэширование слоёв в GitHub Actions Cache. Подробнее ниже.

Tagging стратегия

В metadata-action ты описываешь, какие теги генерировать. Стандарт DE-команд:

tags: |
  # 1. По имени бранча: main -> :main, feature/x -> :feature-x
  type=ref,event=branch

  # 2. По PR номеру: PR #42 -> :pr-42 (только в pull_request событиях)
  type=ref,event=pr

  # 3. По git-тегу: v1.2.3 -> :1.2.3 и :1.2 и :1
  type=semver,pattern={{version}}
  type=semver,pattern={{major}}.{{minor}}
  type=semver,pattern={{major}}

  # 4. По SHA коммита (всегда): :sha-abc1234
  type=sha,prefix=sha-,format=short

  # 5. :latest ТОЛЬКО на main + non-PR
  type=raw,value=latest,enable={{is_default_branch}}

Это даст для commit abc1234 на ветке main теги:

  • ghcr.io/user/repo/etl-job:main
  • ghcr.io/user/repo/etl-job:sha-abc1234
  • ghcr.io/user/repo/etl-job:latest
WARNING

ВНИМАНИЕ: не пушь :latest с feature-веток. Это типичная ошибка, которая ломает prod, если feature-branch проникает в latest. Правило: latest = только main. Используй is_default_branch.

Tagging map: где какой тег
:sha-abc1234иммутабельныйКаждый сабмит -- уникальный SHA-тег. Для production deploy используй именно его (по digest). Никогда не перезаливается.
:mainrollingТекущий main. Перезаливается с каждым merge'ом. Удобно для dev-стендов, плохо для прода.
:1.2.3 / :1.2 / :1semverРеjeases по git-тегам. Старшие (:1, :1.2) перезаписываются при minor/patch update. Используй :1.2.3 в prod-deploy.
:latestMAIN ONLYТолько с main. Никогда с feature-branches. С :latest можно ловить sneaky deploy bugs.

Кэширование слоёв через GHA

Без кэширования каждый CI-run скачивает базовые образы и пересобирает все слои с нуля. На образе типа python:3.11-slim + pip install requirements.txt + COPY это 2-5 минут. С cache-from: type=gha + cache-to: type=gha,mode=max — 30-60 секунд (если ничего не изменилось в requirements.txt).

Как это работает:

  • cache-to: type=gha,mode=max — Buildx экспортирует все слои (включая intermediate из multi-stage) в GitHub Actions Cache.
  • cache-from: type=gha — на следующем run’е Buildx проверяет кэш. Если слой имеет тот же hash inputs (тот же FROM, та же RUN-команда) — reuse без пересборки.

mode=max важен: без него (по умолчанию mode=min) сохраняются только финальные слои, intermediate стадий не кэшируется. Для multi-stage это критично.

TIP

GitHub Actions Cache имеет лимит 10GB на репозиторий и автоматически вытесняет старые entries после 7 дней inactivity. Если у тебя несколько образов в одном репо, используй scope: cache-from: type=gha,scope=etl-job и cache-to: type=gha,scope=etl-job. Иначе кэши пересекаются и одна сборка инвалидирует кэш другой.


Полный workflow для DE-image

name: build-etl

on:
  push:
    branches: [main, develop]
    paths:
      - 'etl/**'
      - 'Dockerfile'
      - 'requirements.txt'
      - '.github/workflows/build-etl.yml'
  pull_request:
    paths:
      - 'etl/**'
      - 'Dockerfile'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'
      - run: pip install -r requirements.txt -r requirements-dev.txt
      - run: pytest -v

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

      - uses: docker/setup-buildx-action@v3

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

      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository_owner }}/etl-job
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=sha,prefix=sha-,format=short
            type=raw,value=latest,enable={{is_default_branch}}

      - uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha,scope=etl-job
          cache-to: type=gha,mode=max,scope=etl-job
          provenance: false
          platforms: linux/amd64

Что добавилось:

  • needs: test — build job стартует только после успешных тестов.
  • paths: в on: — workflow триггерится только если изменились файлы, влияющие на образ.
  • provenance: false — отключает SLSA attestation. Для junior это излишество, для prod-security важно.
  • platforms: linux/amd64 — только одна архитектура. Multi-arch в следующем уроке.

Логин в разные registry

GHCR — самый простой. Но иногда нужен ECR / Docker Hub / GAR. Шаблоны:

# Docker Hub
- uses: docker/login-action@v3
  with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_TOKEN }}

# Amazon ECR (через OIDC, без длинных паролей)
- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
    aws-region: us-east-1
- uses: aws-actions/amazon-ecr-login@v2

# Google Artifact Registry
- uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: 'projects/123/locations/global/workloadIdentityPools/gha/providers/gha'
    service_account: '[email protected]'
- uses: docker/login-action@v3
  with:
    registry: us-east1-docker.pkg.dev
    username: oauth2accesstoken
    password: ${{ steps.auth.outputs.access_token }}

В каждом случае — после login-action ты можешь docker push registry/repo:tag. Разница только в способе аутентификации.


Что НЕ делать в DE-CI

Как HTTPS защищает пуш образов в registry

Антипаттерны, которые приходят с дилетантскими advice:

Антипаттерн 1: docker build без buildx-кэша

# ПЛОХО
- run: docker build -t myimage:latest .
- run: docker push myimage:latest

Каждый run пересобирает с нуля. 5-10 минут. Используй docker/build-push-action с GHA cache.

Антипаттерн 2: latest везде

# ПЛОХО
tags: ghcr.io/x/y:latest

Не воспроизводимо. В проде latest сегодня и latest завтра — разные. Используй sha-теги для deploy, latest только как удобство для разработчиков.

Антипаттерн 3: секреты через build-arg

# ПЛОХО
build-args: |
  SECRET_KEY=${{ secrets.MY_KEY }}

build-arg попадает в docker history. Любой, кто скачает образ, увидит секрет. Используй --mount=type=secret (Dockerfile + Buildx secrets).

Антипаттерн 4: одинаковый образ для prod и debug

Прод должен быть минимальным (distroless / slim). Для debug заводи отдельный target в multi-stage и собирай его в dev-CI.


Попробуй сам

# 1. Создай в репо .github/workflows/docker.yml с workflow выше

# 2. Создай простой Dockerfile в корне:
cat > Dockerfile <<EOF
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY etl/ ./etl/
CMD ["python", "-m", "etl.main"]
EOF

# 3. requirements.txt
echo "requests==2.32.3" > requirements.txt

# 4. Commit + push
git add .
git commit -m "ci: add docker build workflow"
git push origin main

# 5. Открой GitHub -> Actions -> увидишь run
# 6. Когда build пройдёт -- зайди в Packages, увидишь свой образ
# 7. Локально скачай:
docker pull ghcr.io/USERNAME/REPO/etl-job:latest

# 8. На втором push (без изменений) увидишь, что кэш сработал:
# "CACHED [2/4] WORKDIR /app"
# "CACHED [3/4] COPY requirements.txt"
# "CACHED [4/4] RUN pip install"

Проверка знанийKnowledge check
Ты настроил build-push-action с cache-from/to: type=gha, и видишь что кэш создаётся (200MB в Actions Cache), но при каждом run слои всё равно пересобираются. В Buildx логах "RUN pip install" не показывает CACHED. В чём может быть проблема?
ОтветAnswer
Самые частые причины: (1) cache-to: type=gha по умолчанию mode=min -- сохраняет только финальные слои, не intermediate из multi-stage. Добавь mode=max. (2) В Dockerfile перед RUN pip install идёт COPY всего контекста (COPY . .), который меняется на каждом коммите -- invalidate всех последующих слоёв. Правильно: COPY requirements.txt сначала, RUN pip install, и только потом COPY . .. (3) requirements.txt пересобирается генератором (например, через poetry export перед docker build) -- даже без изменений timestamp/checksum меняется. Лочь lockfile в репо.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Зачем использовать docker/setup-buildx-action в GitHub Actions, если можно просто docker build?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4