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-action—push: 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:mainghcr.io/user/repo/etl-job:sha-abc1234ghcr.io/user/repo/etl-job:latest
ВНИМАНИЕ: не пушь :latest с feature-веток. Это типичная ошибка, которая ломает prod, если feature-branch проникает в latest. Правило: latest = только main. Используй is_default_branch.
Кэширование слоёв через 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 это критично.
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"