Як практично використовувати Docker у CI/CD пайплайні?
Docker у CI/CD це workflow, що перетворює code-commit на задеплоєні container. Стандартний патерн добре відомий: build, test, scan, tag, push, deploy. Різниці між командами переважно у cache-стратегії, виборі registry і signing.
Теорія
TL;DR
Форма pipeline:
- Checkout коду.
- Setup Docker (з BuildKit, multi-arch buildx, якщо треба).
- Build image з cache попередніх білдів.
- Test всередині image (multi-stage
--target test). - Scan на CVE (trivy, grype, Snyk).
- Tag commit SHA + branch + semver.
- Push у registry (Docker Hub, ECR, GHCR).
- Sign через Cosign.
- Deploy через посилання на новий tag (або digest).
- Критичний принцип: build раз, deploy скрізь. Той самий image іде з CI → staging → прод. Не перебудовуй для проду.
- Cache-backend важить: без
--cache-fromкожен CI-run стартує холодним. - Tag за SHA для відтворюваності; promote'уй, змінюючи на що deploy показує, не перебудовою.
Повний GitHub Actions приклад
# .github/workflows/ci.yml
name: build-test-deploy
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # для GHCR
id-token: write # для Sigstore OIDC
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
id: build
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
labels: |
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.revision=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run tests
run: |
docker run --rm ghcr.io/${{ github.repository }}:${{ github.sha }} npm test
- name: Scan for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
exit-code: '1'
severity: 'HIGH,CRITICAL'
- uses: sigstore/cosign-installer@v3
- name: Sign image
if: github.event_name != 'pull_request'
run: cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}Це покриває build, test, scan, sign, push в одному workflow. Tag за SHA, promote пізніше.
Cache-стратегії
Без cache кожен CI-run перетягує base-image і перевиконує кожен крок. Три поширені backend:
GitHub Actions cache
cache-from: type=gha
cache-to: type=gha,mode=maxВбудовано у GHA. Безкоштовно до 10GB. Працює для repo-scoped білдів.
Registry cache
cache-from: type=registry,ref=ghcr.io/myorg/myapp:cache
cache-to: type=registry,ref=ghcr.io/myorg/myapp:cache,mode=maxCache зберігається у твоєму registry. Працює між CI-провайдерами, між repo. Найпортативніший варіант.
S3/inline
cache-from: type=s3,region=us-east-1,bucket=mybucket
cache-to: type=s3,region=us-east-1,bucket=mybucketДля self-hosted runner або AWS-native pipeline.
З хорошим caching повторні білди без source-змін завершуються за секунди.
Tagging-стратегія
ghcr.io/myorg/api:abc123def # commit SHA — якір відтворюваності
ghcr.io/myorg/api:1.2.3 # semver — для прод-деплоїв
ghcr.io/myorg/api:1.2 # semver minor — авто-pull останнього patch
ghcr.io/myorg/api:1 # semver major — авто-pull останнього у лінії
ghcr.io/myorg/api:latest # tip of main — для non-production користувачів
ghcr.io/myorg/api:pr-1234 # PR-білди — для review-appПрод-деплої посилаються на SHA-tag (або @digest-форму). Semver-tags для людей і downstream-споживачів.
Build once, promote
CI: build myreg/api:abc123def → push
Staging: deploy myreg/api:abc123def
Prod: deploy ТОЙ САМИЙ myreg/api:abc123defНЕ май окремого «прод-білду». Image, що ти протестував у staging, це image, що іде у прод. Якщо перебудовуєш для проду, ти не реально тестував те, що шиппиш.
Multi-stage Dockerfile для CI
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM deps AS test
COPY . .
RUN npm test
FROM deps AS build
COPY . .
RUN npm run build
FROM node:22-alpine AS runtime
WORKDIR /app
COPY /app/dist /app/dist
COPY /app/node_modules /app/node_modules
USER node
CMD ["node", "dist/server.js"]CI крутить docker build --target test для валідації. Runtime-стейдж це те, що пушиться для деплою. Той самий Dockerfile, кілька use case.
Vulnerability scanning
# Trivy у GitHub Actions
- uses: aquasecurity/trivy-action@master
with:
image-ref: myorg/api:${{ github.sha }}
severity: 'HIGH,CRITICAL'
exit-code: '1'
ignore-unfixed: true
# Або як Dockerfile build-стейдж (ловить при build-time)
FROM aquasec/trivy:0.59.0 AS scan
COPY --from=build / /scan-target
RUN trivy filesystem --severity HIGH,CRITICAL --exit-code 1 /scan-targetЗавали білд на HIGH/CRITICAL CVE. Дозволь винятки через .trivyignore.
Типові помилки
Перебудова для кожного середовища
# НЕПРАВИЛЬНО
- name: Build for staging
run: docker build -t myapp:staging .
- name: Build for prod
run: docker build -t myapp:prod .Два білди, дві можливості для drift. Image, що ти протестував, це не image, що ти деплоїш.
# ПРАВИЛЬНО
- name: Build once
run: docker build -t myapp:${{ github.sha }} .
- name: Tag for staging
run: docker tag myapp:${{ github.sha }} myapp:stagingBuild раз, tag багато.
Забути cache і дивуватися, чому CI повільний
Без cache-from кожен job стартує холодним base-image-pull. Додай cache і дивись, як білди падають з 8 хвилин до 90 секунд.
Використання latest у прод-деплоях
# НЕПРАВИЛЬНО: прод може pull інший image, ніж тестували
deploy:
image: myorg/api:latest
# ПРАВИЛЬНО: пін на SHA або digest
deploy:
image: myorg/api:abc123defМінливі tag = несподівані rollout.
Вшивання secret у build-args
# НЕПРАВИЛЬНО: BUILD_TOKEN видно у image-history
ARG BUILD_TOKEN
RUN curl -H "Auth: $BUILD_TOKEN" ...Бери BuildKit secret-mount: RUN --mount=type=secret,id=token .... Secret лишається поза image-history.
Не тестувати всередині image
Якщо тести крутяться на host (npm test поза Docker), ти не тестуєш те, що шиппиш. Тестуй у тому ж image, що деплоїтиметься: docker build --target test . або docker run --rm myapp:test npm test.
Реальні варіації
GitLab CI
build:
stage: build
image: docker:27
services:
- docker:27-dind
script:
- docker build --cache-from $CI_REGISTRY_IMAGE:cache -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHAПатерн docker-in-docker (DinD) GitLab. Вбудований registry per project.
Jenkins
pipeline {
stages {
stage('Build') {
steps {
sh 'docker buildx build --cache-from type=registry,ref=myreg/cache --tag myreg/api:${env.GIT_COMMIT} .'
}
}
}
}Довга історія, heavy plugin-екосистема, declarative-pipeline стали нормою.
CircleCI
jobs:
build:
docker:
- image: cimg/base:current
steps:
- checkout
- setup_remote_docker
- run: docker buildx build --tag myorg/api:${CIRCLE_SHA1} .setup_remote_docker CircleCI дає daemon для білдів.
Питання для поглиблення
Q: Чи варто крутити тести всередині image чи на host?
A: Всередині image. Test-середовище має точно відповідати проду, та сама OS, ті самі версії бібліотек, ті самі шляхи. Multi-stage build з test-target це канонічний патерн.
Q: Що таке docker buildx і чому використовувати у CI?
A: buildx це BuildKit-aware Docker CLI-розширення. Дає multi-arch builds, просунуті cache-backend, secret-mount і значно швидші білди. CI має використовувати buildx (через docker/setup-buildx-action) за замовчуванням.
Q: Як обробляти secret як NPM-токени у CI-білдах?
A: BuildKit secret-mount: RUN --mount=type=secret,id=npmrc cp /run/secrets/npmrc ~/.npmrc && npm ci. Передавай через CI: --secret id=npmrc,src=$HOME/.npmrc. Secret ніколи не приземляється у жоден шар.
Q: Чи варто пушити pull-request білди у registry?
A: Так, у окремий tag (pr-1234). Дозволяє reviewer крутити реально побудований image, також вмикає review-app. Auto-prune старі PR-tag через cron або registry-policy.
Q: (Senior) Як валідувати, що image, задеплоєний у проді, бі-в-біт той самий, що тестували у CI?
A: Пінься на digest, не на tag. CI ловить digest з output docker push і пише його у deploy-manifest. Прод посилається на myreg/api@sha256:abc.... Tag-mutation на це не впливає. У комбінації з Cosign-верифікацією при admission отримуєш криптографічну впевненість: байти, що обслуговують прод, це байти, що пройшли CI.
Приклади
Full-featured GitHub Actions workflow
name: ci-cd
on:
push: { branches: [main] }
pull_request:
env:
REGISTRY: ghcr.io
IMAGE: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # OIDC для Sigstore
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- name: Test
run: docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} npm test
- name: Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }}
severity: HIGH,CRITICAL
exit-code: '1'
- uses: sigstore/cosign-installer@v3
- name: Sign
run: cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE }}@${{ steps.build.outputs.digest }}
deploy-staging:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: |
# Update staging до нового digest
kubectl set image deploy/api api=${{ env.REGISTRY }}/${{ env.IMAGE }}@${{ needs.build.outputs.digest }}Build → test → scan → sign → push → staging-deploy за digest. Єдине джерело правди.
Promotion через image-retag
# CI зібрав і запушив
myreg/api:abc123def # commit SHA
# Staging отримує
kubectl set image deploy/api api=myreg/api:abc123def
# Після верифікації, promote у прод
docker pull myreg/api:abc123def
docker tag myreg/api:abc123def myreg/api:1.2.3
docker tag myreg/api:abc123def myreg/api:prod-stable
docker push myreg/api:1.2.3
docker push myreg/api:prod-stable
# Прод використовує той самий digest, лише різні tag
kubectl set image deploy/api api=myreg/api:1.2.3ТОЙ САМИЙ image (той самий digest) іде зі staging у прод. Promotion це лише relabel.
Multi-arch build для гібридних x86/ARM кластерів
- uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
push: true
tags: myorg/api:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=maxОдин CI-крок будує для обох архітектур. Споживачі pull'ять, що збігається з їхнім CPU. Важливо для кластерів, що міксують Graviton і x86 node.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів