Автоматизация CI/CD для Docker-образов: сборка, сканирование, подпись

Ручная сборка и публикация Docker-образов замедляет релизы и оставляет уязвимости в продакшене. Мы автоматизируем полный цикл CI/CD: от многоэтапной сборки с кэшированием до сканирования и подписи образов. Наша команда реализует проект под ключ — с надёжным пайплайном и последующей поддержкой.
Часто задаваемые вопросы

Настройка 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
    - tags

Cosign с 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?

Пошаговая инструкция для добавления кэширования слоёв:

  1. Активируйте Docker BuildKit: DOCKER_BUILDKIT: "1" в variables.
  2. Настройте registry caching: --cache-from type=registry,ref=${IMAGE_NAME}:cache и --cache-to type=registry,ref=${IMAGE_NAME}:cache,mode=max.
  3. Убедитесь, что GitLab Runner использует Docker-in-Docker (dind) с достаточным дисковым пространством.
  4. Проверьте инвалидацию кэша: если меняется package.json, BuildKit пересобирает только этап deps, а не весь пайплайн.
  5. Мониторьте размер кэша: периодически очищайте старые записи через 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-инфраструктуры. Свяжитесь с нами — оценим ваш проект за один рабочий день. Закажите внедрение полного пайплайна: от аудита до документации. Получите консультацию по улучшению пайплайна — пишите.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1394
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1369
  • Разработка логотипа компании B2B Advance
    Разработка логотипа компании B2B Advance
    816
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1142