Laptop to Production

Storing images: ECR

Private images, scanning and tags that never change.

Time
35 minutes
Level
Beginner

Part 1: The problem

QuickCart, the first release on AWS

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 v1 quietly 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

WordWhat it isExample
RegistryThe whole store. In ECR, you get one per account per region111122223333.dkr.ecr.us-east-1.amazonaws.com
RepositoryOne app’s images, inside the registryquickcart-orders
TagA label on one image in a repositoryv1
DigestA fingerprint worked out from the image’s contentssha256:7c4e9a1f… (64 characters long)
Image URIThe full name you give Docker or ECS111122223333.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 forYou get
quickcart-orders:v1whatever v1 points to right now
quickcart-orders@sha256:7c4e9a1f…exactly that image, forever
quickcart-orders:latestwhatever 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.

Key point: Make tags immutable, so every version name means exactly one build, forever.

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.

SettingWhat it checksCost
Basic scanningThe operating system packages in the image, when it’s pushedFree
Enhanced scanningAlso your language libraries (like Flask), again whenever new problems are publishedPaid, 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:

RuleWhy
Expire untagged images 7 days after they were pushedNothing refers to them by name, and they pile up quietly
Keep only the newest 20 images whose tag starts with vEnough 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 tags

Repository quickcart-ordersTags: mutable

  • No images yet

Running on ECS

  • Nothing deployed yet
ReadyAn empty repository called quickcart-orders. Choose Next step to push the first build.

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:

Terminal
cd order-service
PartWhat it means
cdchange directory — every command below runs in here
order-servicethe folder with the app and its Dockerfile
  1. 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

    FieldEnter
    Repository namequickcart-orders
    Image tag mutabilityImmutable
    Encryption configurationAES-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 see

    quickcart-orders in the list, with Tag immutability: Immutable. The URI column shows 111122223333.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.

  2. 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 111122223333 with your account ID — it’s the number at the start of the URI you just copied.

    Terminal
    aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-east-1.amazonaws.com
    PartWhat 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 --password would put it
    111122223333.dkr.ecr.us-east-1.amazonaws.comthe registry to log in to: your account ID, dkr.ecr, then the region
    You should see
    Login Succeeded

    The 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.

  3. Build the image for AWS

    Terminal
    docker build --platform linux/amd64 --provenance=false -t order-service:v1 .
    PartWhat 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, tag v1
    .use the files in this folder
    The last lines look like
     => exporting to image
     => => naming to docker.io/library/order-service:v1

    The exact lines vary with your Docker version. The name at the end is what matters.

  4. 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:

    Terminal
    docker tag order-service:v1 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1
    PartWhat 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.

  5. Push it

    Terminal
    docker push 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1
    PartWhat 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 these
    The push refers to repository [111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders]
    5f70bf18a086: Pushed
    e3c9b0a7d41f: Pushed
    v1: digest: sha256:7c4e9a1fb03d6e2a8f5c1b9d0e7a3f6c2b8d4e1a9f0c7b3e6d2a5f8c1e4b7d0a size: 1994

    There’s one Pushed line 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.

  6. Look at the image and its scan

    In the AWS console: Amazon ECRPrivate registryRepositoriesquickcart-orders

    You should see

    One row: tag v1, artifact type Image, a push time, a size of around 50 MB, and the same digest that docker push printed. 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.

  7. Try to overwrite v1

    Leo’s quick fix: the same app, built again with a different version label.

    Terminal
    docker build --platform linux/amd64 --provenance=false --build-arg APP_VERSION=v1-fix -t order-service:v1-fix .
    PartWhat 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 v1 in ECR — the mistake from the story:

    Terminal
    docker tag order-service:v1-fix 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1
    PartWhat 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
    Terminal
    docker push 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1
    PartWhat it means
    docker pushtry to upload it as v1
    ECR refuses
    tag 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.

  8. Push the fix properly, as v2

    Terminal
    docker tag order-service:v1-fix 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v2
    PartWhat it means
    docker taggive the fix a new, unused tag
    quickcart-orders:v2the next version
    Terminal
    docker push 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v2
    PartWhat it means
    docker pushupload it as v2
    Most layers are already there
    The push refers to repository [111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders]
    5f70bf18a086: Layer already exists

    Only the layers that changed are uploaded. The repository now shows two tags, v1 and v2, with two different digests.

  9. Pull by digest

    In the console, copy the full digest of v1 — it starts with sha256:. Then ask for the image by that fingerprint instead of by its tag:

    Terminal
    docker pull 111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders@DIGEST
    PartWhat it means
    docker pulldownload an image
    quickcart-ordersfrom this repository
    @followed by a digest, instead of : followed by a tag
    DIGESTreplace this with the digest you copied, including sha256:
    You should see

    Docker 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.

  10. Add a lifecycle policy

    In the AWS console: Amazon ECRPrivate registryRepositoriesquickcart-ordersLifecycle policyCreate rule

    Create the first rule:

    FieldEnter
    Rule priority1
    Rule descriptionExpire untagged images after 7 days
    Image statusUntagged
    Match criteriaSince image pushed
    Count number7, unit days

    Save it, choose Create rule again, and create the second:

    FieldEnter
    Rule priority2
    Rule descriptionKeep the newest 20 version tags
    Image statusTagged (prefix matching)
    Tag prefixesv
    Match criteriaImage count more than
    Count number20

    Save 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" }
        }
      ]
    }
    SettingWhat 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) or any
    tagPrefixListwith tagged: only images with a tag starting with one of these
    countTypesinceImagePushed means “older than”; imageCountMoreThan means “more than this many”
    countUnit and countNumberhow many days, or how many images
    actionwhat to do with matching images. expire is the only choice, and it deletes them
    You should see

    Two 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.

  11. Clean-up step: Clean up

    If you’re going on to ECS on Fargate, keep the repository and the v1 image. 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 delete to confirm, and choose Delete.

    Then remove the local copies from your computer. Each name you gave an image is removed separately:

    Terminal
    docker 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:v2
    PartWhat it means
    docker image rmremove images from this machine. It doesn’t touch ECR
    order-service:v1 and order-service:v1-fixthe two local names
    the two quickcart-orders namesthe ECR names you added with docker tag
    You should see

    One Untagged: line per name, then Deleted: 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

On one card
Command or settingWhat it does
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin REGISTRYLog Docker in to ECR for 12 hours
Registry addressACCOUNT_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:v1Give the image its ECR name
docker push REGISTRY/repo:v1Upload it
repo@sha256:…Pull one exact image by its digest
Immutable tagsA tag can never be moved to another image
Scan on pushCheck every image for known vulnerabilities when it arrives
Lifecycle policyExpire old or untagged images automatically
no basic auth credentialsNot logged in. Run the login again
name unknownThe repository doesn’t exist, or the name is misspelled

Finished the build and the quiz? Mark it done to fill in its stop on the route map.