Storing images: ECR
Private images, scanning and tags that never change.
Part 1: The problem
Maya has QuickCart’s order service working in a container on her laptop. The servers on AWS need the same image. But it only exists on her laptop.
The team puts it on a public image site under quickcart/order-service:v1. A week later, Leo fixes a bug. He builds, tags the fix v1 again, and pushes. Nobody notices that v1 now means different code.
Then the fix turns out to have a bug of its own. Priya rolls back to v1, the version everyone knows was good. The bug stays. v1 is the fix now. The good build is still in the registry, but nobody knows how to ask for it.
So QuickCart needs a place for images that:
- only QuickCart can push to, and AWS can pull from;
- never lets
v1quietly change meaning; - checks each image for known security problems.
That place is ECR.
Part 2: The idea
A registry is a server that stores images, so that any machine allowed to reach it can download them. ECR, Elastic Container Registry, is AWS’s registry. It lives inside your AWS account, it’s private by default, and ECS and EKS can pull from it without any extra setup.
The words
| Word | What it is | Example |
|---|---|---|
| Registry | The whole store. In ECR, you get one per account per region | 111122223333.dkr.ecr.us-east-1.amazonaws.com |
| Repository | One app’s images, inside the registry | quickcart-orders |
| Tag | A label on one image in a repository | v1 |
| Digest | A fingerprint worked out from the image’s contents | sha256:7c4e9a1f… (64 characters long) |
| Image URI | The full name you give Docker or ECS | 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1 |
The image URI is just the registry, the repository and the tag joined together. Docker reads the part before the first / to know which server to talk to. That’s why a local image called order-service:v1 has to be given its full ECR name before you can push it.
Tags move. Digests don’t.
A tag is a name a person chose. In an ordinary registry, you can push a different image with the same tag, and the tag simply moves to it. The old image loses its name.
A digest is calculated from the image’s bytes. Change one byte and the digest changes. So a digest always means exactly one image.
| You ask for | You get |
|---|---|
quickcart-orders:v1 | whatever v1 points to right now |
quickcart-orders@sha256:7c4e9a1f… | exactly that image, forever |
quickcart-orders:latest | whatever was last pushed with the tag latest — not necessarily the newest build. Avoid it |
Immutable tags
ECR can make a repository’s tags immutable. Once v1 exists, ECR refuses any push that would move it. Every tag then means one build forever, and a rollback to v1 is always exact.
Scanning
ECR can scan each image for known vulnerabilities — published security problems in the software inside it, such as an old version of a system library.
| Setting | What it checks | Cost |
|---|---|---|
| Basic scanning | The operating system packages in the image, when it’s pushed | Free |
| Enhanced scanning | Also your language libraries (like Flask), again whenever new problems are published | Paid, through Amazon Inspector |
Scan on push means each image is scanned as soon as it arrives. Results are grouped by severity: Critical, High, Medium, Low and Informational. A scan doesn’t block anything by itself. It tells you, and your pipeline decides what to do about it — the continuous integration lesson does that.
Lifecycle policies
ECR keeps every image you push, and storage is billed per GB. A lifecycle policy is a set of rules that expire old images automatically. QuickCart uses two:
| Rule | Why |
|---|---|
| Expire untagged images 7 days after they were pushed | Nothing refers to them by name, and they pile up quietly |
Keep only the newest 20 images whose tag starts with v | Enough history to roll back, without keeping every build forever |
Logging in is different
Docker needs a username and password for a private registry. ECR doesn’t use a fixed password. Instead, the AWS CLI asks AWS for a temporary password that lasts 12 hours, using the login you already have in your terminal. You hand that password to Docker, and Docker remembers it until it expires.
The console has no way to push an image, so logging in and pushing are the parts of this lesson that happen in the terminal.
Part 3: See it
Follow the tag v1 through five steps: push a build, deploy it, push a fix with the same tag, lose a task, then roll back. Play it with Mutable tags first, then again with Immutable.
Repository quickcart-ordersTags: mutable
- No images yet
Running on ECS
- Nothing deployed yet
What to notice: with mutable tags, nothing failed and nothing warned. Every push, every task start and the rollback all “worked”. The only sign of trouble was two tasks running different code under one name — and nobody was looking at digests.
Part 4: Do it
Pennies — ECR storage is about $0.10 per GB each month in us-east-1, and this image is under 100 MB. Basic scanning is free. Prices change, so check the ECR pricing page if you want the exact number.
You’ll create a private repository with immutable tags, push QuickCart’s order service to it, watch ECR refuse an overwrite, and add a lifecycle policy. About 25 minutes.
You need Docker Desktop running, the AWS CLI with a working login (Set up your computer), and the order-service folder from Writing a Dockerfile: app.py, requirements.txt and Dockerfile.
Open a terminal in that folder:
cd order-service| Part | What it means |
|---|---|
cd | change directory — every command below runs in here |
order-service | the folder with the app and its Dockerfile |
Create the repository
Check the region selector at the top right of the console says N. Virginia (us-east-1).
In the AWS console: Amazon ECRPrivate registryRepositoriesCreate repository
Field Enter Repository name quickcart-ordersImage tag mutability Immutable Encryption configuration AES-256 — the default If the page has an Image scan settings section, turn Scan on push on. Then choose Create.
Newer versions of the console set scanning for the whole registry instead. If there’s no scan setting on this page, go to
In the AWS console: Amazon ECRPrivate registrySettingsScanning
and choose Basic scanning, with a scan on push filter of*, so every repository is scanned when an image arrives.You should seequickcart-ordersin the list, with Tag immutability: Immutable. The URI column shows111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders, with your own account ID at the start. Copy it — you’ll use it in every command below.Log Docker in to ECR
This is the one step in the lesson with a pipe. It can’t be done in the console. Replace
111122223333with your account ID — it’s the number at the start of the URI you just copied.Terminalaws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-east-1.amazonaws.comPart What it means aws ecr get-login-passwordask AWS for a temporary ECR password, using your terminal’s AWS login. It prints the password and nothing else --region us-east-1for the registry in this region. Each region has its own registry |a pipe: instead of printing the password on screen, pass it straight in as the input of the next command docker loginlog Docker in to a registry and remember the password --username AWSECR’s username is always the word AWS--password-stdinread the password from the pipe. So it never appears on screen, in your command history, or in the list of running processes, where --passwordwould put it111122223333.dkr.ecr.us-east-1.amazonaws.comthe registry to log in to: your account ID, dkr.ecr, then the regionYou should seeLogin SucceededThe login lasts 12 hours. After that, run this command again. Docker may also print a warning about where it stores the password; that’s normal.
Build the image for AWS
Terminaldocker build --platform linux/amd64 --provenance=false -t order-service:v1 .Part What it means docker buildbuild an image from the Dockerfile in this folder --platform linux/amd64build for Intel and AMD processors, which is what this course’s Fargate tasks use. A Mac with an Apple chip would otherwise build for ARM --provenance=falsedon’t attach a build record to the image. Without this, Docker’s builder (BuildKit) pushes an image index — a list holding your image plus a record — and ECR shows it as extra untagged rows -t order-service:v1name the image order-service, tagv1.use the files in this folder The last lines look like=> exporting to image => => naming to docker.io/library/order-service:v1The exact lines vary with your Docker version. The name at the end is what matters.
Give it its ECR name
Docker pushes an image to whatever server is named at the start of the image’s name. So give it a second name that starts with your registry:
Terminaldocker tag order-service:v1 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1Part What it means docker taggive an existing image another name. Nothing is copied order-service:v1the image you just built 111122223333.dkr.ecr.us-east-1.amazonaws.comyour registry: where it will be pushed /quickcart-ordersthe repository inside it :v1the tag It prints nothing when it works.
Push it
Terminaldocker push 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1Part What it means docker pushupload an image to the registry named at the start of its name 111122223333.dkr.ecr.us-east-1.amazonaws.comthe registry to upload to, read from the start of the name /quickcart-orders:v1the repository and tag inside it You should see lines like theseThe push refers to repository [111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders] 5f70bf18a086: Pushed e3c9b0a7d41f: Pushed v1: digest: sha256:7c4e9a1fb03d6e2a8f5c1b9d0e7a3f6c2b8d4e1a9f0c7b3e6d2a5f8c1e4b7d0a size: 1994There’s one
Pushedline per layer, and your IDs, digest and size will differ. The last line is the one to keep: that digest is the image’s fingerprint.Look at the image and its scan
In the AWS console: Amazon ECRPrivate registryRepositoriesquickcart-orders
You should seeOne row: tag v1, artifact type Image, a push time, a size of around 50 MB, and the same digest that
docker pushprinted. After a minute, the vulnerability column fills in. Choose the digest or the tag to see the findings, grouped by severity.A slim base image usually has a few Low or Medium findings and ideally no Critical ones. Findings in the base image are fixed by rebuilding on a newer base image, not by changing your code.
Try to overwrite v1
Leo’s quick fix: the same app, built again with a different version label.
Terminaldocker build --platform linux/amd64 --provenance=false --build-arg APP_VERSION=v1-fix -t order-service:v1-fix .Part What it means docker buildbuild the image again --platform linux/amd64for AWS’s processors, as before --provenance=falseone plain image, as before --build-arg APP_VERSION=v1-fixstamp it with a different version, so it’s a different image with a different digest -t order-service:v1-fixits local name .this folder Now give it the name
v1in ECR — the mistake from the story:Terminaldocker tag order-service:v1-fix 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1Part What it means docker taggive an image another name order-service:v1-fixthe new build 111122223333.dkr.ecr.us-east-1.amazonaws.comyour registry /quickcart-orders:v1the tag that already exists in ECR Terminaldocker push 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1Part What it means docker pushtry to upload it as v1ECR refusestag invalid: The image tag 'v1' already exists in the 'quickcart-orders' repository and cannot be overwritten because the repository is immutable.That refusal is the point. On a mutable repository this push would have worked, silently.
Push the fix properly, as v2
Terminaldocker tag order-service:v1-fix 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v2Part What it means docker taggive the fix a new, unused tag quickcart-orders:v2the next version Terminaldocker push 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v2Part What it means docker pushupload it as v2Most layers are already thereThe push refers to repository [111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders] 5f70bf18a086: Layer already existsOnly the layers that changed are uploaded. The repository now shows two tags,
v1andv2, with two different digests.Pull by digest
In the console, copy the full digest of
v1— it starts withsha256:. Then ask for the image by that fingerprint instead of by its tag:Terminaldocker pull 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders@DIGESTPart What it means docker pulldownload an image quickcart-ordersfrom this repository @followed by a digest, instead of :followed by a tagDIGESTreplace this with the digest you copied, including sha256:You should seeDocker reports the digest and that the image is downloaded or already up to date. Whatever happens to any tag in future, this exact address always gives this exact image.
Add a lifecycle policy
In the AWS console: Amazon ECRPrivate registryRepositoriesquickcart-ordersLifecycle policyCreate rule
Create the first rule:
Field Enter Rule priority 1Rule description Expire untagged images after 7 daysImage status Untagged Match criteria Since image pushed Count number 7, unit daysSave it, choose Create rule again, and create the second:
Field Enter Rule priority 2Rule description Keep the newest 20 version tagsImage status Tagged (prefix matching) Tag prefixes vMatch criteria Image count more than Count number 20Save it. Behind the form, ECR stores the rules as JSON. You can see it on the repository’s lifecycle policy page, and this is what you would write in Terraform later:
lifecycle-policy.json{ "rules": [ { "rulePriority": 1, "description": "Expire untagged images after 7 days", "selection": { "tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 7 }, "action": { "type": "expire" } }, { "rulePriority": 2, "description": "Keep the newest 20 version tags", "selection": { "tagStatus": "tagged", "tagPrefixList": ["v"], "countType": "imageCountMoreThan", "countNumber": 20 }, "action": { "type": "expire" } } ] }Setting What it does rulePrioritythe order rules are checked in. Lower numbers first, and each must be different descriptiona note for people. ECR ignores it tagStatuswhich images the rule looks at: untagged,tagged(then give prefixes or patterns) oranytagPrefixListwith tagged: only images with a tag starting with one of thesecountTypesinceImagePushedmeans “older than”;imageCountMoreThanmeans “more than this many”countUnitandcountNumberhow many days, or how many images actionwhat to do with matching images. expireis the only choice, and it deletes themYou should seeTwo rules on the lifecycle policy page. Nothing is deleted yet: you have only two images, both tagged and both new. ECR applies the rules on its own schedule, usually within a day.
Clean-up step: Clean up
If you’re going on to ECS on Fargate, keep the repository and the
v1image. The next lesson runs them, and its clean-up deletes the repository. It costs pennies in the meantime.Otherwise, delete the repository and every image in it:
In the AWS console: Amazon ECRPrivate registryRepositoriestick quickcart-ordersDelete
Type
deleteto confirm, and choose Delete.Then remove the local copies from your computer. Each name you gave an image is removed separately:
Terminaldocker image rm order-service:v1 order-service:v1-fix 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v2Part What it means docker image rmremove images from this machine. It doesn’t touch ECR order-service:v1andorder-service:v1-fixthe two local names the two quickcart-ordersnamesthe ECR names you added with docker tagYou should seeOne
Untagged:line per name, thenDeleted:lines once the last name for an image is gone. The repository is gone from the ECR console.
Part 5: Check yourself
QuickCart's servers run on AWS. Why can't they just use the image that was built on Maya's laptop?
In an immutable repository, Leo pushes a fix with the tag v1, which already exists. What happens?
Why does the ECR login command use a pipe, with --password-stdin?
What is the difference between a tag and a digest?
The repository keeps every image ever pushed. What stops it growing forever?
Part 6: Cheat sheet
| Command or setting | What it does |
|---|---|
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin REGISTRY | Log Docker in to ECR for 12 hours |
| Registry address | ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com |
docker build --platform linux/amd64 --provenance=false -t name:v1 . | Build one plain image for AWS’s processors |
docker tag name:v1 REGISTRY/repo:v1 | Give the image its ECR name |
docker push REGISTRY/repo:v1 | Upload it |
repo@sha256:… | Pull one exact image by its digest |
| Immutable tags | A tag can never be moved to another image |
| Scan on push | Check every image for known vulnerabilities when it arrives |
| Lifecycle policy | Expire old or untagged images automatically |
no basic auth credentials | Not logged in. Run the login again |
name unknown | The repository doesn’t exist, or the name is misspelled |