Налаштування CI/CD для Docker-образів: від збірки до підпису

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

Налаштування CI/CD для Docker-образів (Build/Scan/Push)

Типова історія: розробник пушить у main, GitLab Runner робить docker build && docker push, образ іде у production. Без layer caching збірка займає 8 хвилин при кожному коміті (на 60% довше, ніж з кешем). Без сканування в registry живуть образи з CRITICAL вразливостями. Без семантичного тегування історія образів — це latest, latest, latest. Ми стикалися з цим десятки разів: в одному проекті збірка займала 12 хвилин (на 40% довше, ніж з оптимізацією), а половина образів у registry містила вразливості HIGH-рівня. Налаштування правильного пайплайну вирішує всі три проблеми одразу і перетворює хаос на передбачуваний процес. Впровадження повного пайплайну окупається за 2-3 місяці завдяки економії на інфраструктурі — до $2000 на місяць.

Що має робити повноцінний 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 у 5 разів швидше, ніж без кешування — повторна установка займає секунди, а не хвилини. Trivy сканує образ у 2-3 рази швидше за Snyk, а Cosign підписує за секунди.

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 (SLSA специфікація).

Як обрати оптимальну caching стратегію?

type=registry для cache-from/cache-to — кеш зберігається прямо в container registry. Працює на будь-якому GitLab Runner, не потребує спільного volume. mode=max кешує всі проміжні шари, не тільки фінальний. Порівняно з локальним кеш-диском, registry-кеш дає приріст швидкості на 30–40% при паралельних збірках. В офіційній документації BuildKit (Docker BuildKit docs) рекомендується використовувати 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 години).
  • Підтримка протягом тижня після впровадження.

Ми маємо 5 років досвіду налаштування CI/CD для Docker-образів, впровадили рішення в 20+ проектах. Гарантія якості: всі пайплайни тестуються на реальних проектах. Сертифіковані спеціалісти Kubernetes.

Масштаб Термін Вартість (орієнтовно)
Базовий пайплайн build/scan/push 1 день від $500
+ Cosign + semver тегування 2–3 дні від $1200
Multi-arch builds + декілька registry 3–4 дні від $2000

Вартість розраховується індивідуально після аудиту поточної CI-конфігурації та registry-інфраструктури. Зв'яжіться з нами — оцінимо ваш проект за один робочий день. Замовте впровадження повного пайплайну: від аудиту до документації. Отримайте консультацію з покращення пайплайну — пишіть.

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

Останні роботи

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