Why a private registry deserves attention
Many teams start by pushing images to a personal Docker Hub account and pulling them onto servers. It works until a rate limit blocks a production deploy, an engineer leaves with the only credentials, or someone discovers that an internal image with embedded configuration was public. A private registry in your own cloud account solves these problems, but only if access, retention and networking are designed rather than left at defaults.
This is part of our Docker containerisation service, and it usually sits beside work on Kubernetes on EKS or other clusters.
What good looks like
- One repository per deployable service, with a consistent naming convention.
- CI is the only identity that pushes to production repositories.
- Clusters pull using cloud identity, not static passwords.
- Tags are immutable, and deployments reference a version or digest.
- Images are scanned on push, and old images expire automatically.
- Everything is defined as code and reviewable.
Choosing the registry
| Cloud | Registry | Notes |
|---|---|---|
| AWS | Amazon ECR | Tight IAM integration, repository policies, lifecycle policies, pull-through cache |
| Azure | Azure Container Registry | Integrates with Microsoft Entra ID and AKS, tiers differ in features |
| Google Cloud | Artifact Registry | Replaces Container Registry (GCR), supports multiple package formats |
| Multi-cloud or self-hosted | Harbor | Open source, more control, more to operate |
If you are on one cloud, use its registry. The integrations with IAM, private networking and the managed Kubernetes service save real effort.
An ECR example
This Terraform sketch creates a repository with immutable tags, scan on push, encryption and a lifecycle rule:
resource "aws_ecr_repository" "api" {
name = "platform/api"
image_tag_mutability = "IMMUTABLE"
image_scanning_configuration {
scan_on_push = true
}
encryption_configuration {
encryption_type = "KMS"
}
}
resource "aws_ecr_lifecycle_policy" "api" {
repository = aws_ecr_repository.api.name
policy = jsonencode({
rules = [{
rulePriority = 1
description = "Expire untagged images after 14 days"
selection = {
tagStatus = "untagged"
countType = "sinceImagePushed"
countUnit = "days"
countNumber = 14
}
action = { type = "expire" }
}]
})
}
For release tags we add a second rule that keeps the last N tagged images, sized to your rollback window. The exact tagPrefixList or tagPatternList depends on how you name versions, so we set it after reviewing your release process.
Access: who pushes and who pulls
Push. Your pipeline authenticates through OIDC federation to a role allowed to push to specific repositories only. No long-lived access keys sit in CI variables. Developers can push to a sandbox repository but not to production ones.
Pull. Clusters pull through node roles or workload identity. Production nodes get read-only access to production repositories. Cross-account pulls are granted explicitly with repository policies.
Network. For stricter environments, we add private endpoints so image pulls never traverse the public internet, which also reduces NAT gateway data charges in some AWS designs.
Repository layout and naming
Layout decisions are cheap on day one and expensive later, because every manifest, pipeline and policy refers to the names. We usually recommend one repository per deployable service, grouped by team or product with a path prefix such as platform/api, and a separate prefix for mirrored third-party images. Environments should not get their own repositories: you promote the same image by digest or tag, and the environments differ only in configuration. Keeping a clear boundary between “built by us”, “mirrored from upstream” and “scratch or sandbox” also makes access policy simpler, because each group has different rules about who may push.
Day-two operations
A registry needs a little ongoing care. Someone should review who can push, check that lifecycle rules still match the release cadence, and watch for repositories that nobody owns. Scan findings on stored images should flow into a backlog, since an image that was clean at push time can acquire a new advisory later, as covered in scanning in CI/CD. We also test the unhappy paths once: a pull with an expired credential, a pull from a node without access, and a push to a repository the pipeline should not reach. Each should fail clearly, with an error message your on-call engineer can act on.
Cross-account and multi-environment access
Many organisations separate production into its own cloud account. The registry then lives in a shared account or in the production account, and other accounts need explicit permission to pull. We define this with repository policies and roles in code, test the pull from each environment, and avoid broad organisation-wide grants. The same discipline applies on Azure and Google Cloud, with role assignments instead of policies. When this is set up as Terraform, an access change becomes a reviewed pull request rather than a console click that nobody remembers.
Decisions and trade-offs
Pull-through caches for public images (ECR supports this for several upstream registries) remove Docker Hub rate limit failures and give you a place to scan third-party images. They add configuration, but for any team with frequent node churn it is worth it.
Replication. Multi-region ECR replication or ACR geo-replication shortens pulls and improves resilience for multi-region deployments. It also increases storage cost, so we enable it only where there is a real need.
Harbor or a cloud registry? Harbor gives you features like project-level policies and proxy caching in one place, but you run it. Most startups should not.
Migrating from GCR. Container Registry has been superseded by Artifact Registry. We map repositories, copy images, update references in manifests and pipelines, and switch over in stages.
Common failure modes
- Lifecycle rules deleting images that are still deployed, causing pull failures when a pod reschedules. We protect tags that production references.
- Mutable
latesttags deployed to production. We forbid it by policy. - Overly broad access, such as
ecr:*on all repositories. We scope by repository. - Forgotten cross-region pulls, causing slow deploys and transfer cost.
Registries also anchor your supply chain: scanning is described in Trivy scanning in CI/CD, and signatures are stored alongside images as covered in image signing with Cosign.
Delivery and handover
We deliver the registry as Terraform in your repository, CI push configuration, cluster pull configuration and a short access runbook. Costs and ownership of each repository are documented. If you want help planning the layout, book a consultation.
How to tell it is working
Deploys never fail on image pull rate limits or expired credentials, no human credentials can push to production, storage cost stays flat as releases accumulate, and every running image can be traced back to a build in your CI.