AWS ECR
Push, pull and secure container images in Amazon ECR. Authentication, tagging, lifecycle policies, image scanning, and the errors you will hit first.
Amazon ECR
Amazon ECR is AWS's private container registry. Its real advantage over Docker Hub isn't features. It's that authentication runs through IAM, so the same roles that govern the rest of your infrastructure govern who can push and pull images, and pulls from inside AWS don't cross the internet or hit rate limits.
You'll spend most of your ECR time on four things: authenticating, tagging correctly, stopping storage costs from creeping, and reading scan results.
Repositories
An ECR repository holds all the tags for one image. It's regional, and the name is part of the image URI:
<account-id>.dkr.ecr.<region>.amazonaws.com/<repository>:<tag>
111122223333.dkr.ecr.us-east-1.amazonaws.com/payments-api:1.4.2
aws ecr create-repository \
--repository-name payments-api \
--region us-east-1 \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability IMMUTABLE \
--encryption-configuration encryptionType=KMS
IMMUTABLE is the setting worth arguing for. With mutable tags, someone can push a different image over 1.4.2 and every future pull silently gets different bytes, which makes rollbacks unreliable and incident timelines impossible to reconstruct. Immutability makes that push fail instead.
Authenticating
ECR doesn't use a static username and password. You exchange IAM credentials for a token that lasts 12 hours:
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-east-1.amazonaws.com
Login Succeeded
The username is literally the string AWS, that's not a placeholder. And the registry argument is the host only, with no repository path and no https://.
Two mistakes account for most login failures. Passing the region of one registry while logging into another gives you a token that's valid but for the wrong endpoint. And a token acquired 13 hours ago in a long-running CI agent will fail with a 401 that reads like a permissions problem but isn't, re-authenticate on every job rather than caching the login.
Pushing an image
# 1. Build
docker build -t payments-api:1.4.2 .
# 2. Tag with the full registry URI. This is the step people forget
docker tag payments-api:1.4.2 \
111122223333.dkr.ecr.us-east-1.amazonaws.com/payments-api:1.4.2
# 3. Push
docker push 111122223333.dkr.ecr.us-east-1.amazonaws.com/payments-api:1.4.2
If you skip step 2, Docker tries to push to Docker Hub and fails with a denied error that says nothing about ECR.
Tag with something traceable. A commit SHA is the most useful default, because it maps a running container back to exact source:
TAG=$(git rev-parse --short HEAD)
REGISTRY=111122223333.dkr.ecr.us-east-1.amazonaws.com
docker build -t $REGISTRY/payments-api:$TAG .
docker push $REGISTRY/payments-api:$TAG
Never deploy :latest. It's mutable by definition, so two deploys of "latest" can run different code, and a rollback has nothing to roll back to.
Lifecycle policies
ECR charges $0.10 per GB per month, and image layers accumulate faster than anyone expects. A CI pipeline pushing on every commit will produce thousands of images a year. Lifecycle policies expire them automatically.
{
"rules": [
{
"rulePriority": 1,
"description": "Keep the last 10 release images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["v"],
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": { "type": "expire" }
},
{
"rulePriority": 2,
"description": "Expire untagged images after 7 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 7
},
"action": { "type": "expire" }
}
]
}
aws ecr put-lifecycle-policy \
--repository-name payments-api \
--lifecycle-policy-text file://lifecycle.json
# Always dry-run first. Deletion is permanent
aws ecr start-lifecycle-policy-preview \
--repository-name payments-api \
--lifecycle-policy-text file://lifecycle.json
Rules evaluate in priority order and an image is expired by the first matching rule, so put your "keep" rules at lower priority numbers than your broad cleanup rules. Run the preview before applying. There is no undelete.
Image scanning
ECR scans images for known CVEs. Basic scanning is free and runs on push; enhanced scanning uses Amazon Inspector, costs more, and continuously rescans as new vulnerabilities are published, which matters, because an image that was clean when built is not clean forever.
aws ecr describe-image-scan-findings \
--repository-name payments-api \
--image-id imageTag=1.4.2 \
--query 'imageScanFindings.findingSeverityCounts'
{
"HIGH": 2,
"MEDIUM": 11,
"LOW": 34
}
Gate your pipeline on it, but gate on severity rather than any finding at all. A policy of "zero vulnerabilities" gets switched off within a month:
CRITICAL=$(aws ecr describe-image-scan-findings \
--repository-name payments-api --image-id imageTag=$TAG \
--query 'imageScanFindings.findingSeverityCounts.CRITICAL' --output text)
if [ "$CRITICAL" != "None" ] && [ "$CRITICAL" -gt 0 ]; then
echo "Blocking deploy: $CRITICAL critical vulnerabilities"
exit 1
fi
Most findings come from the base image, so switching from a full distribution base to a slim or distroless one usually removes more CVEs than any amount of dependency patching. This is one of the clearest payoffs of a good multi-stage build.
Permissions
Two policy types apply, and both must allow the action:
| Policy | Attached to | Answers |
|---|---|---|
| IAM policy | User or role | "Is this principal allowed to call ECR?" |
| Repository policy | The repository | "Is this principal allowed on this repository?" |
A minimal pull-only IAM policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
],
"Resource": "arn:aws:ecr:us-east-1:111122223333:repository/payments-api"
},
{
"Effect": "Allow",
"Action": "ecr:GetAuthorizationToken",
"Resource": "*"
}
]
}
ecr:GetAuthorizationToken must be granted on *. It's an account-level call, not a repository-level one. Scoping it to a repository ARN silently breaks authentication, and it's a common cause of "it works for me but not in CI".
Repository policies are how you share images across accounts. A shared build account pushes once; every environment account pulls from it, so the artifact that reaches production is the exact one that passed tests.
Pull through cache
Pull through cache mirrors upstream registries (Docker Hub, ECR Public, quay.io, GitHub) into your own ECR on first pull. It removes Docker Hub rate limits from your builds and keeps working if the upstream is down:
aws ecr create-pull-through-cache-rule \
--ecr-repository-prefix docker-hub \
--upstream-registry-url registry-1.docker.io \
--credential-arn arn:aws:secretsmanager:us-east-1:111122223333:secret:ecr-pullthroughcache/dockerhub
Then pull 111122223333.dkr.ecr.us-east-1.amazonaws.com/docker-hub/library/nginx:1.27 instead of nginx:1.27.
Common errors
| Error | Cause |
|---|---|
no basic auth credentials | Never logged in, or the 12-hour token expired |
denied: requested access to the resource is denied | Image wasn't tagged with the full registry URI, or the repository doesn't exist |
name unknown: The repository ... does not exist | ECR does not auto-create repositories on push |
401 Unauthorized mid-pipeline | Token expired during a long build (re-authenticate per job) |
tag invalid: ... cannot be overwritten | Working as intended: the repository has immutable tags |
Quick reference
| Task | Command |
|---|---|
| Authenticate | aws ecr get-login-password --region <r> | docker login --username AWS --password-stdin <registry> |
| Create repository | aws ecr create-repository --repository-name <name> |
| List repositories | aws ecr describe-repositories |
| List image tags | aws ecr list-images --repository-name <name> |
| Scan results | aws ecr describe-image-scan-findings --repository-name <name> --image-id imageTag=<tag> |
| Delete an image | aws ecr batch-delete-image --repository-name <name> --image-ids imageTag=<tag> |
| Repository size | aws ecr describe-images --repository-name <name> --query 'sum(imageDetails[].imageSizeInBytes)' |
Key takeaways
- Authentication is a 12-hour IAM-derived token, not a stored password. Re-authenticate every CI job rather than caching it.
- Tag with the full registry URI before pushing, or Docker will try Docker Hub and give you a misleading error.
- Set
IMMUTABLEtags and deploy commit SHAs, never:latest. Mutable tags make rollbacks and incident forensics unreliable. - Lifecycle policies are not optional at $0.10/GB/month. Preview them before applying; expiry is permanent.
- Grant
ecr:GetAuthorizationTokenon*: scoping it to a repository ARN silently breaks login. - Most scan findings come from the base image. A slimmer base fixes more than dependency patching does.
Check your understanding
Broken CI authentication
A CI role has ecr:GetAuthorizationToken scoped to a specific repository ARN, plus full push permissions on that repository. The docker login step fails. Why?
Lifecycle rule ordering
A repository has rule priority 1 expiring any image older than 30 days, and rule priority 2 keeping the 10 most recent tagged releases. Release images are disappearing. What is wrong?
Frequently Asked Questions
Why does docker push to ECR fail with "denied: requested access to the resource is denied"?
Nearly always one of two things. Either the image wasn't tagged with the full registry URI, docker tag myapp:1.0 111122223333.dkr.ecr.us-east-1.amazonaws.com/myapp:1.0, in which case Docker is trying to push to Docker Hub, or the repository doesn't exist yet. ECR never creates repositories implicitly on push, unlike Docker Hub. Create it first with aws ecr create-repository, and confirm your docker login used the same account and region as the tag.
How long does ECR authentication last?
Twelve hours. aws ecr get-login-password exchanges your IAM credentials for a token with that lifetime. For CI this is usually invisible because each job authenticates fresh, but long-running agents that cache a login will start failing with 401 errors partway through the day. Authenticate at the start of every job rather than treating login as one-time setup.
Should I use ECR or Docker Hub?
For anything running on AWS, ECR. Authentication uses IAM instead of a separate credential set, pulls from within AWS stay on the AWS network and avoid Docker Hub's rate limits, and images sit inside your account's security and compliance boundary. Docker Hub still makes sense for publicly distributed images where you want easy anonymous pulls. Pull through cache gives you a middle path: mirror the upstream images you depend on into ECR automatically.
How do I stop ECR storage costs from growing?
Set a lifecycle policy on every repository at creation time. Storage is $0.10 per GB per month and a pipeline that pushes on each commit accumulates quickly. A practical pair of rules: expire untagged images after 7 days, and keep only the most recent 10 to 20 tagged releases. Always run start-lifecycle-policy-preview first, because expiry is permanent and a mis-ordered rule can delete images you meant to keep.
Can another AWS account pull images from my ECR repository?
Yes, through a repository policy that names the other account as a principal and allows the pull actions, ecr:GetDownloadUrlForLayer, ecr:BatchGetImage, and ecr:BatchCheckLayerAvailability. The pulling account also needs ecr:GetAuthorizationToken in its own IAM policies. This is the standard shared-build-account pattern: one account builds and pushes, environment accounts pull, and the artifact promoted to production is byte-identical to the one tested.
What is the difference between basic and enhanced image scanning?
Basic scanning is free, runs on push, and checks operating system packages against a CVE database. Enhanced scanning is powered by Amazon Inspector, costs per image, and additionally inspects application-language dependencies while continuously rescanning stored images as new vulnerabilities are disclosed. That continuous part is the real difference. An image that scanned clean six months ago may have several critical findings today, and only enhanced scanning tells you.