Alright, let’s talk about ECR private repositories. Think of them as your own private, highly secure art gallery for your container images. Unlike Docker Hub, where you might leave your images on a public park bench for anyone to poke at, an ECR private repo is a vault. You control exactly who and what gets in. And because it’s AWS, it’s deeply integrated with all the other toys in their sandbox (IAM, CloudTrail, etc.), which is both its greatest strength and occasionally its most annoying source of complexity.

Creating Your First Private Repository

Creating a repo is laughably simple. You can do it through the AWS Console by clicking around, but we’re not animals, are we? We use the CLI. The magic incantation is:

aws ecr create-repository \
    --repository-name my-awesome-app \
    --image-tag-mutability MUTABLE \
    --image-scanning-configuration scanOnPush=true

Let’s break down the two non-obvious flags here. --image-tag-mutability can be MUTABLE or IMMUTABLE. Mutable is what you’re used to: you can push a new image over an existing tag (latest, I’m looking at you). Immutable means once a tag is pushed, it’s set in stone. You cannot overwrite it. This is fantastic for production environments where you absolutely, positively do not want an accidental redeploy to clobber the v1.0.1 tag you’re running. For dev, mutable is usually fine.

The --image-scanning-configuration enables a basic scan for known OS vulnerabilities on every push. It’s not a silver bullet, but it’s a free, zero-effort safety net. You’d be a fool not to enable it.

The Authentication Tango

Here’s where everyone gets tripped up the first time. Your local Docker daemon has no idea who AWS is. You can’t just docker push and hope for the best. You have to authenticate Docker with your ECR registry. AWS, in its infinite wisdom, uses your IAM credentials to generate a temporary Docker password. It’s secure, but it’s a bit of a dance.

Step one: Get the auth token. The command is:

aws ecr get-login-password --region us-east-1

This doesn’t do anything by itself. It just outputs a long, gibberish password to your terminal. The complete, standard way to do this is to pipe that password directly to the docker login command, all in one shot:

aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com

Replace that giant number with your actual AWS account ID. Why is it so long? No one knows. The --password-stdin flag is crucial—it tells Docker to read the password from the standard input pipe instead of prompting you for it, which is both more secure and allows you to automate this.

Pro Tip: The authorization token this generates is valid for 12 hours. If your CI/CD pipeline takes longer than that to build and push an image… well, you have other problems. But seriously, remember that this is a short-lived credential. You must run this command as part of any script that needs to push or pull from ECR.

IAM: The Real Gatekeeper

Creating the repo and authenticating is only half the battle. The real access control happens in IAM. You can have the most perfectly crafted docker login command, but if your IAM user/role doesn’t have the correct permissions, you’ll get a frustratingly generic “access denied” error.

You need to attach a policy to your IAM principal that allows ecr actions. The managed policy AmazonEC2ContainerRegistryPowerUser is a good starting point for a developer machine—it lets you push and pull anything. For production systems, you’ll want to be far more granular. A minimal push policy might look like this:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "ecr:BatchCheckLayerAvailability",
                "ecr:PutImage",
                "ecr:InitiateLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:CompleteLayerUpload"
            ],
            "Resource": "arn:aws:ecr:us-east-1:123456789012:repository/my-awesome-app"
        },
        {
            "Effect": "Allow",
            "Action": "ecr:GetAuthorizationToken",
            "Resource": "*"
        }
    ]
}

Notice the two parts? The first statement grants permissions on a specific repository ARN. The second statement grants permission to get the login token, and it must be a wildcard (*). AWS designed it this way, so you just have to accept this little quirk. Don’t waste an hour trying to lock down the GetAuthorizationToken call to a specific resource; it won’t work.

Pushing and Pulling Like a Pro

Once you’re authenticated and authorized, the commands are standard Docker fare, just with the full ECR URI.

# Tag your local image to match the ECR repo
docker tag my-awesome-app:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-awesome-app:latest

# Push it
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-awesome-app:latest

# Pull it (somewhere else)
docker pull 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-awesome-app:latest

Common Pitfall: The most common mistake is a typo in that giant URI. The format is absolutely critical: [account-id].dkr.ecr.[region].amazonaws.com/[repo-name]:[tag]. Miss a dash or get the region wrong and you’ll be scratching your head for longer than you’d care to admit. My advice? Copy-paste the repository URI directly from the AWS Console the first few times.