Choosing a container registry (ECR/GCR/Harbor/Docker Hub)
Scope: Registry options (ECR/GCR/Harbor), image tagging, promotion workflows Lines: ~280 Last Updated: 2025-10-18
Activate this skill when:
Container registry: Storage and distribution system for container images.
Key properties:
Registry workflow:
Build → Tag → Push → Registry → Pull → Deploy
Characteristics:
Usage:
# Login
docker login
# Tag
docker tag myapp:latest username/myapp:v1.0.0
# Push
docker push username/myapp:v1.0.0
# Pull
docker pull username/myapp:v1.0.0
Pros: Easy, well-integrated, global CDN Cons: Rate limits (100 pulls/6h unauthenticated), costs for private repos
Characteristics:
Setup:
# Login (get token from AWS)
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
123456789012.dkr.ecr.us-east-1.amazonaws.com
# Create repository
aws ecr create-repository --repository-name myapp
# Tag
docker tag myapp:latest \
123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0.0
# Push
docker push \
123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0.0
Pros: AWS integration, security scanning, fine-grained IAM Cons: AWS-only, regional (latency), costs per storage/transfer
Characteristics:
Setup:
# Login (using gcloud)
gcloud auth configure-docker
# Tag
docker tag myapp:latest gcr.io/my-project/myapp:v1.0.0
# Push
docker push gcr.io/my-project/myapp:v1.0.0
# Pull
docker pull gcr.io/my-project/myapp:v1.0.0
Artifact Registry (newer, recommended):
# Login
gcloud auth configure-docker us-docker.pkg.dev
# Push
docker push us-docker.pkg.dev/my-project/my-repo/myapp:v1.0.0
Pros: GCP integration, global, good performance Cons: GCP-only, costs per storage/transfer
Characteristics:
Setup:
# Login
az acr login --name myregistry
# Tag
docker tag myapp:latest myregistry.azurecr.io/myapp:v1.0.0
# Push
docker push myregistry.azurecr.io/myapp:v1.0.0
Pros: Azure integration, geo-replication, security scanning Cons: Azure-only, costs per tier
Characteristics:
Setup:
# Login (using GitHub token)
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
# Tag
docker tag myapp:latest ghcr.io/username/myapp:v1.0.0
# Push
docker push ghcr.io/username/myapp:v1.0.0
GitHub Actions:
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
push: true
tags: ghcr.io/${{ github.repository }}:latest
Pros: Free, GitHub integration, easy CI/CD Cons: GitHub-specific, less enterprise features
Characteristics:
Setup (Docker Compose):
version: '3.8'
services:
registry:
image: goharbor/harbor-registryctl:v2.9.0
volumes:
- registry_data:/storage
core:
image: goharbor/harbor-core:v2.9.0
depends_on:
- registry
volumes:
registry_data:
Pros: Self-hosted, full control, no vendor lock-in Cons: Operational overhead, requires infrastructure
| Registry | Cost | Private | Scanning | Multi-Cloud | Best For | |----------|------|---------|----------|-------------|----------| | Docker Hub | Free/Paid | Limited | No | Yes | Public images, quick start | | ECR | Pay-per-use | Yes | Yes | No (AWS) | AWS workloads | | GCR/AR | Pay-per-use | Yes | Yes | No (GCP) | GCP workloads | | ACR | Tiered | Yes | Yes | No (Azure) | Azure workloads | | GHCR | Free (generous) | Yes | No | Yes | GitHub projects | | Harbor | Infrastructure | Yes | Yes | Yes | Self-hosted, multi-cloud |
Format: MAJOR.MINOR.PATCH (e.g., 1.2.3)
# Tag with semantic version
docker tag myapp:latest myregistry/myapp:1.2.3
# Also tag mutable aliases
docker tag myapp:latest myregistry/myapp:1.2 # Minor series
docker tag myapp:latest myregistry/myapp:1 # Major series
docker tag myapp:latest myregistry/myapp:latest # Latest
# Push all tags
docker push myregistry/myapp:1.2.3
docker push myregistry/myapp:1.2
docker push myregistry/myapp:1
docker push myregistry/myapp:latest
Deployment:
# Production: Pin exact version
image: myregistry/myapp:1.2.3
# Staging: Minor series (auto-patch updates)
image: myregistry/myapp:1.2
# Dev: Latest (always newest)
image: myregistry/myapp:latest
Commit SHA (immutable, traceable):
# Tag with git commit SHA
GIT_SHA=$(git rev-parse --short HEAD)
docker tag myapp:latest myregistry/myapp:$GIT_SHA
docker push myregistry/myapp:$GIT_SHA
Branch name:
# Tag with branch
BRANCH=$(git rev-parse --abbrev-ref HEAD)
docker tag myapp:latest myregistry/myapp:$BRANCH
docker push myregistry/myapp:$BRANCH
Combined (version + SHA):
VERSION=1.2.3
GIT_SHA=$(git rev-parse --short HEAD)
docker tag myapp:latest myregistry/myapp:${VERSION}-${GIT_SHA}
docker push myregistry/myapp:${VERSION}-${GIT_SHA}
# Example: myregistry/myapp:1.2.3-abc123
# Tag by environment
docker tag myapp:latest myregistry/myapp:dev
docker tag myapp:latest myregistry/myapp:staging
docker tag myapp:latest myregistry/myapp:prod
CI/CD:
# GitHub Actions
- name: Tag and push
run: |
docker tag myapp:latest myregistry/myapp:${{ github.sha }}
docker tag myapp:latest myregistry/myapp:${{ github.ref_name }}
docker push --all-tags myregistry/myapp
❌ Using only :latest
docker push myregistry/myapp:latest
# Problem: No version history, hard to rollback
❌ Dates as tags
docker tag myapp:latest myregistry/myapp:2025-10-18
# Problem: No semantic meaning, hard to compare
❌ Overwriting tags
docker tag myapp:latest myregistry/myapp:1.2.3
docker push myregistry/myapp:1.2.3
# Later: rebuild and push same tag
# Problem: Tag now points to different image!
✅ Best practice: Use immutable tags (SHA, version) + mutable aliases (:latest, :stable)
Workflow: Dev → Staging → Production
# Step 1: Build and tag
docker build -t myregistry/myapp:${VERSION}-${SHA} .
# Step 2: Push to registry
docker push myregistry/myapp:${VERSION}-${SHA}
# Step 3: Deploy to dev
docker tag myregistry/myapp:${VERSION}-${SHA} myregistry/myapp:dev
docker push myregistry/myapp:dev
# Step 4: Promote to staging (after tests pass)
docker pull myregistry/myapp:${VERSION}-${SHA}
docker tag myregistry/myapp:${VERSION}-${SHA} myregistry/myapp:staging
docker push myregistry/myapp:staging
# Step 5: Promote to prod (after staging validation)
docker pull myregistry/myapp:${VERSION}-${SHA}
docker tag myregistry/myapp:${VERSION}-${SHA} myregistry/myapp:prod
docker push myregistry/myapp:prod
Key: Immutable base tag (${VERSION}-${SHA}), mutable environment tags.
Multiple registries (dev/staging/prod):
# Dev registry
docker tag myapp:latest dev-registry/myapp:1.2.3
docker push dev-registry/myapp:1.2.3
# Promote to staging registry
docker pull dev-registry/myapp:1.2.3
docker tag dev-registry/myapp:1.2.3 staging-registry/myapp:1.2.3
docker push staging-registry/myapp:1.2.3
# Promote to prod registry
docker pull staging-registry/myapp:1.2.3
docker tag staging-registry/myapp:1.2.3 prod-registry/myapp:1.2.3
docker push prod-registry/myapp:1.2.3
Single registry, different repos:
# Push to dev repo
docker push myregistry/dev/myapp:1.2.3
# Promote to staging repo
docker pull myregistry/dev/myapp:1.2.3
docker tag myregistry/dev/myapp:1.2.3 myregistry/staging/myapp:1.2.3
docker push myregistry/staging/myapp:1.2.3
# Promote to prod repo
docker pull myregistry/staging/myapp:1.2.3
docker tag myregistry/staging/myapp:1.2.3 myregistry/prod/myapp:1.2.3
docker push myregistry/prod/myapp:1.2.3
name: Build and Promote
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Login to Registry
uses: docker/login-action@v2
with:
registry: myregistry.io
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4
with:
push: true
tags: |
myregistry.io/myapp:${{ github.sha }}
myregistry.io/myapp:dev
promote-staging:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Promote to staging
run: |
docker pull myregistry.io/myapp:${{ github.sha }}
docker tag myregistry.io/myapp:${{ github.sha }} myregistry.io/myapp:staging
docker push myregistry.io/myapp:staging
promote-prod:
needs: promote-staging
runs-on: ubuntu-latest
environment: production # Requires approval
steps:
- name: Promote to prod
run: |
docker pull myregistry.io/myapp:${{ github.sha }}
docker tag myregistry.io/myapp:${{ github.sha }} myregistry.io/myapp:prod
docker push myregistry.io/myapp:prod
# Login
docker login
# Enter: username, password
# Logout
docker logout
# Get login password
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
123456789012.dkr.ecr.us-east-1.amazonaws.com
IAM Policy (push/pull):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:PutImage"
],
"Resource": "*"
}
]
}
# Configure Docker to use gcloud credentials
gcloud auth configure-docker
# Or manually
gcloud auth print-access-token | \
docker login -u oauth2accesstoken --password-stdin \
https://gcr.io
# Create personal access token (read:packages, write:packages)
echo $GITHUB_TOKEN | \
docker login ghcr.io -u USERNAME --password-stdin
docker login myregistry.example.com
# Enter: username, password
Manually delete:
# Via UI or API
curl -X DELETE \
-H "Authorization: Bearer $TOKEN" \
https://hub.docker.com/v2/repositories/username/myapp/tags/old-tag/
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 10 images",
"selection": {
"tagStatus": "any",
"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"
}
}
]
}
Apply:
aws ecr put-lifecycle-policy \
--repository-name myapp \
--lifecycle-policy-text file://policy.json
# Delete images older than 30 days
gcloud artifacts docker images delete \
us-docker.pkg.dev/my-project/my-repo/myapp:old-tag \
--delete-tags
Via UI: Project → Policy → Retention Rules
Symptom: unauthorized: authentication required
Solution: Re-login
# Check current login
cat ~/.docker/config.json
# Re-login
docker login myregistry.io
Symptom: Error response from daemon: manifest for myapp:v1.0.0 not found
Solution: Check tag exists
# List tags (Docker Hub)
curl https://hub.docker.com/v2/repositories/username/myapp/tags
# List tags (ECR)
aws ecr describe-images --repository-name myapp
Symptom: Push/pull takes very long
Solutions:
# Login
docker login myregistry.io
# Tag
docker tag myapp:latest myregistry.io/myapp:v1.0.0
# Push
docker push myregistry.io/myapp:v1.0.0
# Pull
docker pull myregistry.io/myapp:v1.0.0
# Remove local image
docker rmi myregistry.io/myapp:v1.0.0
# Push all tags
docker push --all-tags myregistry.io/myapp
# Immutable: Version + SHA
docker tag myapp:latest myregistry/myapp:1.2.3-abc123
# Mutable: Environment
docker tag myapp:latest myregistry/myapp:dev
docker tag myapp:latest myregistry/myapp:staging
docker tag myapp:latest myregistry/myapp:prod
# Mutable: Latest
docker tag myapp:latest myregistry/myapp:latest
dockerfile-optimization.md - Building efficient imagescontainer-security.md - Scanning images for vulnerabilitiesci-cd-pipelines.md - Automating builds and promotionskubernetes-deployments.md - Deploying images to K8sLast Updated: 2025-10-18 Format Version: 1.0 (Atomic)