AWS ECR Tutorial: Push, Pull and Manage Docker Images (CLI)

AWS ECR tutorial with the CLI: log in with get-login-password, create repos, push and pull images, add lifecycle policies, scan images and clean up old tags.

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.

Prerequisites

  • AWS CLI v2 configured with credentials (aws sts get-caller-identity should work).
  • Docker installed and running.
  • An IAM identity allowed to use ECR, for example the AmazonEC2ContainerRegistryPowerUser managed 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=true runs a basic vulnerability scan on every push.
  • IMMUTABLE stops anyone overwriting an existing tag such as 1.0.0. Leave it out (the default is MUTABLE) if you push a moving latest tag.

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 credentials on push or pull – you are not logged in to that registry, or the 12-hour token expired. Run the get-login-password | docker login command 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 to MUTABLE with aws ecr put-image-tag-mutability.
  • denied: User ... is not authorized to perform: ecr:InitiateLayerUpload – the IAM identity can pull but not push. Attach AmazonEC2ContainerRegistryPowerUser or an equivalent policy.
  • exec format error when the container starts – the image was built for the wrong CPU architecture. Rebuild with --platform linux/amd64 (or linux/arm64 for Graviton).
Last updated on Nov 10, 2026 12:49 +0300
comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy