Skip to content
ExponaBLOG
AWS FOR DEVELOPERS · DAY 022 / 100

ECR and image management: a hands-on guide for engineers

A private registry for your containers.

Expona Admin7 min read

ECR and image management: a hands-on guide for engineers

Container registries are the backbone of modern deployment pipelines, yet many teams treat them as a fire-and-forget service. In reality, how you manage images in AWS Elastic Container Registry (ECR) directly impacts your ability to debug production issues, recover from deployments gone wrong, and control infrastructure costs. This hands-on guide walks you through the essential practices for ECR and image management that keep your deployments traceable, immutable, and efficient.

Why ECR matters in your deployment workflow

ECR sits at a critical junction in your infrastructure. Every container that runs in ECS, EKS, or on EC2 passes through it. A poorly managed registry becomes a liability: you lose the ability to pin exact versions, you can't easily trace which code built an image, and you burn budget on images you no longer need. Done right, ECR becomes a single source of truth for your deployments.

Tag with commit SHA — Traceable, immutable deployments

The most common mistake we see in production systems is tagging images with semantic versions or human-readable labels that change over time. When you push a new build with tag latest or v1.2.3, you've just overwritten the previous image. If something goes wrong, you can't easily revert because you've lost the exact artifact that was deployed.

The solution is straightforward: tag every image with the commit SHA from your version control system. This creates an immutable link between your code, your image, and your deployment.

Here's a practical example in a CI/CD pipeline using GitHub Actions:

bash
#!/bin/bash
# In your build step
COMMIT_SHA=$(git rev-parse --short HEAD)
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ECR_REGISTRY=$AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com
ECR_REPO=my-app

# Login to ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $ECR_REGISTRY

# Build and push with commit SHA
docker build -t $ECR_REGISTRY/$ECR_REPO:$COMMIT_SHA .
docker push $ECR_REGISTRY/$ECR_REPO:$COMMIT_SHA

# Optionally also tag as latest for convenience
docker tag $ECR_REGISTRY/$ECR_REPO:$COMMIT_SHA $ECR_REGISTRY/$ECR_REPO:latest
docker push $ECR_REGISTRY/$ECR_REPO:latest

Now your deployment configuration references the exact SHA:

bash
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: app
    image: 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:a1b2c3d

If you need to rollback, you know exactly which image to redeploy. If you need to debug, you can check out that exact commit SHA from git. You've created an auditable chain from code to container to production.

Immutable tags — Prevent overwriting an existing tag

Even with the best intentions, accidents happen. A developer might manually push an image with an existing tag, or a misconfigured CI job might overwrite a production tag. Immutable tags in ECR prevent this entirely.

When you enable immutability on a repository, ECR rejects any attempt to push an image with a tag that already exists. This is a safety mechanism that forces intentional decisions about tagging.

Enable immutable tags via the AWS CLI:

bash
aws ecr put-image-tag-mutability \
  --repository-name my-app \
  --image-tag-mutability IMMUTABLE \
  --region us-east-1

Or in Terraform:

bash
resource "aws_ecr_repository" "my_app" {
  name             = "my-app"
  image_tag_mutability = "IMMUTABLE"
}

With immutable tags enabled, if your build script tries to push to a tag that already exists, the push fails and your CI pipeline alerts you to investigate. This prevents the silent corruption of your registry.

A common concern is flexibility: what if you want to retag an image? With commit SHA tags, you don't need to. Instead of mutating a tag, you push a new image with a new tag and update your deployment configuration. This is the correct pattern anyway—it keeps your audit trail clean.

Lifecycle policies — Delete old untagged images automatically

Over months and years of deployments, your ECR repository accumulates images. Each image consumes storage, and AWS charges per GB per month. Without a cleanup strategy, your registry bill grows unnecessarily.

Lifecycle policies are rules that automatically delete images based on age or tag status. The most common use case is deleting untagged images older than a certain age. Untagged images are usually the result of failed builds or images that have been retagged and are no longer referenced.

Here's a lifecycle policy that deletes untagged images older than 30 days:

bash
aws ecr put-lifecycle-policy \
  --repository-name my-app \
  --lifecycle-policy-text '
{
  "rules": [
    {
      "rulePriority": 1,
      "description": "Delete untagged images older than 30 days",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 30
      },
      "action": {
        "type": "expire"
      }
    }
  ]
}
' \
  --region us-east-1

You can also add rules to clean up tagged images with specific patterns. For example, delete all images tagged with branch names older than 7 days (useful if you deploy every pull request):

bash
{
  "rules": [
    {
      "rulePriority": 1,
      "description": "Delete branch images older than 7 days",
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["pr-", "feature-"],
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 7
      },
      "action": {
        "type": "expire"
      }
    },
    {
      "rulePriority": 2,
      "description": "Keep last 100 production images",
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["v"],
        "countType": "imageCountMoreThan",
        "countNumber": 100
      },
      "action": {
        "type": "expire"
      }
    }
  ]
}

Set lifecycle policies in Terraform as well:

bash
resource "aws_ecr_lifecycle_policy" "my_app" {
  repository = aws_ecr_repository.my_app.name

  policy = jsonencode({
    rules = [
      {
        rulePriority = 1
        description  = "Delete untagged images after 30 days"
        selection = {
          tagStatus     = "untagged"
          countType     = "sinceImagePushed"
          countUnit     = "days"
          countNumber   = 30
        }
        action = {
          type = "expire"
        }
      }
    ]
  })
}

Test your lifecycle policy with a dry run before deploying to production:

bash
aws ecr start-lifecycle-policy-preview \
  --repository-name my-app \
  --lifecycle-policy-text file://policy.json \
  --region us-east-1

Pull through cache — Cache public images to avoid rate limits

Most applications depend on public images from Docker Hub, GitHub Container Registry, or other public registries. When you pull from these registries directly at runtime, you're subject to their rate limits. Docker Hub, for example, limits unauthenticated pulls to 100 per six hours.

ECR's pull through cache feature acts as a proxy. You configure ECR to automatically cache and serve copies of public images. After the first pull, subsequent pulls come from your private ECR registry, bypassing public rate limits and improving deployment speed in high-volume environments.

Create a pull through cache rule:

bash
aws ecr create-pull-through-cache-rule \
  --ecr-repository-prefix docker-hub \
  --upstream-registry-url docker.io \
  --region us-east-1

Now when your container runtime tries to pull docker.io/postgres:15, you reference it as:

bash
123456789.dkr.ecr.us-east-1.amazonaws.com/docker-hub/postgres:15

ECR transparently fetches from Docker Hub on the first pull and caches it. Subsequent pulls come from your registry.

You can set up multiple pull through cache rules for different upstream registries:

bash
aws ecr create-pull-through-cache-rule \
  --ecr-repository-prefix public-ecr \
  --upstream-registry-url public.ecr.aws \
  --region us-east-1

aws ecr create-pull-through-cache-rule \
  --ecr-repository-prefix ghcr \
  --upstream-registry-url ghcr.io \
  --region us-east-1

In Terraform:

bash
resource "aws_ecr_pull_through_cache_rule" "docker_hub" {
  ecr_repository_prefix = "docker-hub"
  upstream_registry_url = "docker.io"
}

Combine pull through cache with lifecycle policies to manage space: cache rules can be separately cleaned up if they exceed storage quotas.

Common mistakes

  • Overwriting tags: Using semantic versions or branch names as tags and allowing overwrites. Always use immutable tags or commit SHAs to maintain an audit trail.
  • Ignoring image size: Not optimizing Dockerfiles leads to large images that slow deployments and consume bandwidth. Multi-stage builds and minimal base images matter.
  • Forgetting lifecycle policies: Leaving years of untagged build artifacts in your repository inflates storage costs unnecessarily.
  • No image scanning: ECR can scan images for vulnerabilities. Don't push production images without at least basic scanning enabled.
  • Manual image management: Treating ECR like a file store rather than a versioned artifact repository. Automate your tagging and cleanup.

Key takeaways

  • Tag images with commit SHAs to create immutable, auditable links between code and containers.
  • Enable image tag immutability to prevent accidental overwrites and enforce intentional tagging decisions.
  • Use lifecycle policies to automatically clean up old, untagged images and control storage costs.
  • Implement pull through cache to reduce rate limit issues and improve deployment speed for public image dependencies.
  • Treat ECR as a versioned artifact repository, not a file store—automate tagging, cleanup, and scanning in your CI/CD pipeline.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles