Amazon Elastic Container Registry (Amazon ECR) is a fully managed container registry that makes it easy to store, manage, share, and deploy your container images and artifacts anywhere. With AWS ECR, you won’t need to host your own Docker registry.
In this guide we will use the AWS CLI v2 and Docker to log in to ECR, create a repository, push and pull an image, keep the repository tidy with a lifecycle policy, scan images for vulnerabilities, let other accounts and EKS pull from it, and finally clean up.
Related content
- Using AWS S3 from the terminal with AWS CLI v2 (covers installing and configuring the CLI)
- How to Set up self-hosted Private Docker Registry
- Create and Manage an Amazon EKS Cluster With eksctl
- How to Install and Use Docker Engine on Ubuntu 26.04 LTS
Prerequisites
- AWS CLI v2 configured with credentials (
aws sts get-caller-identityshould work). - Docker installed and running.
- An IAM identity allowed to use ECR, for example the
AmazonEC2ContainerRegistryPowerUsermanaged policy.
Set a few variables so the commands below can be pasted as-is. 123456789012 is a placeholder; the sts call fills in your real account ID:
export AWS_REGION=us-east-1
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export REGISTRY=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
echo $REGISTRY
123456789012.dkr.ecr.us-east-1.amazonaws.com
Every account has one private registry per region at that address. Repositories live inside it.
Log in to ECR
Older versions of this guide used aws ecr get-login. That command only existed in AWS CLI v1 and is gone in v2:
aws: [ERROR]: An error occurred (ParamValidation): argument operation: Found invalid choice 'get-login'
Use get-login-password and pipe it into docker login:
aws ecr get-login-password --region $AWS_REGION \
| docker login --username AWS --password-stdin $REGISTRY
Login Succeeded
The username is always AWS. The token is valid for 12 hours, so CI jobs should log in at the start of every run. If you push often from a workstation, the amazon-ecr-credential-helper fetches tokens automatically.
Create a repository
aws ecr create-repository \
--repository-name citizix/blog-app \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability IMMUTABLE \
--region $AWS_REGION
- Names can contain slashes, which is handy for grouping (
team/app). scanOnPush=trueruns a basic vulnerability scan on every push.IMMUTABLEstops anyone overwriting an existing tag such as1.0.0. Leave it out (the default isMUTABLE) if you push a movinglatesttag.
The response includes the repositoryUri you push to. To get it later:
aws ecr describe-repositories --repository-names citizix/blog-app \
--query 'repositories[0].repositoryUri' --output text
123456789012.dkr.ecr.us-east-1.amazonaws.com/citizix/blog-app
List all repositories in the region:
aws ecr describe-repositories --query 'repositories[].repositoryName' --output table
Tag and push an image
Build (or pick) an image, tag it with the full repository URI, then push:
docker build -t blog-app:1.0.0 .
docker tag blog-app:1.0.0 $REGISTRY/citizix/blog-app:1.0.0
docker push $REGISTRY/citizix/blog-app:1.0.0
If you build on an Apple Silicon Mac and run on x86 servers, build for the right platform, otherwise the container fails with exec format error:
docker buildx build --platform linux/amd64 -t $REGISTRY/citizix/blog-app:1.0.0 --push .
List and pull images
aws ecr list-images --repository-name citizix/blog-app
# tagStatus = TAGGED | UNTAGGED | ANY
aws ecr list-images --repository-name citizix/blog-app --filter tagStatus=UNTAGGED
describe-images adds push time and size. Sorted oldest first:
aws ecr describe-images --repository-name citizix/blog-app \
--query 'sort_by(imageDetails,&imagePushedAt)[*].[imageTags[0],imagePushedAt,imageSizeInBytes]' \
--output table
Pull it anywhere that is logged in to the registry:
docker pull $REGISTRY/citizix/blog-app:1.0.0
Clean up old images with a lifecycle policy
Without a policy a busy repository grows forever (and you pay for the storage). A lifecycle policy expires images automatically. Save this as lifecycle-policy.json:
{
"rules": [
{
"rulePriority": 1,
"description": "Expire untagged images older than 7 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 7
},
"action": { "type": "expire" }
},
{
"rulePriority": 2,
"description": "Keep the 30 most recent release images",
"selection": {
"tagStatus": "tagged",
"tagPatternList": ["v*"],
"countType": "imageCountMoreThan",
"countNumber": 30
},
"action": { "type": "expire" }
},
{
"rulePriority": 10,
"description": "Cap everything else at 100 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 100
},
"action": { "type": "expire" }
}
]
}
Rules are evaluated from the lowest rulePriority up. A rule with tagStatus: any must have the highest priority number, and tagged rules need a tagPatternList (wildcards) or tagPrefixList.
Preview what the policy would delete before applying it:
aws ecr start-lifecycle-policy-preview --repository-name citizix/blog-app \
--lifecycle-policy-text file://lifecycle-policy.json
aws ecr wait lifecycle-policy-preview-complete --repository-name citizix/blog-app
aws ecr get-lifecycle-policy-preview --repository-name citizix/blog-app
Then apply it:
aws ecr put-lifecycle-policy --repository-name citizix/blog-app \
--lifecycle-policy-text file://lifecycle-policy.json
Expiry is not instant; ECR applies the policy within about 24 hours.
Scan images for vulnerabilities
With scanOnPush=true every pushed image is scanned. To scan an existing image on demand and read the result:
aws ecr start-image-scan --repository-name citizix/blog-app --image-id imageTag=1.0.0
aws ecr wait image-scan-complete --repository-name citizix/blog-app --image-id imageTag=1.0.0
aws ecr describe-image-scan-findings --repository-name citizix/blog-app \
--image-id imageTag=1.0.0 --query 'imageScanFindings.findingSeverityCounts'
The last command returns a count per severity (CRITICAL, HIGH, MEDIUM…). Add --query 'imageScanFindings.findings[].[name,severity]' to list the CVEs.
Basic scanning covers OS packages. Enhanced scanning uses Amazon Inspector, also covers language packages (npm, pip, Go modules…) and rescans continuously when new CVEs are published. It is a registry-wide setting and is billed by Inspector:
aws ecr put-registry-scanning-configuration --scan-type ENHANCED \
--rules '[{"scanFrequency":"SCAN_ON_PUSH","repositoryFilters":[{"filter":"*","filterType":"WILDCARD"}]}]'
Pull from another account or from EKS
Same account (EC2, ECS, EKS nodes): no repository policy is needed. Give the instance or node role the AmazonEC2ContainerRegistryReadOnly managed policy. EKS managed node groups created by eksctl or the console already have it, so pods can use 123456789012.dkr.ecr.us-east-1.amazonaws.com/citizix/blog-app:1.0.0 as their image with no imagePullSecrets.
Another account: add a repository policy that lets the other account pull. Save as repo-policy.json, with 210987654321 as the consuming account:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPullFromOtherAccount",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::210987654321:root" },
"Action": [
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer"
]
}
]
}
aws ecr set-repository-policy --repository-name citizix/blog-app \
--policy-text file://repo-policy.json
The consuming account still needs its own IAM permission for those actions plus ecr:GetAuthorizationToken, and it logs in to your registry address (123456789012.dkr.ecr...), not its own.
Delete images and repositories
Delete a single tag:
aws ecr batch-delete-image --repository-name citizix/blog-app --image-ids imageTag=1.0.0
Delete all untagged images in a repository:
aws ecr list-images --repository-name citizix/blog-app \
--filter tagStatus=UNTAGGED --query 'imageIds[*]' --output json > untagged.json
aws ecr batch-delete-image --repository-name citizix/blog-app --image-ids file://untagged.json
batch-delete-image accepts at most 100 image IDs per call, so for large repositories use --query 'imageIds[0:100]' and loop, or let a lifecycle policy do it.
The same for every repository in the region:
for repo in $(aws ecr describe-repositories --query 'repositories[].repositoryName' --output text); do
ids=$(aws ecr list-images --repository-name "$repo" --filter tagStatus=UNTAGGED \
--query 'imageIds[0:100]' --output json)
[ "$ids" = "[]" ] && continue
aws ecr batch-delete-image --repository-name "$repo" --image-ids "$ids"
done
Delete a repository. --force also deletes the images in it; without it the call fails if the repository is not empty:
aws ecr delete-repository --repository-name citizix/blog-app --force
Troubleshooting
no basic auth credentialson push or pull – you are not logged in to that registry, or the 12-hour token expired. Run theget-login-password | docker logincommand again, for the right region and account.name unknown: The repository with name '...' does not exist– ECR does not create repositories on push. Create it first, and check the region in the URI.tag invalid: The image tag '1.0.0' already exists ... cannot be overwritten because the repository is immutable– use a new tag, or switch the repository toMUTABLEwithaws ecr put-image-tag-mutability.denied: User ... is not authorized to perform: ecr:InitiateLayerUpload– the IAM identity can pull but not push. AttachAmazonEC2ContainerRegistryPowerUseror an equivalent policy.exec format errorwhen the container starts – the image was built for the wrong CPU architecture. Rebuild with--platform linux/amd64(orlinux/arm64for Graviton).