AWS ECR

By Vishvesh Patel · DevOps EngineerUpdated September 11, 2026

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:

PolicyAttached toAnswers
IAM policyUser or role"Is this principal allowed to call ECR?"
Repository policyThe 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

ErrorCause
no basic auth credentialsNever logged in, or the 12-hour token expired
denied: requested access to the resource is deniedImage wasn't tagged with the full registry URI, or the repository doesn't exist
name unknown: The repository ... does not existECR does not auto-create repositories on push
401 Unauthorized mid-pipelineToken expired during a long build (re-authenticate per job)
tag invalid: ... cannot be overwrittenWorking as intended: the repository has immutable tags

Quick reference

TaskCommand
Authenticateaws ecr get-login-password --region <r> | docker login --username AWS --password-stdin <registry>
Create repositoryaws ecr create-repository --repository-name <name>
List repositoriesaws ecr describe-repositories
List image tagsaws ecr list-images --repository-name <name>
Scan resultsaws ecr describe-image-scan-findings --repository-name <name> --image-id imageTag=<tag>
Delete an imageaws ecr batch-delete-image --repository-name <name> --image-ids imageTag=<tag>
Repository sizeaws 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 IMMUTABLE tags 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:GetAuthorizationToken on *: 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

Exercise

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?

Exercise

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.

Continue Learning

Explore Related Topics

When it breaks

Related Resources