Налаштування 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: покрокова інструкція
- Активуйте 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 години).
- Підтримка протягом тижня після впровадження.
Ми маємо 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-інфраструктури. Зв'яжіться з нами — оцінимо ваш проект за один робочий день. Замовте впровадження повного пайплайну: від аудиту до документації. Отримайте консультацію з покращення пайплайну — пишіть.







