Hands-On: Push Your First Image to ECR
What You Will Build
A small Node API packaged as a Docker image, pushed to a private ECR repository with proper version tags, then pulled and run on an EC2 instance using an IAM role instead of stored credentials.
You need Docker and the AWS CLI configured locally, plus a running EC2 instance for the final step.
Step 1 — Something to Containerize
A minimal API so the focus stays on the registry workflow:
mkdir ecr-demo && cd ecr-demo
npm init -y
npm install express
server.js:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.json({ service: 'ecr-demo', version: process.env.APP_VERSION || 'dev' });
});
app.listen(3000, () => console.log('Up on 3000'));
Dockerfile:
FROM node:22-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js .
EXPOSE 3000
USER node
CMD ["node", "server.js"]
Build and smoke-test locally:
docker build -t ecr-demo .
docker run --rm -p 3000:3000 ecr-demo
curl localhost:3000 # {"service":"ecr-demo","version":"dev"}
Step 2 — Create the Repository
aws ecr create-repository \
--repository-name ecr-demo \
--image-scanning-configuration scanOnPush=true \
--region us-east-1
The response includes repositoryUri — the address everything else hangs off. Stash your account ID and the URI in shell variables to avoid typos:
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=us-east-1
REPO=$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/ecr-demo
echo $REPO
Step 3 — Authenticate Docker
aws ecr get-login-password --region $REGION \
| docker login --username AWS --password-stdin \
$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
Login Succeeded means Docker now holds a token valid for 12 hours. When a push fails tomorrow with no basic auth credentials, this is the command to rerun — worth aliasing in your shell profile.
Step 4 — Tag and Push
Tag the local image with both a version and latest:
docker tag ecr-demo:latest $REPO:v1.0.0
docker tag ecr-demo:latest $REPO:latest
docker push $REPO:v1.0.0
docker push $REPO:latest
The second push completes almost instantly — the layers already exist in the registry, only the tag pointer is new. Verify:
aws ecr describe-images \
--repository-name ecr-demo \
--query 'imageDetails[].{tags:imageTags,pushed:imagePushedAt,sizeMB:imageSizeInBytes}' \
--output table
Since scan-on-push is enabled, findings show up a minute later:
aws ecr describe-image-scan-findings \
--repository-name ecr-demo \
--image-id imageTag=v1.0.0 \
--query 'imageScanFindings.findingSeverityCounts'
Step 5 — Ship a Second Version
Change the default version string in server.js to '2.0', then:
docker build -t ecr-demo .
docker tag ecr-demo:latest $REPO:v1.1.0
docker tag ecr-demo:latest $REPO:latest
docker push $REPO:v1.1.0
docker push $REPO:latest
Now latest points at v1.1.0, but v1.0.0 is still pullable — that's your rollback path. This tag-both-push-both loop is exactly what your CI pipeline will automate.
Step 6 — Give EC2 Pull Permission the Right Way
Never copy AWS keys onto an instance. Attach an IAM role instead:
- IAM → Roles → Create role → AWS service → EC2.
- Attach the managed policy
AmazonEC2ContainerRegistryReadOnly. - Name it
ec2-ecr-pull, create it. - EC2 → select instance → Actions → Security → Modify IAM role → attach
ec2-ecr-pull.
No instance restart required — credentials are available within seconds via the instance metadata service.
Step 7 — Pull and Run on EC2
SSH into the instance. Install Docker and the AWS CLI if they aren't there:
sudo apt update && sudo apt install -y docker.io awscli
sudo usermod -aG docker ubuntu && newgrp docker
Authenticate — same command as Step 3, but the token now comes from the instance role, no aws configure needed:
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS --password-stdin \
$ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com
docker pull $ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/ecr-demo:v1.1.0
docker run -d --name ecr-demo --restart unless-stopped -p 3000:3000 \
$ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/ecr-demo:v1.1.0
curl localhost:3000 # {"service":"ecr-demo","version":"2.0"}
Rolling back is just pulling and running v1.0.0 instead — the payoff of never deploying by latest alone.
Cleanup
aws ecr delete-repository \
--repository-name ecr-demo \
--force # deletes contained images too
On the EC2 side, docker rm -f ecr-demo and detach the IAM role if it was only for this exercise.
Common Problems
| Symptom | Likely Cause |
|---|---|
no basic auth credentials on push/pull | Login token expired (12 h) — rerun the login command |
denied: ... not authorized to perform ecr:InitiateLayerUpload | Your IAM identity lacks push permissions — needs PowerUser-level ECR policy |
| Pull works locally but fails on EC2 | Instance role missing or lacks AmazonEC2ContainerRegistryReadOnly |
repository does not exist | Region mismatch — the repo lives in one region, your command targets another |