Container Registry Management#
A container registry stores and distributes your images. Getting registry operations right – tagging, access control, garbage collection, signing – prevents a class of problems ranging from “which version is deployed?” to “someone pushed a compromised image.”
Registry Options#
Docker Hub – The default registry. Free tier has rate limits (100 pulls per 6 hours for anonymous, 200 for authenticated). Public images only on free plans.
GitHub Container Registry (ghcr.io) – Tight integration with GitHub Actions. Free for public images, included storage for private repos. Authenticate with a GitHub PAT or GITHUB_TOKEN in Actions.
Amazon ECR – Managed AWS registry. Per-region, per-account. Includes built-in image scanning (basic and enhanced via Inspector). Lifecycle policies for automated cleanup.
Google Artifact Registry – Replaces GCR. Multi-format (Docker, Maven, npm, Python). Integrates with GKE workload identity for pull authentication.
Harbor – Open-source, self-hosted. Supports replication, vulnerability scanning, image signing, RBAC, and audit logs. Run this when you need full control or air-gapped environments.
Image Tagging Strategies#
Tags are mutable pointers to image digests. This is a critical point: pushing a new image with the same tag overwrites the previous one.
Avoid :latest#
The :latest tag is the default when no tag is specified. It tells you nothing about what version is running. Kubernetes caches images by tag, so if you push a new :latest and the node already has the old one cached, it will not pull the new image unless imagePullPolicy: Always is set.
Recommended: Semantic Version + Git SHA#
# Tag with semantic version for humans
docker tag myapp:build myregistry.io/myapp:1.4.2
# Tag with git SHA for exact traceability
docker tag myapp:build myregistry.io/myapp:sha-a1b2c3d
# Push both
docker push myregistry.io/myapp:1.4.2
docker push myregistry.io/myapp:sha-a1b2c3dIn your Kubernetes manifests, reference the semantic version for readability. In your CI logs, record the SHA tag so you can trace any deployed image back to the exact commit.
A CI pipeline typically handles this:
VERSION=$(git describe --tags --always)
SHA=$(git rev-parse --short HEAD)
IMAGE="myregistry.io/myapp"
docker build -t ${IMAGE}:${VERSION} -t ${IMAGE}:${SHA} .
docker push ${IMAGE}:${VERSION}
docker push ${IMAGE}:${SHA}Authenticated Pulls in Kubernetes#
Private registries require credentials. Kubernetes uses imagePullSecrets to authenticate.
Create the secret:
kubectl create secret docker-registry regcred \
--docker-server=myregistry.io \
--docker-username=robot-account \
--docker-password="${REGISTRY_TOKEN}" \
--namespace=productionReference in pod spec:
spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: myregistry.io/myapp:1.4.2Attach to a ServiceAccount so every pod in the namespace uses it:
kubectl patch serviceaccount default -n production \
-p '{"imagePullSecrets": [{"name": "regcred"}]}'For AWS ECR, the token expires every 12 hours. Use a CronJob or a tool like ecr-credential-helper to refresh it automatically. For GKE with Artifact Registry, use Workload Identity to avoid managing credentials entirely.
Image Signing with Cosign#
Image signing verifies that an image was built by your CI pipeline and has not been tampered with. Cosign from the Sigstore project makes this straightforward.
Generate a key pair (for key-based signing):
cosign generate-key-pair
# Creates cosign.key (private) and cosign.pub (public)Sign an image after pushing:
cosign sign --key cosign.key myregistry.io/myapp:1.4.2Verify a signature before deploying:
cosign verify --key cosign.pub myregistry.io/myapp:1.4.2Keyless signing (recommended for CI):
Keyless signing uses OIDC identity from your CI provider (GitHub Actions, GitLab CI) instead of a long-lived key. No key management required.
# GitHub Actions step
- name: Sign image
uses: sigstore/cosign-installer@v3
- run: cosign sign --yes myregistry.io/myapp:${{ github.sha }}
env:
COSIGN_EXPERIMENTAL: 1The signature is stored in the registry alongside the image. Admission controllers (Kyverno, Connaisseur) can verify signatures at deploy time, rejecting unsigned images.
Garbage Collection and Retention Policies#
Registries accumulate images over time. Without cleanup, storage costs grow and old vulnerable images remain available.
ECR Lifecycle Policies#
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 20 tagged images",
"selection": {
"tagStatus": "tagged",
"countType": "imageCountMoreThan",
"countNumber": 20
},
"action": { "type": "expire" }
},
{
"rulePriority": 2,
"description": "Delete untagged images older than 7 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 7
},
"action": { "type": "expire" }
}
]
}Harbor Garbage Collection#
Harbor has a built-in garbage collector accessible via the admin UI or API. Configure a schedule and tag retention rules per project.
GitHub Container Registry#
GHCR does not have built-in lifecycle policies. Use a GitHub Action to prune old images:
- name: Delete old container images
uses: snok/container-retention-policy@v3
with:
image-names: myapp
cut-off: 30 days ago
keep-at-least: 10
account: my-org
token: ${{ secrets.GITHUB_TOKEN }}Pull-Through Caches#
A pull-through cache proxies requests to an upstream registry and caches images locally. This reduces external bandwidth, avoids Docker Hub rate limits, and speeds up pulls.
Harbor supports pull-through caching natively. Configure it as a proxy cache for Docker Hub:
- In Harbor, create a new registry endpoint pointing to
https://registry-1.docker.io. - Create a project of type “Proxy Cache” linked to that endpoint.
- Configure your Kubernetes nodes to pull from
harbor.internal/dockerhub-cache/library/nginx:1.27instead ofnginx:1.27.
For cloud environments, ECR has pull-through cache rules that proxy requests to Docker Hub, GitHub Container Registry, and other upstreams.
Cost Management#
Registry costs are driven by storage and data transfer. Practical measures:
- Lifecycle policies – Delete images you will never deploy again. Keep the last N tagged versions.
- Multi-stage builds – Smaller images mean less storage per version.
- Single registry per region – Cross-region pulls incur data transfer charges.
- Pull-through caches – Reduce redundant pulls from expensive upstream registries.
- Monitor image sizes – Use
crane manifest --platform linux/amd64 myregistry.io/myapp:1.4.2 | jq '.config.size'ordocker manifest inspectto track image sizes over time.
Key Takeaways#
- Tag images with semantic versions and git SHAs. Never deploy
:latestto production. - Attach
imagePullSecretsto ServiceAccounts rather than individual pod specs. - Sign images with Cosign (keyless in CI) and verify with admission controllers.
- Set up lifecycle policies on day one. Storage costs compound silently.
- Use pull-through caches to avoid rate limits and reduce pull latency.