Настройка CI/CD для Docker-образов (Build/Scan/Push)
Типичная история: разработчик пушит в main, GitLab Runner делает docker build && docker push, образ уходит в production. Без layer caching сборка занимает 8 минут при каждом коммите. Без сканирования в registry живут образы с CRITICAL уязвимостями. Без семантического тегирования история образов — это latest, latest, latest. Мы сталкивались с этим десятки раз: в одном проекте сборка занимала 12 минут, а половина образов в registry содержала уязвимости HIGH-уровня. Настройка правильного пайплайна решает все три проблемы сразу и превращает хаос в предсказуемый процесс.
Что должен делать полноценный Docker CI/CD пайплайн
Сборка → Сканирование → Подпись → Публикация → Тегирование. Это не один docker build -t myapp:latest && docker push, это пять отдельных этапов с разными инструментами и точками отказа. Каждый этап — отдельная стадия в CI, чтобы при ошибке не перезапускать весь процесс.
Пайплайн должен завершаться неудачей, если сканер нашёл CRITICAL уязвимость, — и никогда не пушить небезопасный образ в registry. Это предотвращает ситуацию, когда команда даже не знает, что деплоит образ с дырами. По нашему опыту, автоматическое сканирование сокращает количество инцидентов безопасности на 70%.
Multi-stage Dockerfile с BuildKit
Основа правильного пайплайна — хороший Dockerfile. Многоэтапная сборка уменьшает итоговый образ в 5–10 раз и улучшает кэширование. Сравните: одноэтапный Dockerfile с полным SDK весит 1.2 GB, а multi-stage — 150 MB. Разница в скорости деплоя и затратах на хранение очевидна.
# syntax=docker/dockerfile:1.4
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN --mount=type=cache,target=/root/.npm \
npm run build
FROM node:20-alpine AS runner
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
WORKDIR /app
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
USER nextjs
EXPOSE 3000
CMD ["node", "dist/server.js"]
Production-образ не содержит devDependencies, исходников и кэша. Запускается под непривилегированным пользователем. BuildKit cache mounts ускоряют npm install — повторная установка занимает секунды, а не минуты.
GitLab CI: полный пайплайн
# .gitlab-ci.yml
variables:
DOCKER_BUILDKIT: "1"
IMAGE_NAME: ${CI_REGISTRY_IMAGE}
IMAGE_TAG: ${CI_COMMIT_SHA}
stages:
- build
- scan
- sign
- push
build:
stage: build
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
script:
- docker buildx create --use
- docker buildx build --cache-from type=registry,ref=${IMAGE_NAME}:cache --cache-to type=registry,ref=${IMAGE_NAME}:cache,mode=max --tag ${IMAGE_NAME}:${IMAGE_TAG} --output type=image,push=false --load .
- docker save ${IMAGE_NAME}:${IMAGE_TAG} > image.tar
artifacts:
paths:
- image.tar
expire_in: 1 hour
scan:
stage: scan
image: aquasec/trivy:latest
before_script:
- docker load < image.tar
script:
- trivy image --severity CRITICAL,HIGH --ignore-unfixed --exit-code 1 --format table ${IMAGE_NAME}:${IMAGE_TAG}
dependencies:
- build
push:
stage: push
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
- docker load < image.tar
script:
- docker push ${IMAGE_NAME}:${IMAGE_TAG}
- |
if [ "${CI_COMMIT_BRANCH}" = "main" ]; then
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest
docker push ${IMAGE_NAME}:latest
fi
- |
if [ -n "${CI_COMMIT_TAG}" ]; then
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:${CI_COMMIT_TAG}
docker push ${IMAGE_NAME}:${CI_COMMIT_TAG}
fi
dependencies:
- build
only:
- main
- tags
Передача образа между stages через image.tar — способ избежать повторной сборки. Сканируем именно тот образ, который будет задеплоен, а не пересобираем. Это экономит до 40% времени CI-пайплайна.
Подпись образов через Cosign
sign:
stage: sign
image: cgr.dev/chainguard/cosign:latest
script:
- docker load < image.tar
- cosign sign --yes ${IMAGE_NAME}:${IMAGE_TAG}@$(docker inspect --format='{{index .RepoDigests 0}}' ${IMAGE_NAME}:${IMAGE_TAG})
variables:
COSIGN_EXPERIMENTAL: "1"
dependencies:
- build
only:
- main
- tagsCosign с keyless signing использует GitLab OIDC token как идентификатор — не нужно хранить приватный ключ. Подпись прикрепляется к образу в registry по digest. При деплое можно верифицировать: cosign verify ${IMAGE_NAME}:${CI_COMMIT_SHA}. Это обязательный шаг для соответствия SLSA Level 2.
Caching стратегии
type=registry для cache-from/cache-to — кэш хранится прямо в container registry. Работает на любом GitLab Runner, не требует общего volume. mode=max кэширует все промежуточные слои, не только финальный. По сравнению с локальным кэш-диском, registry-кэш даёт прирост скорости на 30–40% при параллельных сборках. В официальной документации BuildKit рекомендуется использовать registry-кэш для распределённых сборок.
Альтернатива для GitHub Actions — cache-from: type=gha с actions/cache. Хранится в GitHub Artifacts, максимум 10GB, автоматически истекает. Но registry-кэш надёжнее — не зависит от лимитов артефактов.
Дополнительная оптимизация кэша
Для ускорения сборки используйте `--cache-to` с `mode=max` и `compression=zstd` для уменьшения размера кэша. При параллельных сборках это даёт дополнительный прирост до 20%.Семантическое тегирование
| Событие | Теги |
|---|---|
| Коммит в main | sha-abc1234, main-latest |
Git tag v1.2.3 |
v1.2.3, 1.2.3, 1.2, 1, latest |
| PR/MR | pr-123-sha, без latest |
| Nightly cron | nightly-YYYYMMDD |
Для автоматического семантического тегирования в GitHub Actions — docker/metadata-action. В GitLab — скрипт на основе CI_COMMIT_TAG и CI_COMMIT_SHA. Правильное тегирование позволяет легко откатить образ до любой версии и аудитировать изменения.
Почему важен layer caching?
Без кэширования каждый коммит пересобирает все слои: установка зависимостей, сборка, копирование. Если зависимости не менялись, они должны браться из кэша. BuildKit с --cache-from registry даёт ускорение в 5–7 раз на типовом проекте. На одном из проектов мы сократили время сборки с 9 минут до 1.5 минут — разработчики перестали ждать и ускорили релизный цикл.
Как настроить layer caching в GitLab CI?
Пошаговая инструкция для добавления кэширования слоёв:
- Активируйте Docker BuildKit:
DOCKER_BUILDKIT: "1"в variables. - Настройте registry caching:
--cache-from type=registry,ref=${IMAGE_NAME}:cacheи--cache-to type=registry,ref=${IMAGE_NAME}:cache,mode=max. - Убедитесь, что GitLab Runner использует Docker-in-Docker (dind) с достаточным дисковым пространством.
- Проверьте инвалидацию кэша: если меняется
package.json, BuildKit пересобирает только этап deps, а не весь пайплайн. - Мониторьте размер кэша: периодически очищайте старые записи через
docker buildx pruneили настройте политику автоматического удаления.
Как избежать повторной сборки образов?
Используйте артефакты для передачи собранного образа между стадиями. В GitLab это image.tar, в GitHub Actions — docker save + upload-artifact. Сканирование и подпись работают с тем же самым артефактом, а не с пересобранным. Это исключает расхождение между тем, что просканировано, и тем, что задеплоено.
Что входит в работу
- Аудит текущего Dockerfile и CI-конфигурации.
- Переписывание Dockerfile под multi-stage с BuildKit cache mounts.
- Настройка пайплайна со стадиями build/scan/push.
- Конфигурирование registry caching (cache-from/cache-to).
- Добавление Cosign подписи с keyless signing.
- Настройка семантического тегирования по git tags.
- Документация и обучение команды (1–2 часа).
- Поддержка в течение недели после внедрения.
| Масштаб | Срок |
|---|---|
| Базовый пайплайн build/scan/push | 1 день |
| + Cosign + semver тегирование | 2–3 дня |
| Multi-arch builds + несколько registry | 3–4 дня |
Стоимость рассчитывается индивидуально после аудита текущей CI-конфигурации и registry-инфраструктуры. Свяжитесь с нами — оценим ваш проект за один рабочий день. Закажите внедрение полного пайплайна: от аудита до документации. Получите консультацию по улучшению пайплайна — пишите.







