Автосборка в GitHub Actions:
Возьмём GitHub Actions и публикацию в GHCR. Токен для реестра выдаёт сам раннер, отдельный секрет заводить не нужно. Файл .github/workflows/docker.yml:
Код: Выделить всё
name: docker
on:
push:
branches: [main]
tags: ['v*']
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha
type=semver,pattern={{version}}
- id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Сканирование образов, Trivy и Docker Scout:
Образ собрался, но внутри него базовый дистрибутив с пакетами, у которых есть известные CVE. Сканер сверяет состав образа с базами уязвимостей. Trivy полностью открытый и работает где угодно, Scout встроен в Docker CLI. Локально:
Код: Выделить всё
trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:1.4.2
docker scout quickview myapp:1.4.2
docker scout cves --only-severity critical,high myapp:1.4.2
Код: Выделить всё
- uses: aquasecurity/trivy-action@0.33.0
with:
image-ref: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: '1'
GitLab CI и реалии СНГ:
В СНГ заметная часть команд сидит на self-hosted GitLab. Тот же конвейер на dind выглядит так:
Код: Выделить всё
stages: [build, scan]
build:
stage: build
image: docker:28
services:
- docker:28-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
scan:
stage: scan
image:
name: aquasec/trivy:0.65.0
entrypoint: [""]
variables:
TRIVY_USERNAME: "$CI_REGISTRY_USER"
TRIVY_PASSWORD: "$CI_REGISTRY_PASSWORD"
script:
- trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
И про доступность. Анонимные pull с Docker Hub жёстко лимитированы, а с части провайдеров СНГ внешние реестры периодически отваливаются. Базовые образы держите у себя: подойдут GitLab Dependency Proxy, Harbor с proxy-cache или Yandex Container Registry.
Типичные грабли:
Деплой по latest. Тег перезаписываемый, вы никогда не докажете, что именно крутится на проде. Деплойте по тегу с коммитом или по digest.
Секреты через build-arg. ARG с токеном остаётся в метаданных образа, docker history его покажет. Секреты передавайте только через RUN --mount=type=secret, как в главе 15.
Скан без гейта. Если push прошёл, а скан просто пишет отчёт в артефакты, дырявый образ уже доступен для деплоя. Либо сканируйте до push (в GitHub Actions соберите с load: true), либо блокируйте деплой результатом скана.
Кэш gha не резиновый: лимит 10 ГБ на репозиторий, старые слои вытесняются. Если сборка внезапно стала холодной, смотрите, что выело кэш.
Pull без логина в CI. Облачные раннеры ходят в Docker Hub с общих IP и быстро упираются в лимит. Логиньтесь даже там, где образы только тянутся.
Итог:
Честный минимум пайплайна: buildx с кэшем, Trivy с порогом по severity, публикация с неизменяемым тегом по коммиту и гейт на деплой. Этого хватает большинству команд. В следующей главе соберём итоговый проект целиком, от Dockerfile до продакшена, и посмотрим в сторону Kubernetes, Podman и стандартов OCI.