How to Push a Docker Image to Amazon ECR (Step by Step)
To push a Docker image to Amazon ECR: create a repository, log in with aws ecr get-login-password | docker login, tag the image with the full ECR URI (ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPO:TAG), and run docker push. The account, region and repository in the tag must match the ones you logged in to. Most errors come from a mismatch there, or from missing IAM permissions.
Quick answer
aws ecr create-repository --repository-name orders-api --region ap-south-1
aws ecr get-login-password --region ap-south-1 | \
docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-south-1.amazonaws.com
docker tag orders-api:2.4.1 123456789012.dkr.ecr.ap-south-1.amazonaws.com/orders-api:2.4.1
docker push 123456789012.dkr.ecr.ap-south-1.amazonaws.com/orders-api:2.4.1
A live example first
We have built orders-api:2.4.1 on an EC2 instance in Mumbai (ap-south-1). The AWS account ID here is the documentation example 123456789012. Use yours from aws sts get-caller-identity.
- Create the repository with scanning and immutable tags turned on:
aws ecr create-repository \
--repository-name orders-api \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability IMMUTABLE \
--region ap-south-1
"repositoryUri": "123456789012.dkr.ecr.ap-south-1.amazonaws.com/orders-api"
- Log in:
aws ecr get-login-password --region ap-south-1 | docker login --username AWS \
--password-stdin 123456789012.dkr.ecr.ap-south-1.amazonaws.com
Login Succeeded
- Tag and push:
docker tag orders-api:2.4.1 123456789012.dkr.ecr.ap-south-1.amazonaws.com/orders-api:2.4.1
docker push 123456789012.dkr.ecr.ap-south-1.amazonaws.com/orders-api:2.4.1
The push refers to repository [123456789012.dkr.ecr.ap-south-1.amazonaws.com/orders-api]
5f70bf18a086: Pushed
2.4.1: digest: sha256:3c1f...9ab2 size: 1784
- Check:
aws ecr describe-images --repository-name orders-api --region ap-south-1 \
--query 'imageDetails[].imageTags'
Done. Friends, notice that the tag itself tells Docker where to send the image. That is why the full URI matters.
How does pushing to ECR work?
The image tag is the address: account, region and repository. Log in to that same address before you push.
Docker does not know about AWS. It only knows registries and logins. get-login-password asks AWS for a temporary password (valid for 12 hours) using your IAM identity, and docker login saves it for that registry address. When you push, Docker reads the registry address from the tag and uploads the layers there. Layers that already exist are skipped, so the second push is fast.
How do I push step by step?
- Run
aws sts get-caller-identityand note the account ID. - Pick a region and stay with it.
- Create the repository once.
- Log in with
get-login-password. - Tag the image with the full URI.
- Push.
- Check with
describe-imagesor the console.
IAM permissions for pushing
Attach a policy like this to the CI role or user (narrowed to one repository):
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": "ecr:GetAuthorizationToken", "Resource": "*" },
{ "Effect": "Allow",
"Action": ["ecr:BatchCheckLayerAvailability", "ecr:InitiateLayerUpload",
"ecr:UploadLayerPart", "ecr:CompleteLayerUpload", "ecr:PutImage"],
"Resource": "arn:aws:ecr:ap-south-1:123456789012:repository/orders-api" }
]
}
GetAuthorizationToken must be on *. The upload actions can be limited to one repository.
Push from CI, one tool at a time
GitHub Actions (with OIDC, no stored keys)
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-ecr-push
aws-region: ap-south-1
- uses: aws-actions/amazon-ecr-login@v2
- run: |
docker build -t $ECR/orders-api:${{ github.sha }} .
docker push $ECR/orders-api:${{ github.sha }}
Jenkins on EC2
Give the Jenkins EC2 instance an IAM role with the policy above, and run the same four commands in a shell step. Do not save access keys in Jenkins.
GitLab CI
Use GitLab's OIDC token with an IAM role, or a short-lived role from a runner on EC2.
Common errors and fixes
| Error | Cause | Fix |
|---|---|---|
no basic auth credentials |
Not logged in to this URI | Log in again with the same account and region |
denied: User ... is not authorized to perform: ecr:InitiateLayerUpload |
IAM policy missing | Add the push actions |
name unknown: The repository with name 'orders-api' does not exist |
Repo not created, or wrong region | Create it, or fix the region |
tag invalid: The image tag '2.4.1' already exists ... immutable |
Immutable tag reused | Use a new tag, such as the git SHA |
Your authorization token has expired |
Token older than 12 hours | Log in again |
exec format error when the pod runs |
Built for arm64 on a Mac | docker buildx build --platform linux/amd64 |
Keep the registry clean and safe
- Turn on scan on push, or enhanced scanning with Amazon Inspector.
- Turn on tag immutability, so
2.4.1always means the same image. - Add a lifecycle policy to delete untagged images after 7 days.
- Keep repositories private. Use ECR Public only for images you really want to share.
- Use pull-through cache rules to copy Docker Hub images into ECR and avoid rate limits.
The same on other clouds
| Cloud | Registry | Login |
|---|---|---|
| AWS | ECR | aws ecr get-login-password \| docker login |
| Google Cloud | Artifact Registry | gcloud auth configure-docker asia-south1-docker.pkg.dev |
| Azure | ACR | az acr login --name myregistry |
Real example: why private registries matter
In April 2019, Docker Hub disclosed unauthorised access to a database holding data for about 190,000 accounts, including some GitHub and Bitbucket tokens used for automated builds. Docker asked affected users to change passwords and reconnect their links. A private registry such as ECR, controlled by your own IAM roles with short-lived tokens, limits the damage when any one service has a breach.
Summary table
| Step | Command |
|---|---|
| Find account | aws sts get-caller-identity |
| Create repo | aws ecr create-repository --repository-name NAME |
| Login | aws ecr get-login-password \| docker login ... |
| Tag | docker tag img ACCOUNT.dkr.ecr.REGION.amazonaws.com/NAME:TAG |
| Push | docker push ...:TAG |
| Verify | aws ecr describe-images --repository-name NAME |
Quick quiz: check what you learned
How long is an ECR login token from get-login-password valid?
- About 12 hours
- Exactly 5 minutes
- About 30 days
- It never expires
Show answer
A. ECR authorization tokens last 12 hours, so CI logs in each run.
What must the image tag contain before pushing to ECR?
- Account, region and repo name
- Only the image ID number
- The Docker Hub username
- The EC2 instance ID
Show answer
A. docker push sends the image to the registry written inside the tag.
"no basic auth credentials" during push means what?
- Docker is not logged in there
- The repository is too large
- The image has a critical CVE
- The node role lacks EBS
Show answer
A. Docker has no saved login for that registry address.
Which ECR feature stops a tag from being overwritten?
- Tag immutability
- Lifecycle policy rules
- Cross-region replication
- Pull-through cache rules
Show answer
A. With immutable tags, a pushed tag like 2.4.1 cannot be replaced.
Learn it properly
This guide is the short path. These pages go deeper, with labs:
Frequently asked questions
What is Amazon ECR?
Amazon Elastic Container Registry is AWS's private Docker registry. EKS, ECS, Lambda and App Runner can pull images from it using IAM permissions.
How long does the ECR login last?
The token from get-login-password is valid for 12 hours. CI jobs should log in at the start of every run.
Why do I get "no basic auth credentials"?
Docker is not logged in to that exact registry URI. Run the login command again with the same account and region as the tag.
What IAM permissions do I need to push?
ecr:GetAuthorizationToken plus BatchCheckLayerAvailability, InitiateLayerUpload, UploadLayerPart, CompleteLayerUpload and PutImage on the repository.
Do I need a repository before I push?
Yes, by default. Pushing to a name that does not exist fails with "repository ... does not exist". Create it first.
How do I keep ECR storage costs low?
Add a lifecycle policy, for example to keep only the last 20 tagged images and remove untagged images after 7 days.