Skip to content
AWS aws containers 5 min read

Amazon ECR (Container Registry)

Before a container can run on ECS, EKS, or App Runner, its image has to live somewhere those services can pull it from. Amazon ECR (Elastic Container Registry) is AWS’s fully managed home for those images. Think of it as a private, secure version of Docker Hub that lives inside your AWS account, plugs straight into IAM (Identity and Access Management, AWS’s permissions system), and can scan your images for known security holes. You push images to it from your laptop or your CI/CD pipeline, and your compute services pull from it at deploy time.

What ECR actually stores

ECR stores container images (OCI-compliant images, the open standard Docker images follow) organized into repositories. A repository is one named bucket of related images for a single application — for example a repo called web-api might hold the tags latest, v1.4.2, and v1.4.3. Each push of a new build adds an image, identified by a tag (a human label like v1.4.2) and a digest (a content hash like sha256:9f86d0... that never changes for the same bytes).

ECR comes in two flavours:

TypeWho can pullWhen to use it
Private registryOnly IAM principals you grant access toYour own apps and internal services. The default for almost everything.
Public registry (ECR Public / public.ecr.aws)Anyone on the internet, no AWS account neededDistributing open-source images or base images to the world.

When to use ECR: any time you run containers on AWS. Keeping images in your own account means private network pulls (no internet egress charges from VPC), IAM-based access control, and no Docker Hub rate limits. When NOT to bother: if you only ever run a single container locally and never deploy it, you do not need a registry at all.

Creating a repository

You create one repository per application image.

Console steps:

  1. Open the Amazon ECR console and choose Repositories > Private.
  2. Click Create repository.
  3. Enter a name, for example web-api.
  4. Under Image scan settings, turn on Scan on push (free basic scanning).
  5. Optionally turn on Tag immutability so a tag like v1.4.2 can never be overwritten.
  6. Click Create repository.

AWS CLI v2:

aws ecr create-repository \
  --repository-name web-api \
  --image-scanning-configuration scanOnPush=true \
  --image-tag-mutability IMMUTABLE \
  --region us-east-1

Output:

{
    "repository": {
        "repositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/web-api",
        "registryId": "123456789012",
        "repositoryName": "web-api",
        "repositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/web-api",
        "imageScanningConfiguration": { "scanOnPush": true },
        "imageTagMutability": "IMMUTABLE"
    }
}

That repositoryUri is the address you will tag and push images to.

Authenticating, building, and pushing

Docker does not know how to log in to AWS by itself. ECR gives you a short-lived password through the CLI, and you pipe it into docker login. Here is the full cycle from a clean checkout:

# 1. Get a 12-hour login token and hand it to Docker
aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS \
      --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com

# 2. Build your image locally
docker build -t web-api .

# 3. Tag it with the full ECR URI plus a version tag
docker tag web-api:latest \
  123456789012.dkr.ecr.us-east-1.amazonaws.com/web-api:v1.4.2

# 4. Push it to ECR
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/web-api:v1.4.2

Output:

Login Succeeded
The push refers to repository [123456789012.dkr.ecr.us-east-1.amazonaws.com/web-api]
8b291f0a2c3d: Pushed
a1c2e3f4b5d6: Pushed
v1.4.2: digest: sha256:9f86d081884c7d659a2feaa0c55ad015... size: 1782

The IAM user or role doing this needs ecr:GetAuthorizationToken, ecr:BatchCheckLayerAvailability, ecr:PutImage, ecr:InitiateLayerUpload, ecr:UploadLayerPart, and ecr:CompleteLayerUpload. The managed policy AmazonEC2ContainerRegistryPowerUser covers all of these.

Gotcha — the token expires after 12 hours. aws ecr get-login-password returns a token that is only valid for 12 hours. That is fine on your laptop, but in a CI/CD pipeline (an automated build server) you must run the get-login-password | docker login step fresh on every run. Caching the token between builds will eventually fail with denied: Your authorization token has expired. Always re-authenticate at the start of each pipeline job.

Image scanning

ECR can inspect images for known vulnerabilities (the CVE database — Common Vulnerabilities and Exposures) so you catch insecure base images before they ship.

  • Basic scanning — free, uses the open-source Clair scanner, runs on push or on demand.
  • Enhanced scanning — powered by Amazon Inspector, continuously rescans as new CVEs are published and covers OS packages plus language libraries. It is billed per image scan.

When to use enhanced scanning: production workloads where a vulnerability disclosed next week should re-flag images you pushed last month. When basic is enough: dev and test repos where a one-time scan-on-push is plenty.

aws ecr describe-image-scan-findings \
  --repository-name web-api \
  --image-id imageTag=v1.4.2 \
  --region us-east-1

Lifecycle policies (control storage cost)

ECR storage is billed at about $0.10 per GB-month. That sounds tiny, but every CI build pushes a new image, and untagged layers from overwritten tags pile up silently. A busy repo can quietly grow to hundreds of gigabytes and cost real money if nothing ever cleans it up.

A lifecycle policy is a JSON rule that automatically expires old images. Save this as lifecycle.json:

{
  "rules": [
    {
      "rulePriority": 1,
      "description": "Expire untagged images after 14 days",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 14
      },
      "action": { "type": "expire" }
    },
    {
      "rulePriority": 2,
      "description": "Keep only the 20 most recent tagged images",
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["v"],
        "countType": "imageCountMoreThan",
        "countNumber": 20
      },
      "action": { "type": "expire" }
    }
  ]
}

Apply it:

aws ecr put-lifecycle-policy \
  --repository-name web-api \
  --lifecycle-policy-text file://lifecycle.json \
  --region us-east-1

Console steps: open the repository > Lifecycle Policy tab > Create rule > pick untagged or tagged, set the age or count, then Save.

Tip: Set a lifecycle policy the moment you create a repository, not after the storage bill surprises you. Expiring untagged images after two weeks and capping tagged images at 20-30 keeps almost any repo’s storage cost negligible.

Infrastructure as code

Defining the repo in Terraform keeps scanning and lifecycle settings under version control:

resource "aws_ecr_repository" "web_api" {
  name                 = "web-api"
  image_tag_mutability = "IMMUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }
}

resource "aws_ecr_lifecycle_policy" "web_api" {
  repository = aws_ecr_repository.web_api.name
  policy     = file("lifecycle.json")
}

Best Practices

  • Turn on scan-on-push for every repository, and use enhanced scanning for production images.
  • Attach a lifecycle policy at creation time to expire untagged and old images and keep storage cost near zero.
  • In CI/CD, run get-login-password | docker login at the start of each job — the token lasts only 12 hours.
  • Use immutable tags in production so a release like v1.4.2 can never be silently overwritten, and pin deployments to image digests for true reproducibility.
  • Grant the least privilege needed: read-only pull policies for runtime roles, push permission only for build pipelines.
  • Use a VPC interface endpoint for ECR so containers pull images over the private network instead of the public internet, avoiding NAT/egress charges.
Last updated June 15, 2026
Was this helpful?