Amazon ECR Best Practices: Security, Tagging, Scanning & Lifecycle Policies
Amazon ECR stores your Docker images on AWS and serves them to ECS, EKS, Lambda, and App Runner using IAM roles. No Docker Hub credentials, no pull rate limits, no third-party registry traffic. The ECR overview covers the fundamentals. This guide covers what production configuration actually looks like: safer images, predictable deployments, controlled storage costs, and tight access control.
Simple explanation#
ECR is a private warehouse for container images. Your CI pipeline builds an image from your code, pushes it to ECR with a unique tag, and your ECS service pulls it from there when starting new containers.
Think of each image tag like a tracking number on a package. my-app:abc12345 means “the software built from commit abc12345, packaged and ready to ship.” Without that tracking number, you cannot tell which version of your app is running in production — and when something breaks, you are searching through boxes with no labels.
Production ECR configuration adds guardrails: preventing tags from being overwritten, automatically deleting old images, scanning for vulnerabilities, and restricting who can push and pull.
ECR is just storage. The practices on this page are about making that storage trustworthy: knowing what is in it, keeping it clean, and controlling who can write to it.
How ECR fits into a CI/CD pipeline#
A typical AWS container pipeline works like this:
- Developer pushes code to GitHub or CodeCommit
- AWS CodeBuild builds the Docker image and runs tests
- CodeBuild authenticates to ECR and pushes the image tagged with the commit SHA
- The pipeline triggers an ECS service update referencing the new image tag
- ECS pulls the image from ECR and starts new tasks
ECR sits at step 3. Your choices here (how images are tagged, who can push, what vulnerabilities are acceptable) affect everything downstream. A misconfigured registry means unknown images in production, broken rollbacks, and undetected CVEs. For the full pipeline walkthrough, see CI/CD Pipelines for ECS.
Recommended production defaults#
Start here when creating a new ECR repository for production use:
- Tag immutability: enabled. Commit SHA tags cannot be overwritten after push.
- Lifecycle policy: set at creation. Expire untagged images after 1 day; keep the last 30 tagged images.
- Scanning: basic at minimum; enhanced for production repositories.
- Push access: CI/CD role only. Restrict
ecr:PutImageto the build role. - Pull access: task execution role only. Not open to account root.
- Encryption: AWS-managed by default; KMS for regulated workloads.
- VPC endpoints: add if tasks run in private subnets without a NAT gateway.
- Replication: add if deploying to multiple regions.
The rest of this guide explains each setting, when it matters, and what tradeoffs to expect.
Image tagging and tag immutability#
Tag every image with the commit SHA#
Deploying with :latest is the most common ECR mistake. You cannot tell what is running in production, cannot roll back to a specific version, and cannot correlate an incident to a specific build.
Tag every image with the git commit SHA. In a CodeBuild buildspec:
IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8)
docker build -t $REPO_URI:$IMAGE_TAG .
docker push $REPO_URI:$IMAGE_TAG
This gives you a stable, unique, traceable tag per build. If something breaks in production, you know exactly which commit caused it and can redeploy the previous tag immediately.
If you deploy with :latest and a production incident happens at 2am, your only option is to dig through CloudWatch logs and piece together what commit was running. Commit SHA tags make incident response measurably faster. They cost nothing to implement.
Enable tag immutability#
Tag immutability prevents any push from overwriting an existing tag. Think of it like a printed edition number on a published book: once “978-X” is printed, that number always refers to that exact edition. You never go back and stamp a different book with the same number.
aws ecr put-image-tag-mutability \
--repository-name my-app \
--image-tag-mutability IMMUTABLE \
--region us-east-1
| Mode | Behavior | When to use |
|---|---|---|
| IMMUTABLE | Push to an existing tag fails with an error | Production repos deploying by commit SHA |
| MUTABLE | Existing tags can be overwritten silently | Dev repos or workflows that rebuild :latest |
With immutable tags, you can no longer push :latest on every build. Most teams solve this by dropping :latest entirely for production repositories and deploying by commit SHA only.
Lifecycle policies#
Without lifecycle policies, ECR repositories grow indefinitely. Think of it like a garage with no “clear out” rule: every car you ever owned is still parked in there, taking up space. Every CI build pushes at least one image. After months of active development, you accumulate hundreds of old images with unpatched vulnerabilities and an increasing storage bill.
A practical starting policy for most teams:
{
"rules": [
{
"rulePriority": 1,
"description": "Expire untagged images after 1 day",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 1
},
"action": { "type": "expire" }
},
{
"rulePriority": 2,
"description": "Keep last 30 tagged images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["v"],
"countType": "imageCountMoreThan",
"countNumber": 30
},
"action": { "type": "expire" }
}
]
}
aws ecr put-lifecycle-policy \
--repository-name my-app \
--lifecycle-policy-text file://lifecycle-policy.json \
--region us-east-1
Rule 1 cleans up untagged images quickly. When you push to an existing mutable tag, the old image loses its tag and becomes untagged. These orphaned images accumulate silently without this rule. Rule 2 caps the total count of tagged images to control storage costs over time.
ECR evaluates rules in priority order. Set lifecycle policies when you create the repository. Retroactive cleanup after a year of builds is more painful than starting correctly.
For Terraform for AWS users, manage lifecycle policies alongside the repository definition with the aws_ecr_lifecycle_policy resource.
Vulnerability scanning#
Basic vs enhanced scanning#
| Basic scanning | Enhanced scanning | |
|---|---|---|
| Engine | Clair | Amazon Inspector |
| Coverage | OS packages | OS packages + language packages (npm, pip, Maven, Go) |
| Re-scans after push | No (results go stale) | Yes, continuous as new CVEs are published |
| Cost | Free | Additional cost per image |
| Findings location | ECR console | Amazon Inspector + EventBridge |
| Recommended for | Dev and non-critical repos | All production repositories |
Enable enhanced scanning at the registry level so it applies to all repositories automatically:
aws ecr put-registry-scanning-configuration \
--scan-type ENHANCED \
--rules '[{
"repositoryFilters": [{"filter": "*", "filterType": "WILDCARD"}],
"scanFrequency": "CONTINUOUS_SCAN"
}]' \
--region us-east-1
Basic scanning is free and takes one line to enable. Skipping it means you are shipping containers to production with no awareness of known CVEs. Enhanced scanning costs money, but basic scanning has no excuse to skip.
Blocking deployments on critical findings#
Add this check to your CodeBuild buildspec after pushing the image. If critical vulnerabilities are found, the build fails before deployment begins:
CRITICAL=$(aws ecr describe-image-scan-findings \
--repository-name my-app \
--image-id imageTag=$IMAGE_TAG \
--query 'imageScanFindings.findingSeverityCounts.CRITICAL' \
--output text 2>/dev/null || echo "0")
[ "${CRITICAL:-0}" -gt "0" ] && { echo "Critical CVEs found. Failing build."; exit 1; }
This pairs with the Secure CI/CD Pipelines practice of making the build pipeline a security gate, not just a delivery mechanism.
Access control: IAM vs repository policies vs registry policies#
Three policy types control ECR access. Understanding which to use is one of the more confusing parts of ECR setup.
| Policy type | Attached to | Controls | Cross-account? |
|---|---|---|---|
| IAM policy | Role or user | What that principal can do | Same account only |
| Repository policy | ECR repository | Who can access this repository | Yes |
| Registry policy | ECR registry | Registry-wide rules (replication, pull-through cache) | Yes |
For most teams: IAM policies on the CodeBuild and ECS roles, with repository policies only when cross-account access is needed. Here is what least-privilege push and pull looks like as a repository policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCIRolePush",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:role/CodeBuildRole"},
"Action": [
"ecr:BatchCheckLayerAvailability", "ecr:PutImage",
"ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload"
]
},
{
"Sid": "AllowECSPull",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:role/ECSTaskExecutionRole"},
"Action": [
"ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer", "ecr:BatchCheckLayerAvailability"
]
}
]
}
This restricts push to the CI role only. Every image in ECR has traceable build provenance. No other principal in the account can push without using that specific role. If IAM roles are new to you, that guide covers the mechanics of the push/pull role separation.
In a single-account setup, you often do not need a repository policy at all. IAM policies on the CodeBuild role (push) and ECS task execution role (pull) are sufficient. Repository policies become necessary when you need cross-account access or want an extra explicit restriction on who can write to the registry.
Encryption and image signing#
Encryption at rest#
ECR encrypts images at rest by default using AWS-managed KMS keys. This is sufficient for most workloads. If compliance requires customer-managed keys (for rotation control, audit trails, or cross-account key management), configure KMS at repository creation:
aws ecr create-repository \
--repository-name my-app \
--encryption-configuration \
encryptionType=KMS,kmsKey=arn:aws:kms:us-east-1:123456789012:key/mrk-abc123 \
--region us-east-1
You cannot change a repository’s encryption configuration after creation. Decide on KMS before creating the repository, not after.
Image signing with AWS Signer#
Image signing lets you verify that a container image was produced by your authorized pipeline and has not been modified since. AWS Signer attaches a cryptographic signature to each image, and a verification step in your deployment pipeline checks it before running the container.
This is an advanced practice, most valuable in regulated environments or when you need to enforce that only pipeline-produced images can run in production. Implement the foundational settings on this page before adding image signing.
Cross-account and cross-region patterns#
Cross-account pulls#
In a multi-environment setup, images are typically built in a central tooling account and deployed into separate production, staging, and dev accounts. Cross-account pulls require permission on both sides.
In the source account — add a repository policy granting pull access to the destination account:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowProductionAccountPull",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::999888777666:root"},
"Action": [
"ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer", "ecr:BatchCheckLayerAvailability"
]
}]
}
In the destination account — the ECS task execution role also needs an IAM policy allowing ecr:GetAuthorizationToken and the pull actions against the source registry ARN. The repository policy alone is not sufficient.
Cross-region replication#
If ECS services run in multiple regions, pulling images across regions adds latency and data transfer costs. ECR replication copies images to destination regions automatically after each push:
aws ecr put-replication-configuration \
--replication-configuration '{
"rules": [{
"destinations": [{"region": "eu-west-1", "registryId": "123456789012"}],
"repositoryFilters": [{"filter": "my-app", "filterType": "PREFIX_MATCH"}]
}]
}' \
--region us-east-1
Replication is asynchronous. Do not trigger a cross-region deployment immediately after a push without confirming the image has arrived in the target region.
Private networking with VPC endpoints#
By default, ECR API calls and image pulls use the public internet, even from within AWS. Without VPC endpoints, your private ECS tasks are like workers in a secured office building who must step outside onto a public street every time they need to collect a package from ECR. VPC endpoints are a private loading dock inside the building: the traffic never leaves AWS’s network.
ECR requires three endpoints for fully private access:
| Endpoint | Type | Purpose |
|---|---|---|
com.amazonaws.region.ecr.api | Interface | ECR control plane (create repos, list images) |
com.amazonaws.region.ecr.dkr | Interface | Docker registry protocol (image pulls) |
com.amazonaws.region.s3 | Gateway | Image layer storage (ECR stores layers in S3) |
The S3 gateway endpoint is the one teams most often forget. ECR stores image layers in S3, not directly in the ECR service. Adding only the ecr.api and ecr.dkr interface endpoints will leave image pulls failing with confusing network errors.
resource "aws_vpc_endpoint" "ecr_api" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.ecr.api"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.endpoints.id]
private_dns_enabled = true
}
resource "aws_vpc_endpoint" "ecr_dkr" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.ecr.dkr"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.endpoints.id]
private_dns_enabled = true
}
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = aws_route_table.private[*].id
}
Interface endpoints have an hourly cost per Availability Zone. If you already have a NAT gateway for other traffic, routing ECR through NAT may be cheaper than adding dedicated endpoints. Run the numbers for your specific workload.
When to use these settings#
Not every team needs everything from day one. Here is a practical decision guide:
| Setting | Use when | Skip when |
|---|---|---|
| Tag immutability | Deploying by commit SHA to production | Frequently rebuilding :latest during development |
| Lifecycle policies | Always; set at creation | — |
| Basic scanning | Always; it is free | — |
| Enhanced scanning | Production repos; regulated workloads | Dev and scratch repositories |
| Repository policy (push restriction) | Multiple people or systems have ECR write permissions | Single-pipeline, single-account setups |
| Cross-account policy | Multi-account org with a shared image registry | Single-account workloads |
| Cross-region replication | Multi-region ECS deployments | Single-region workloads |
| VPC endpoints | ECS tasks in private subnets without NAT | Tasks already behind a NAT gateway |
| KMS encryption | Compliance requires customer-managed keys | Standard workloads |
| Image signing | Regulated environments; enforcing pipeline provenance | Most teams (implement basics first) |
For handling secrets that your build pipeline accesses alongside ECR pushes, see Secrets in CI/CD Pipelines.
Common mistakes#
- Deploying with
:latest. You cannot tell what is running in production, cannot roll back to a specific version, and cannot correlate an incident to a specific build. Tag every image with the commit SHA. - No lifecycle policies at repository creation. After a year of CI builds, you have thousands of images: most with known CVEs, all costing storage fees. Add lifecycle policies when you create the repository, not retroactively when costs get noticed.
- Skipping vulnerability scanning. Basic scanning is free and takes one setting to enable. Not scanning means you are unaware of publicly-disclosed CVEs in your production images. Enable it by default; use enhanced scanning for production repositories.
- Open push access. Without a repository policy restricting push, any IAM principal with
ecr:PutImagecan push images, including developers’ local machines. Restrict push to the CI/CD role so every image has traceable build provenance. - Only adding the repository policy for cross-account access. The source account repository policy alone is not sufficient. The pulling account also needs an IAM policy allowing the pull actions. Both sides must grant permission.
- Missing the S3 gateway endpoint in private networking setups. ECR stores image layers in S3. Adding the
ecr.apiandecr.dkrinterface endpoints without the S3 gateway endpoint results in image pulls failing with misleading network errors.
Summary#
- Tag every image with the commit SHA. Never deploy with
:latestin production. - Enable tag immutability on production repositories to prevent tag overwrites.
- Set lifecycle policies at repository creation: expire untagged images quickly and cap the total tagged image count.
- Use enhanced scanning (Amazon Inspector) for production repositories; block deployments when critical CVEs are found.
- Restrict push to the CI/CD role and pull to the task execution role using repository policies.
- Cross-account access requires permission on both sides: repository policy in the source account and IAM policy in the destination account.
- ECR replication copies images to other regions automatically. Add it for multi-region ECS deployments.
- VPC endpoints (ecr.api, ecr.dkr, S3 gateway) let private subnets pull images without a NAT gateway. All three are required.
- KMS encryption and image signing are advanced controls; implement the foundational settings first.
Frequently asked questions
What is ECR tag immutability and should I enable it?
Tag immutability prevents pushing a new image to an existing tag. Once you push my-app:abc12345, that tag always points to the same image. Enable it for production repositories that deploy by commit SHA. Keep it disabled only if your workflow requires overwriting mutable tags like latest.
How does an ECR lifecycle policy work?
Lifecycle policies define rules for automatically deleting images. Each rule matches images by tag status and a count or age threshold. ECR evaluates rules in priority order and expires matching images. Without lifecycle policies, repositories grow indefinitely and you pay for all stored layers, including old ones with unpatched vulnerabilities.
What is the difference between ECR basic and enhanced scanning?
Basic scanning uses Clair to check OS packages on push only, and results go stale as new CVEs are published. Enhanced scanning uses Amazon Inspector, covers OS packages and language packages (npm, pip, Maven, Go modules), and re-scans continuously. Use basic for dev and non-critical repos; use enhanced for anything running in production.
What is the difference between an IAM policy and an ECR repository policy?
IAM policies are identity-based: attached to a role, they define what that principal can do. Repository policies are resource-based: attached to the ECR repository, they define who can access it, including principals from other AWS accounts. Cross-account pulls require both a repository policy granting access from the source account and an IAM policy in the pulling account.
Do I need VPC endpoints to use ECR from private subnets?
Yes, if your ECS tasks run in private subnets with no internet access. Without endpoints, ECR calls and image pulls use the public internet, which requires a NAT gateway. Interface endpoints for ecr.api and ecr.dkr, plus an S3 gateway endpoint for image layers, let private subnets pull images without a NAT gateway.