Skip to main content

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:

  1. IAM → Roles → Create role → AWS service → EC2.
  2. Attach the managed policy AmazonEC2ContainerRegistryReadOnly.
  3. Name it ec2-ecr-pull, create it.
  4. 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

SymptomLikely Cause
no basic auth credentials on push/pullLogin token expired (12 h) — rerun the login command
denied: ... not authorized to perform ecr:InitiateLayerUploadYour IAM identity lacks push permissions — needs PowerUser-level ECR policy
Pull works locally but fails on EC2Instance role missing or lacks AmazonEC2ContainerRegistryReadOnly
repository does not existRegion mismatch — the repo lives in one region, your command targets another