Охота за тиками: как сократить сборку Docker-образа в GitHub Actions с 9 минут до 90 секунд
Пятница, 23:11. Джун-дев Кира в ярости: CI крутит job build-and-push уже девятый раз — каждый прогон тянется 9 минут. В чате горит «🔥 срочный релиз», а тикеты закрываются медленнее, чем кофе остывает.
Кира надевает плащ детектива и идёт по следу «утраченных секунд». Её цель — вычислить, где прячутся лишние слои Docker и почему GitHub Actions ленится.
1. Осмотр места преступления
- Фиксируем улики — самый медленный шаг:
docker buildx build \ --platform linux/amd64,linux/arm64 \ --push -t ghcr.io/acme/my-app:latest .
Старт-финиш: 9 мин 08 сек. - Подозрение: GitHub-раннер каждый раз компилирует весь frontend и устанавливает NPM-зависимости.
2. План расследования
«Если преступник оставляет отпечатки пальцев, значит, есть кэш, который мы не используем». — Стив БилдКит, вымышленный инспектор.
Наша стратегия:
- Настроить Buildx-builder c включённым BuildKit.
- Сказать BuildKitу хранить слои в cache-backend type=gha — встроенный кэш GitHub Actions.
- Сохранить node_modules в отдельный actions/cache.
- Включить inline-cache в итоговом образе, чтобы локальные машины и другие CI тоже могли reuse слои.
3. Шаг-за-шагом: создаём workflow
3.1 Подготавливаем builder
name: ci
on:
push:
branches: [main]
jobs:
docker:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU (для multi-arch)
uses: docker/setup-qemu-action@v3
- name: Set up Buildx
id: buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=moby/buildkit:v0.13 # >=0.13 = поддержка cache v2 :contentReference[oaicite:2]{index=2}
setup-buildx-action
рождает изолированный builder-контейнер; кэш слоёв теперь можно экспортировать без root-прав.
3.2 Кэшируем node_modules (фронтенд)
- name: Cache NPM deps
uses: actions/cache@v4
with:
path: |
**/node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
Так мы экономим ≈ 40–60 с даже до начала сборки.
3.3 Кэш Docker-слоёв BuildKit
- name: Build & push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/acme/my-app:latest
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false # чуть ускоряем build, если SBOM не нужен
outputs: type=image,name=ghcr.io/acme/my-app:latest,push=true,cache-from=type=registry,ref=ghcr.io/acme/my-app:cache
- type=gha — GitHub-встроенное хранилище; не занимает место в контейнерном реестре.
- mode=max — сохраняет все слои; для pet-проекта достаточно.
- inline-cache (
cache-from=registry
) даёт разработчикам локальный «холодный» старт.
4. Первый прогон: репортаж с места
Итерация | Длительность job |
---|---|
baseline (без кэша) | 9 мин 08 с |
+ NPM cache | 6 мин 17 с |
+ BuildKit GHA cache | 1 мин 32 с |
Экономия ~83 % за счёт двух строчек cache-from/to
.
5. Что может пойти не так и как чинить
Симптом | Вероятная причина | Фикс |
---|---|---|
error: failed to solve: no available cache | Новый ключ кэша | Убедись, что key: стабилен; зависит от lock-файла. |
no space left on device | Cache-tarball > 5 ГБ | Добавь --build-arg BUILDKIT_INLINE_CACHE=1 и чисти кэш type=gha,mode=min . |
Buildx жалуется на версию | Buildx < 0.21 | Укажи image=moby/buildkit:v0.13 при setup-buildx. |
6. Усиляем расследование
- Параллельные builder-нэймспейсы:
build-with: | name=my-app-${{ github.run_id }}
Позволяет одновременно катить несколько PR без конфликтов кэша. - Secret mounts: ENV-переменные
--secret id=sentry,env=SENTRY_AUTH_TOKEN
передаются BuildKit-у безопасно — их нет в итоговом образе. - Мониторинг кэша:
docker buildx du
локально покажет, сколько МБ «седых» слоёв можно подчистить.
7. Финальный акт
Кира запускает workflow — зелёные чеки через 90 секунд. Релиз выходит вовремя, кофе остаётся тёплым, а начальник задаётся вопросом, как быстро повысить столь ценного детектива.