Skip to main content

Як практично використовувати Docker у CI/CD пайплайні?

Docker у CI/CD це workflow, що перетворює code-commit на задеплоєні container. Стандартний патерн добре відомий: build, test, scan, tag, push, deploy. Різниці між командами переважно у cache-стратегії, виборі registry і signing.

Теорія

TL;DR

Форма pipeline:

  1. Checkout коду.
  2. Setup Docker (з BuildKit, multi-arch buildx, якщо треба).
  3. Build image з cache попередніх білдів.
  4. Test всередині image (multi-stage --target test).
  5. Scan на CVE (trivy, grype, Snyk).
  6. Tag commit SHA + branch + semver.
  7. Push у registry (Docker Hub, ECR, GHCR).
  8. Sign через Cosign.
  9. Deploy через посилання на новий tag (або digest).
  • Критичний принцип: build раз, deploy скрізь. Той самий image іде з CI → staging → прод. Не перебудовуй для проду.
  • Cache-backend важить: без --cache-from кожен CI-run стартує холодним.
  • Tag за SHA для відтворюваності; promote'уй, змінюючи на що deploy показує, не перебудовою.

Повний GitHub Actions приклад

yaml
# .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

yaml
cache-from: type=gha cache-to: type=gha,mode=max

Вбудовано у GHA. Безкоштовно до 10GB. Працює для repo-scoped білдів.

Registry cache

yaml
cache-from: type=registry,ref=ghcr.io/myorg/myapp:cache cache-to: type=registry,ref=ghcr.io/myorg/myapp:cache,mode=max

Cache зберігається у твоєму registry. Працює між CI-провайдерами, між repo. Найпортативніший варіант.

S3/inline

yaml
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

dockerfile
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 --from=build /app/dist /app/dist COPY --from=build /app/node_modules /app/node_modules USER node CMD ["node", "dist/server.js"]

CI крутить docker build --target test для валідації. Runtime-стейдж це те, що пушиться для деплою. Той самий Dockerfile, кілька use case.

Vulnerability scanning

yaml
# 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.

Типові помилки

Перебудова для кожного середовища

yaml
# НЕПРАВИЛЬНО - name: Build for staging run: docker build -t myapp:staging . - name: Build for prod run: docker build -t myapp:prod .

Два білди, дві можливості для drift. Image, що ти протестував, це не image, що ти деплоїш.

yaml
# ПРАВИЛЬНО - name: Build once run: docker build -t myapp:${{ github.sha }} . - name: Tag for staging run: docker tag myapp:${{ github.sha }} myapp:staging

Build раз, tag багато.

Забути cache і дивуватися, чому CI повільний

Без cache-from кожен job стартує холодним base-image-pull. Додай cache і дивись, як білди падають з 8 хвилин до 90 секунд.

Використання latest у прод-деплоях

yaml
# НЕПРАВИЛЬНО: прод може pull інший image, ніж тестували deploy: image: myorg/api:latest # ПРАВИЛЬНО: пін на SHA або digest deploy: image: myorg/api:abc123def

Мінливі tag = несподівані rollout.

Вшивання secret у build-args

dockerfile
# НЕПРАВИЛЬНО: 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

yaml
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

groovy
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

yaml
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.

Приклади

yaml
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

bash
# 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 кластерів

yaml
- 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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів