Приватные registry: GHCR, ECR, GAR, Harbor
Docker Hub — это публичный registry. Для prod-кода ты пушишь в приватный registry. В 2026 году выбор такой:
- GitHub Container Registry (GHCR) — встроен в GitHub. Логин через
GITHUB_TOKEN, бесплатно для public, дёшево для private. - Amazon ECR — AWS-native. Интеграция с IAM, лучший выбор, если ты на AWS.
- Google Artifact Registry (GAR) — GCP-native. Заменил deprecated Container Registry.
- Harbor — self-hosted OSS. Под контролем команды, с CVE-scanning и signing из коробки.
В этом уроке: чем они отличаются, как выбрать, и что важно знать про retention/immutability/signing для production-стека.
Зачем TLS — три гарантии безопасности и что без него
Какой registry выбрать
| Registry | Хостинг | Цена (private) | Когда брать |
|---|---|---|---|
| GHCR | github.com | $0.25/GB/мес после 0.5GB | Если код на GitHub, простой setup |
| ECR | AWS | $0.10/GB/мес | k8s/ECS в AWS, IAM integration |
| GAR | GCP | $0.10/GB/мес | GKE / Cloud Run, IAM integration |
| ACR | Azure | $0.167/GB/мес | AKS, Azure integration |
| Harbor | self-hosted | стоимость хоста | Хочешь полный контроль, on-prem, air-gapped |
Для junior’а решение обычно простое: используй cloud-native registry своего облака. На AWS — ECR, на GCP — GAR, на Azure — ACR. Это минимизирует latency (registry и compute в одном регионе) и интегрируется с IAM/IRSA/Workload Identity.
GHCR — отличный default, если у тебя monorepo на GitHub и compute разбросан по разным местам.
Harbor подходит только командам с DevOps-ресурсом для поддержки self-hosted сервиса.
GHCR: самый простой setup
GitHub Container Registry адресуется как ghcr.io/{owner}/{repo}/{image}:{tag}. Логин:
# Создай Personal Access Token (PAT) с scope "write:packages"
# https://github.com/settings/tokens/new
echo $GHCR_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
# Push
docker tag my-etl:v1 ghcr.io/USERNAME/my-repo/my-etl:v1
docker push ghcr.io/USERNAME/my-repo/my-etl:v1
В CI ещё проще — GITHUB_TOKEN создаётся автоматически:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
Visibility: packages в GHCR по умолчанию private. Сделать public:
- Repo Settings -> Packages -> найди package -> Package settings -> Visibility -> Public.
Или через CLI:
gh api --method PATCH /user/packages/container/my-etl/visibility -f visibility=public
ECR: AWS-native
# Создать repository через CLI или Terraform
aws ecr create-repository \
--repository-name my-etl \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability IMMUTABLE
# Login (token expires per 12 hours)
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
# Push
docker tag my-etl:v1 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-etl:v1
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-etl:v1
Ключевые ECR-фичи:
- scan on push — Amazon Inspector сканит CVE при каждом push. Результаты в Console.
- image tag mutability: IMMUTABLE — нельзя перезаписать тег. Push с тем же тегом возвращает ImageAlreadyExists. Это критично для prod-deploy.
- Lifecycle policy — auto-cleanup по правилам (“оставить 30 последних untagged”, “удалить старше 90 дней”).
В CI с OIDC (без AWS-credentials):
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123:role/gha-ecr-push
aws-region: us-east-1
- uses: aws-actions/amazon-ecr-login@v2
- run: |
docker build -t my-etl:${{ github.sha }} .
docker tag my-etl:${{ github.sha }} 123.dkr.ecr.us-east-1.amazonaws.com/my-etl:${{ github.sha }}
docker push 123.dkr.ecr.us-east-1.amazonaws.com/my-etl:${{ github.sha }}
GAR: GCP-native
GAR заменил deprecated Container Registry. Адресуется как {region}-docker.pkg.dev/{project}/{repo}/{image}:{tag}.
gcloud artifacts repositories create my-etl-repo \
--repository-format=docker \
--location=us-east1 \
--description="My ETL images"
gcloud auth configure-docker us-east1-docker.pkg.dev
docker tag my-etl:v1 us-east1-docker.pkg.dev/my-project/my-etl-repo/my-etl:v1
docker push us-east1-docker.pkg.dev/my-project/my-etl-repo/my-etl:v1
GAR features:
- Per-repo permissions через IAM.
- Cleanup policies — аналог ECR lifecycle.
- Native CVE-scanning через Container Analysis API.
- Cross-region replication для disaster recovery.
Harbor: self-hosted
Harbor — OSS-проект от CNCF (incubating). Деплоится как k8s-helm-чарт или docker compose. Фичи:
- Robot accounts — service-аккаунты для CI без человеческих credentials.
- CVE-scanning через встроенный Trivy.
- Image signing через Cosign / Notary.
- Replication между Harbor-instances и external registries.
- Quotas per-project.
Helm-install:
helm repo add harbor https://helm.goharbor.io
helm install harbor harbor/harbor \
--namespace harbor \
--create-namespace \
--set externalURL=https://harbor.internal.company.com
Push:
docker login harbor.internal.company.com -u robot$ci-push -p ROBOT_TOKEN
docker tag my-etl:v1 harbor.internal.company.com/de-team/my-etl:v1
docker push harbor.internal.company.com/de-team/my-etl:v1
Стоимость Harbor — это compute и storage в твоём cloud / on-prem + поддержка. Для маленькой команды это излишество. Для compliance-strict орг (банки, госструктуры) — стандарт.
Retention policies: автоудаление
В prod-CI registry быстро забивается: каждый push на main = новый тег. Через год — гигабайты untagged images. Retention policy автоматически чистит.
ECR пример
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 10 prod tags",
"selection": {
"tagStatus": "tagged",
"tagPatternList": ["v*"],
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": {"type": "expire"}
},
{
"rulePriority": 2,
"description": "Delete untagged after 7 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 7
},
"action": {"type": "expire"}
}
]
}
Применить:
aws ecr put-lifecycle-policy \
--repository-name my-etl \
--lifecycle-policy-text file://policy.json
GHCR пример
В GHCR retention настраивается через UI (Package settings -> Manage versions) или через REST API:
# Удалить все версии старше 90 дней, не привязанные к active deployment
gh api --method GET /user/packages/container/my-etl/versions \
--jq '.[] | select(.created_at < (now - 90*24*60*60 | strftime("%Y-%m-%dT%H:%M:%SZ"))) | .id' | \
xargs -I{} gh api --method DELETE /user/packages/container/my-etl/versions/{}
В GHCR с 2024 года также есть UI-policy: Settings -> Code, planning, and automation -> Packages -> Manage retention.
Immutable tags: критично для prod
Immutable tag — registry отказывается перезаписать существующий тег. Зачем:
Без immutability:
# Прод смотрит на :v1.0.0
# Кто-то делает push my-etl:v1.0.0 с broken-кодом
# Все prod-инстансы при следующем pull получат broken-код
# В docker-логах не понятно, что произошло -- digest изменился, но tag тот же
С immutability:
docker push my-etl:v1.0.0
# uploaded
docker push my-etl:v1.0.0
# Error: tag invalid: The image tag 'v1.0.0' already exists
Прод-теги никогда не перезаписываются. Чтобы пушить новый билд — нужен новый тег.
Включается в registry:
- ECR:
--image-tag-mutability IMMUTABLEпри создании. - GHCR: package settings -> Tag immutability (с 2024).
- GAR:
--immutable-tagsпри создании repo. - Harbor: Project -> Configuration -> Tag immutability (rules per pattern, например
release-*).
ВНИМАНИЕ: immutability обычно делается для release-тегов (v1.2.3), но НЕ для rolling-тегов (latest, main, dev). На rolling-теги immutability ломает CI. Используй паттерн-правила: immutable matching v*, mutable matching main / latest.
Image signing: Cosign / Sigstore
Чтобы убедиться, что образ из registry не подменён, его подписывают. Современный стандарт — Cosign от Sigstore.
# Установка
brew install cosign # или go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# Подписать образ (keyless через OIDC)
cosign sign ghcr.io/USERNAME/my-etl:v1.0.0
# Подписать с ключом
cosign generate-key-pair
cosign sign --key cosign.key ghcr.io/USERNAME/my-etl:v1.0.0
# Верификация
cosign verify --key cosign.pub ghcr.io/USERNAME/my-etl:v1.0.0
Keyless-mode — подпись делается через OIDC-токен GitHub Actions, без хранения ключей. Это де-факто стандарт в 2026 для open-source. Подписи хранятся в Rekor (immutable transparency log).
Для prod-deploy:
- В k8s ставится policy-controller (Kyverno, OPA Gatekeeper), который верифицирует подписи перед запуском Pod’а.
- Образ без подписи — Pod не стартует.
Это защищает от двух атак:
- Compromised registry: атакующий пушит зловреда вместо твоего образа. Без подписи он не пройдёт верификацию.
- Compromised CI: атакующий получил доступ к билду. Но если подпись идёт через OIDC, в Rekor видны метаданные (“этот образ подписан pipeline на main-branch”) — можно отследить инцидент.
Pull-through cache
В CI каждый job стартует с нуля и делает docker pull python:3.11-slim, docker pull postgres:16. Это медленно (Docker Hub rate limits) и тратит сетевой трафик.
Pull-through cache — registry, который проксирует upstream-registries и кэширует.
- ECR pull-through cache — нативная feature: ECR-repo проксирует Docker Hub / Quay / GAR.
123.dkr.ecr.us-east-1.amazonaws.com/docker-hub/python:3.11-slim— кэшированная копия. - Harbor proxy cache projects — то же самое self-hosted.
- registry:2 (open-source Docker Distribution) с
proxyconfig — простой DIY-вариант.
Для DE-команд это снижает CI build time на 30-50% и обходит anonymous rate limit Docker Hub (100 pull/6h).
Попробуй сам
# 1. Создай GitHub PAT с write:packages
# https://github.com/settings/tokens
# 2. Логин в GHCR
echo $TOKEN | docker login ghcr.io -u $USER --password-stdin
# 3. Build & push простого образа
docker pull alpine:3.21
docker tag alpine:3.21 ghcr.io/$USER/test-registry:v1
docker push ghcr.io/$USER/test-registry:v1
# 4. Проверь на github.com -> твой профиль -> Packages
# 5. Pull откуда-то ещё
docker logout ghcr.io
docker pull ghcr.io/$USER/test-registry:v1
# Если public -- работает без логина
# Если private -- 403
# 6. Cosign-подпись (keyless, требует OIDC)
brew install cosign
cosign sign ghcr.io/$USER/test-registry:v1
# Откроется browser для GitHub OAuth, подпись будет сохранена
cosign verify \
--certificate-identity=YOUR_GITHUB_EMAIL \
--certificate-oidc-issuer=https://github.com/login/oauth \
ghcr.io/$USER/test-registry:v1
# 7. Cleanup
docker rmi ghcr.io/$USER/test-registry:v1