When you need this
A pipeline fails with no basic auth credentials, a repository has grown to thousands of untagged images, or another account needs to pull your image. Below are the commands for each case. Set these once per shell:
ACCOUNT=111122223333
REGION=eu-west-1
REGISTRY=$ACCOUNT.dkr.ecr.$REGION.amazonaws.com
Log in, push and pull
# Log in (the token lasts 12 hours)
aws ecr get-login-password --region $REGION | \
docker login --username AWS --password-stdin $REGISTRY
# Create a repository with scan on push and immutable tags
aws ecr create-repository --repository-name payments/api --region $REGION \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability IMMUTABLE
# Tag and push
docker tag payments-api:1.4.2 $REGISTRY/payments/api:1.4.2
docker push $REGISTRY/payments/api:1.4.2
# Pull
docker pull $REGISTRY/payments/api:1.4.2
To build one image for both x86 and Graviton:
docker buildx build --platform linux/amd64,linux/arm64 \
-t $REGISTRY/payments/api:1.4.2 --push .
Inventory
# Repositories and their settings
aws ecr describe-repositories \
--query "repositories[].{name:repositoryName,mutability:imageTagMutability,scan:imageScanningConfiguration.scanOnPush}" \
--output table
# Newest 10 images in a repository
aws ecr describe-images --repository-name payments/api \
--query "reverse(sort_by(imageDetails,&imagePushedAt))[:10].{tags:imageTags[0],pushed:imagePushedAt,sizeMB:imageSizeInBytes}" \
--output table
# Does a tag exist?
aws ecr describe-images --repository-name payments/api --image-ids imageTag=1.4.2
# Count untagged images
aws ecr list-images --repository-name payments/api --filter tagStatus=UNTAGGED \
--query "length(imageIds)"
# Digest behind a tag (pin deployments to this)
aws ecr describe-images --repository-name payments/api --image-ids imageTag=1.4.2 \
--query "imageDetails[0].imageDigest" --output text
Common errors
| Error | Cause | Fix |
|---|---|---|
no basic auth credentials | Not logged in, token expired after 12 hours, or logged in to another Region's registry | Run get-login-password again for the registry's Region |
denied: ... not authorized to perform: ecr:InitiateLayerUpload | Pushing principal lacks push actions | Grant ecr:InitiateLayerUpload, ecr:UploadLayerPart, ecr:CompleteLayerUpload, ecr:PutImage, ecr:BatchCheckLayerAvailability |
name unknown: The repository with name '...' does not exist | Repository not created, typo, or wrong account/Region in the URI | aws ecr create-repository, check the registry URI |
tag invalid: The image tag '...' already exists ... immutable | Repository has IMMUTABLE tags | Push a new tag. Don't reuse version tags |
manifest unknown / not found on pull | Tag never pushed, or a lifecycle policy expired it | describe-images --image-ids imageTag=..., check the lifecycle policy |
exec format error when the container starts | Image architecture doesn't match the node (amd64 vs arm64) | Build multi-arch with buildx --platform |
403 Forbidden from a cross-account pull | Repository policy missing, or the puller's IAM lacks ecr:GetAuthorizationToken | See the cross-account section below |
| Pull works locally, fails from a private subnet | No NAT gateway and no VPC endpoints | Add ecr.api, ecr.dkr interface endpoints and an S3 gateway endpoint |
To see which principal is calling, run aws sts get-caller-identity with the same credentials the pipeline uses. For ECS failures, read the task's stopped reason first. The CannotPullContainerError text usually names the cause.
Clean up with lifecycle policies
Untagged layers from every CI build add up quickly in storage cost. A lifecycle policy expires them automatically.
lifecycle.json:
{
"rules": [
{
"rulePriority": 1,
"description": "Expire untagged images after 7 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 7
},
"action": { "type": "expire" }
},
{
"rulePriority": 2,
"description": "Keep the last 30 build images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["build-"],
"countType": "imageCountMoreThan",
"countNumber": 30
},
"action": { "type": "expire" }
}
]
}
Always preview before you apply:
aws ecr start-lifecycle-policy-preview --repository-name payments/api \
--lifecycle-policy-text file://lifecycle.json
aws ecr get-lifecycle-policy-preview --repository-name payments/api \
--query "{status:status,expiring:previewResults[].imageTags}"
aws ecr put-lifecycle-policy --repository-name payments/api \
--lifecycle-policy-text file://lifecycle.json
Caution
Lifecycle expiry is permanent. A rule like "keep the last 10 images" with
tagStatus: anycan delete the image that production is running or rolls back to. Exclude release tags (for example, usetagPrefixListonly onbuild-tags), and check the preview for any tag currently deployed.
Manual one-off delete of untagged images (up to 100 IDs per call):
IDS=$(aws ecr list-images --repository-name payments/api --filter tagStatus=UNTAGGED \
--query "imageIds[:100]" --output json)
aws ecr batch-delete-image --repository-name payments/api --image-ids "$IDS"
Cross-account pulls
The pulling account needs two things. First, a repository policy in the owning account that allows it:
cat > repo-policy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowPullFromProd",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::444455556666:root" },
"Action": [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchCheckLayerAvailability"
]
}]
}
EOF
aws ecr set-repository-policy --repository-name payments/api --policy-text file://repo-policy.json
Second, IAM permissions in the pulling account: ecr:GetAuthorizationToken on "Resource": "*" plus the three pull actions above on the repository ARN. The ECS task execution role or EKS node role needs these, not the application role.
Tip
To share with every account in your organization, use a
Conditiononaws:PrincipalOrgIDwith"Principal": "*"instead of listing account IDs.
If the repository uses a customer managed KMS key, the pulling principal also needs kms:Decrypt on that key, granted in the key policy.
Scanning and replication
# Scan results for one image (basic scanning)
aws ecr describe-image-scan-findings --repository-name payments/api --image-id imageTag=1.4.2 \
--query "imageScanFindings.findingSeverityCounts"
# Registry-level scanning configuration (basic or enhanced/Inspector)
aws ecr get-registry-scanning-configuration
# Replication rules (cross-Region or cross-account)
aws ecr describe-registry --query replicationConfiguration
Replication copies new pushes only. Images that existed before you created the rule are not copied, so push them again if the destination needs them.
Prevent it next time
- Put a lifecycle policy on every repository when you create it, using IaC or repository creation templates.
- Use immutable tags and deploy by digest so a re-push can't change what production runs.
- Add
ecr.api,ecr.dkrand S3 endpoints to every private VPC that pulls images. - Alarm on failed pulls in your orchestrator (ECS stopped reasons, Kubernetes
ErrImagePullevents) rather than waiting for a deploy to fail.