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.

# 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-a1b2c3d

In 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=production

Reference in pod spec:

spec:
  imagePullSecrets:
    - name: regcred
  containers:
    - name: app
      image: myregistry.io/myapp:1.4.2

Attach 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.2

Verify a signature before deploying:

cosign verify --key cosign.pub myregistry.io/myapp:1.4.2

Keyless 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: 1

The 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:

  1. In Harbor, create a new registry endpoint pointing to https://registry-1.docker.io.
  2. Create a project of type “Proxy Cache” linked to that endpoint.
  3. Configure your Kubernetes nodes to pull from harbor.internal/dockerhub-cache/library/nginx:1.27 instead of nginx: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' or docker manifest inspect to track image sizes over time.

Key Takeaways#

  • Tag images with semantic versions and git SHAs. Never deploy :latest to production.
  • Attach imagePullSecrets to 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.