Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як практично використовувати Docker у CI/CD пайплайні?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Стандартний Docker CI/CD-pipeline:** checkout → build image з cache → run тести у image → scan на вразливості → tag commit SHA + semver → push у registry → deploy. ```yaml # GitHub Actions приклад - uses: docker/login-action@v3 - uses: docker/build-push-action@v5 with: push: true tags: myorg/api:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max ``` **Головне:** трактуй image як build-артефакт. Бери cache-backend (registry, GHA) для швидких rebuild. Tag за commit SHA для відтворюваності. Підписуй через Cosign. Promote'уй протестовані image у прод, не перебудовуй.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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. ## Приклади ### Full-featured GitHub Actions workflow ```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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.