Amazon ECR (Container Registry)
Before a container can run on ECS, EKS, or App Runner, its image has to live somewhere those services can pull it from. Amazon ECR (Elastic Container Registry) is AWS’s fully managed home for those images. Think of it as a private, secure version of Docker Hub that lives inside your AWS account, plugs straight into IAM (Identity and Access Management, AWS’s permissions system), and can scan your images for known security holes. You push images to it from your laptop or your CI/CD pipeline, and your compute services pull from it at deploy time.
What ECR actually stores
ECR stores container images (OCI-compliant images, the open standard Docker images follow) organized into repositories. A repository is one named bucket of related images for a single application — for example a repo called web-api might hold the tags latest, v1.4.2, and v1.4.3. Each push of a new build adds an image, identified by a tag (a human label like v1.4.2) and a digest (a content hash like sha256:9f86d0... that never changes for the same bytes).
ECR comes in two flavours:
| Type | Who can pull | When to use it |
|---|---|---|
| Private registry | Only IAM principals you grant access to | Your own apps and internal services. The default for almost everything. |
| Public registry (ECR Public / public.ecr.aws) | Anyone on the internet, no AWS account needed | Distributing open-source images or base images to the world. |
When to use ECR: any time you run containers on AWS. Keeping images in your own account means private network pulls (no internet egress charges from VPC), IAM-based access control, and no Docker Hub rate limits. When NOT to bother: if you only ever run a single container locally and never deploy it, you do not need a registry at all.
Creating a repository
You create one repository per application image.
Console steps:
- Open the Amazon ECR console and choose Repositories > Private.
- Click Create repository.
- Enter a name, for example
web-api. - Under Image scan settings, turn on Scan on push (free basic scanning).
- Optionally turn on Tag immutability so a tag like
v1.4.2can never be overwritten. - Click Create repository.
AWS CLI v2:
aws ecr create-repository \
--repository-name web-api \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability IMMUTABLE \
--region us-east-1
Output:
{
"repository": {
"repositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/web-api",
"registryId": "123456789012",
"repositoryName": "web-api",
"repositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/web-api",
"imageScanningConfiguration": { "scanOnPush": true },
"imageTagMutability": "IMMUTABLE"
}
}
That repositoryUri is the address you will tag and push images to.
Authenticating, building, and pushing
Docker does not know how to log in to AWS by itself. ECR gives you a short-lived password through the CLI, and you pipe it into docker login. Here is the full cycle from a clean checkout:
# 1. Get a 12-hour login token and hand it to Docker
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS \
--password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
# 2. Build your image locally
docker build -t web-api .
# 3. Tag it with the full ECR URI plus a version tag
docker tag web-api:latest \
123456789012.dkr.ecr.us-east-1.amazonaws.com/web-api:v1.4.2
# 4. Push it to ECR
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/web-api:v1.4.2
Output:
Login Succeeded
The push refers to repository [123456789012.dkr.ecr.us-east-1.amazonaws.com/web-api]
8b291f0a2c3d: Pushed
a1c2e3f4b5d6: Pushed
v1.4.2: digest: sha256:9f86d081884c7d659a2feaa0c55ad015... size: 1782
The IAM user or role doing this needs ecr:GetAuthorizationToken, ecr:BatchCheckLayerAvailability, ecr:PutImage, ecr:InitiateLayerUpload, ecr:UploadLayerPart, and ecr:CompleteLayerUpload. The managed policy AmazonEC2ContainerRegistryPowerUser covers all of these.
Gotcha — the token expires after 12 hours.
aws ecr get-login-passwordreturns a token that is only valid for 12 hours. That is fine on your laptop, but in a CI/CD pipeline (an automated build server) you must run theget-login-password | docker loginstep fresh on every run. Caching the token between builds will eventually fail withdenied: Your authorization token has expired. Always re-authenticate at the start of each pipeline job.
Image scanning
ECR can inspect images for known vulnerabilities (the CVE database — Common Vulnerabilities and Exposures) so you catch insecure base images before they ship.
- Basic scanning — free, uses the open-source Clair scanner, runs on push or on demand.
- Enhanced scanning — powered by Amazon Inspector, continuously rescans as new CVEs are published and covers OS packages plus language libraries. It is billed per image scan.
When to use enhanced scanning: production workloads where a vulnerability disclosed next week should re-flag images you pushed last month. When basic is enough: dev and test repos where a one-time scan-on-push is plenty.
aws ecr describe-image-scan-findings \
--repository-name web-api \
--image-id imageTag=v1.4.2 \
--region us-east-1
Lifecycle policies (control storage cost)
ECR storage is billed at about $0.10 per GB-month. That sounds tiny, but every CI build pushes a new image, and untagged layers from overwritten tags pile up silently. A busy repo can quietly grow to hundreds of gigabytes and cost real money if nothing ever cleans it up.
A lifecycle policy is a JSON rule that automatically expires old images. Save this as lifecycle.json:
{
"rules": [
{
"rulePriority": 1,
"description": "Expire untagged images after 14 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 14
},
"action": { "type": "expire" }
},
{
"rulePriority": 2,
"description": "Keep only the 20 most recent tagged images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["v"],
"countType": "imageCountMoreThan",
"countNumber": 20
},
"action": { "type": "expire" }
}
]
}
Apply it:
aws ecr put-lifecycle-policy \
--repository-name web-api \
--lifecycle-policy-text file://lifecycle.json \
--region us-east-1
Console steps: open the repository > Lifecycle Policy tab > Create rule > pick untagged or tagged, set the age or count, then Save.
Tip: Set a lifecycle policy the moment you create a repository, not after the storage bill surprises you. Expiring untagged images after two weeks and capping tagged images at 20-30 keeps almost any repo’s storage cost negligible.
Infrastructure as code
Defining the repo in Terraform keeps scanning and lifecycle settings under version control:
resource "aws_ecr_repository" "web_api" {
name = "web-api"
image_tag_mutability = "IMMUTABLE"
image_scanning_configuration {
scan_on_push = true
}
}
resource "aws_ecr_lifecycle_policy" "web_api" {
repository = aws_ecr_repository.web_api.name
policy = file("lifecycle.json")
}
Best Practices
- Turn on scan-on-push for every repository, and use enhanced scanning for production images.
- Attach a lifecycle policy at creation time to expire untagged and old images and keep storage cost near zero.
- In CI/CD, run
get-login-password | docker loginat the start of each job — the token lasts only 12 hours. - Use immutable tags in production so a release like
v1.4.2can never be silently overwritten, and pin deployments to image digests for true reproducibility. - Grant the least privilege needed: read-only pull policies for runtime roles, push permission only for build pipelines.
- Use a VPC interface endpoint for ECR so containers pull images over the private network instead of the public internet, avoiding NAT/egress charges.