14.1
Docker, Containers vs VMs, and ECR
What a container is, build-push-pull-run, and storing images in ECR.
18 min Reading + animations
Tip: tap or hover any dotted-underlined term to see what it means.
By the end of this lesson you will be able to
- Explain what Docker is and why “it works on my machine” stops being a problem once an application ships as a container.
- Tell an image (the frozen package) from a container (a running copy of it), read a Dockerfile, and explain image layers and the build cache.
- Follow the build → push → pull → run cycle, and name the registries an image can live in: Docker Hub, Amazon ECR private and ECR Public.
- Compare containers with virtual machines: shared kernel, start time, density and isolation.
- Run Docker by hand on EC2, and explain why that breaks down at a hundred containers and why orchestration (ECS, EKS) exists.
- Place ECS, EKS, Fargate, ECR and App Runner on one map of the container services on AWS.
- Use Amazon ECR well: repositories and URIs, IAM and repository policies,
get-login-password, tags vs digests, tag immutability, basic vs enhanced scanning, lifecycle policies, replication and pull through cache. - Pull images from a private subnet with VPC endpoints (or a NAT gateway), and troubleshoot
CannotPullContainerError.
#Why containers: “it works on my machine”
A developer at a Dhaka e-commerce shop finishes the new checkout API on her laptop: Node.js 22, a particular version of an image library, a config file in a particular folder. It works. The operations team installs it on the servers, where Node.js 18 is installed and the image library is a different version. It crashes. Two days before the Eid sale, two teams are arguing about whose machine is “right”.
The application was never just the code. It was the code plus its runtime, its libraries, its settings and the operating system files it expects. A container ships all of that together.
#What Docker is
Docker is a platform for building, shipping and running applications in containers. Released in 2013, it made a set of Linux kernel features (namespaces to isolate what a process can see, cgroups to limit how much CPU and memory it may use) easy enough for every developer to use. Since 2015 the formats are open standards of the OCI (Open Container Initiative), so an image built with Docker also runs on containerd, Podman, ECS, Kubernetes or Lambda.
What you get:
- Runs the same everywhere: any machine with a container runtime, no compatibility issues, predictable behaviour.
- Any language, any stack: Node.js, Java, Python, PHP, Go, .NET; the runtime only sees a container.
- Less work to deploy and maintain: one artefact moves from laptop to test to production unchanged.
- Fast and light: a container starts in about a second and many fit on one server (see “Docker vs virtual machines” below).
Typical uses: microservices (each service in its own container, deployed on its own schedule), lift-and-shift of on-premises applications to AWS without rewriting them, batch jobs, CI build environments, and machine learning models packaged with their exact library versions.
#Image and container are not the same thing
The notes use “image” and “container” almost interchangeably. Keep them apart; the exam and every error message do.
| Image | Container | |
|---|---|---|
| What it is | A read-only package: file system layers plus metadata (which command to start, which port, which environment variables) | A running (or stopped) instance of an image: a process with its own isolated file system, network and limits |
| Where it lives | In a registry (Docker Hub, ECR) and in each host’s local image cache | On one host, while it exists |
| Changes? | Never. A change means a new image with a new digest | Writes go to a thin writable layer of its own |
| How many | One image… | …can run as many containers as you like, on many hosts |
| Lifetime | Until you delete it from the registry | Until it is removed; then its writable layer is gone |
A programmer’s comparison: the image is the class, the container is the object. A cook’s comparison: the image is the sealed recipe box with ingredients measured out; a container is one pot of biryani cooked from it. You can cook ten pots from one box, and spilling one pot does not change the box.
The last row matters in production: anything an application writes inside its container (uploaded files, a local database file, logs on disk) disappears when the container is replaced. Persistent data belongs in a volume or a managed service: EFS, EBS, S3 or a database (lesson 14.2 covers volumes for ECS tasks).
Self-checkQuestion
An operations engineer says: we deployed the same image to 12 servers, so there are 12 images in production. What is the accurate statement?
#The Dockerfile
An image is built from a Dockerfile, a short text file of instructions. The checkout API’s Dockerfile:
# 1. Base image: Linux files + Node.js 22, from a registry
FROM public.ecr.aws/docker/library/node:22-alpine
WORKDIR /app
# 2. Dependencies first: this layer is rebuilt only when package files change
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# 3. The application code: changes on every commit
COPY src/ ./src/
# 4. Metadata: which port the app listens on, which user, what to start
EXPOSE 3000
USER node
CMD ["node", "src/server.js"]
FROMpicks a base image (here the official Node.js image, served from ECR Public instead of Docker Hub).COPYandRUNadd files and run commands; each one creates a layer.EXPOSE,USER,CMDadd metadata: they do not add files.
#Layers and the build cache
Each file-changing instruction produces a read-only layer, a set of file changes stacked on the previous one. Layers are identified by a hash of their contents, which gives three big savings:
- Build cache. When you rebuild after changing only
src/, Docker reuses the base and dependency layers and rebuilds only the last ones. That is why the Dockerfile copiespackage.jsonbefore the code: the slownpm cilayer is reused on most builds. - Push and pull only what changed. A registry already holding the base and dependency layers says “layer already exists”; only the new code layer (a few MB) travels. A host that already has the old version downloads only that layer.
- Shared on disk. Ten containers from images that share a base layer store that base once per host.
When a container starts, the runtime puts one thin writable layer on top of the image’s read-only layers. That layer, and only that layer, belongs to the container, and it is deleted with it.
#Build, push, pull, run
The whole Docker workflow fits in four verbs:
Build
docker buildturns the Dockerfile and your source folder into an image on your machine (or in a CI pipeline).Push
docker pushuploads the image’s layers and its manifest to a repository in a registry.Pull
On any host,
docker pulldownloads the manifest, then every layer the host does not have yet.Run
docker runcreates a container from the image: a writable layer, an isolated process, network and limits, and starts the command fromCMD.
Build, push, pull, run
The checkout API of a Dhaka online shop goes from a developer’s laptop to an EC2 host through Amazon ECR. Watch which bytes travel at each step, and what the layer cache saves.
Scenario
The four verbs of Docker, from Dockerfile to running container.
The developer writes a Dockerfile: start from the Node.js 22 base image, copy package.json, install dependencies, copy the code, start server.js.
#Where images are stored: registries
A registry is a server that stores images; a repository is one named collection inside it (one per application, usually), holding many versions of that image identified by tags and digests.
| Registry | Who uses it | Notes |
|---|---|---|
| Docker Hub (hub.docker.com) | Everyone; the default for the Docker CLI | Mostly public repositories, with official base images for many technologies and operating systems (Ubuntu, MySQL, Node.js, nginx). Pulls by anonymous and free users are rate-limited per IP address |
| Amazon ECR, private | Your AWS account | Private repositories secured by IAM, in each Region of your account; integrated with ECS, EKS, Lambda, App Runner |
| Amazon ECR Public (gallery.ecr.aws) | Anyone can pull, you publish | Public repositories under public.ecr.aws/<alias>/; AWS and many vendors publish there, including copies of Docker’s official images (public.ecr.aws/docker/library/…) |
| Others | GitHub Container Registry, Quay, Azure Container Registry, self-hosted registries… |
The full name of an image tells you exactly where it lives:
| Part | Example |
|---|---|
| Registry (account + Region) | 111122223333.dkr.ecr.ap-southeast-1.amazonaws.com |
| Repository | checkout-example |
| Tag (a movable label) | :1.4.2 |
| or digest (the content hash) | @sha256:9f86d081884c7d65… |
So the checkout image is 111122223333.dkr.ecr.ap-southeast-1.amazonaws.com/checkout-example:1.4.2. A name with no registry, such as nginx, means Docker Hub (docker.io/library/nginx), and a name with no tag means the tag latest.
#Docker vs virtual machines
Docker is sort of a virtualization technology, but not quite.
Virtual machinesContainers
- A hypervisor on the physical server runs several guest operating systems, each with its own kernel.
- Each VM boots a whole OS: minutes to start, GBs of disk and hundreds of MB of RAM before your app runs.
- Strong isolation: a separate kernel per VM.
- This is what an EC2 instance is: a VM on the AWS Nitro hypervisor.
- A container runtime (the Docker daemon) on one host OS runs isolated processes that share the host’s kernel.
- No OS to boot: a container starts in about a second, and an image is often tens of MB.
- Many containers per server: the host’s resources are shared, so density is much higher.
- Weaker isolation: all containers trust the same kernel. Do not mix mutually untrusted tenants on one host without extra isolation.
On AWS you usually get both: your containers run on EC2 instances (VMs), and Docker packs many containers onto each instance. Fargate takes this further: every Fargate task runs in its own isolated micro-VM that shares no kernel with other tasks, so you get container convenience with VM-level isolation (lesson 14.2).
Virtual machines, containers and image layers
What sits between the hardware and your application in each model, and what an image is made of.
Scenario
A physical server with an operating system. In AWS this layer is AWS’s, inside the Nitro system.
#Docker by hand on EC2
#The empty plot of land
To run containers on an instance yourself:
# On an Amazon Linux 2023 instance (SSH or Session Manager)
sudo dnf install -y docker
sudo systemctl enable --now docker # start the Docker daemon, now and at boot
sudo usermod -aG docker ec2-user # let ec2-user run docker without sudo
# Log in to ECR (the instance role must allow ECR pulls), pull and run
aws ecr get-login-password --region ap-southeast-1 | docker login --username AWS \
--password-stdin 111122223333.dkr.ecr.ap-southeast-1.amazonaws.com
docker run -d --name checkout -p 80:3000 --restart unless-stopped \
111122223333.dkr.ecr.ap-southeast-1.amazonaws.com/checkout-example:1.4.2
-p 80:3000 maps port 80 of the host to port 3000 in the container. --restart unless-stopped asks the Docker daemon to restart the container if its process exits. For one small application on one instance, this is fine. (Elastic Beanstalk’s Docker platform, lesson 9.4, automates exactly this.)
#Why it breaks down at 100 containers
Now the shop has 12 microservices, 100 containers spread over 10 instances, and an Eid sale coming. By hand, someone must:
- Place each container: which instance has free CPU and memory, and which host ports are still free?
- Restart crashes.
--restartrestarts a process that exits on the same host, but it does not know whether the app is healthy (a frozen app keeps running), and it can do nothing when the host itself dies: every container on that instance is gone and nobody starts them elsewhere. - Deploy new versions without downtime: stop and start 100 containers in the right order, and roll back if the new version fails.
- Scale with traffic: more containers before the sale, fewer after.
- Wire up the load balancer: register each new container’s host and port, deregister dead ones.
- Hand out secrets, logs and permissions to each container.
Doing this by logging in to servers is impossible to keep up with, and it fails exactly at 3 a.m. during a sale. The answer is orchestration: you declare what should run (“9 copies of checkout:1.4.2, each with 0.5 vCPU and 1 GiB, behind this load balancer”), and an orchestrator keeps reality equal to that declaration, around the clock. AWS offers two orchestrators: ECS and EKS.
100 containers by hand, or with an orchestrator
The shop runs its checkout containers on three EC2 hosts before the Eid sale. First you manage them yourself over SSH; then ECS keeps them running for you.
Scenario
Fine for one app on one instance.
EC2 is an empty plot of land. You SSH in, install Docker with dnf, start the daemon, and log in to ECR.
#Containers on AWS: the menu
| Service | What it is | When you pick it | Lesson |
|---|---|---|---|
| Amazon ECS (Elastic Container Service) | AWS’s own container orchestrator | Run containers with the deepest AWS integration and the least to learn | 14.2, 14.3 |
| Amazon EKS (Elastic Kubernetes Service) | Managed Kubernetes (open source) | You already use Kubernetes, want its tooling, or want to avoid lock-in | 14.4 |
| AWS Fargate | Serverless compute for containers: no EC2 instances to manage | Works with ECS and with EKS: you size the task, AWS runs it | 14.2, 14.4 |
| Amazon ECR (Elastic Container Registry) | Stores and scans container images | Every one of the above pulls from it | This lesson |
| AWS App Runner | Source code or an image → an HTTPS web service, no cluster at all | The simplest end of the spectrum; check its current availability | 14.4 |
Two more places run containers: Elastic Beanstalk with its Docker platforms (lesson 9.4), and AWS Lambda, which can run a function packaged as a container image of up to 10 GB, pulled from ECR (chapter 15).
Think of it as layers: ECR stores the image; ECS or EKS decides what runs where and keeps it running; EC2 or Fargate provides the compute underneath.
#Amazon ECR: AWS’s own Docker Hub
Amazon Elastic Container Registry (ECR) is a fully managed registry for Docker and OCI images (and other OCI artifacts such as Helm charts and signatures). The notes call it AWS’s own Docker Hub, which is a good start, with two corrections: it has private and public repositories, and it does much more than store.
#Private registry, repositories and storage
- Every AWS account has one private registry per Region:
<account>.dkr.ecr.<region>.amazonaws.com. Inside it you create repositories (checkout-example,payments/ledger-example…). - Private repositories are reachable only with IAM credentials.
- Public repositories live in ECR Public and are listed in the ECR Public Gallery (gallery.ecr.aws). Anyone can pull them anonymously; you publish open-source tools or public SDK images there. They are created through the
us-east-1API, whatever Region your compute is in. - Image layers are stored in Amazon S3 behind the scenes, so they inherit S3’s durability, and they are encrypted at rest (AES-256 by default, or a KMS key you choose when the repository is created; it cannot be changed later).
- You pay for storage (private: about USD 0.10 per GB-month) and for data transferred out of the Region. Pulls by EC2, Fargate, Lambda or App Runner in the same Region are free. ECR Public gives 50 GB of storage free each month.
#Access control: IAM, repository policies and the login token
ECR is “secured by IAM”, as the notes say. In practice there are three kinds of policy and one token:
| Mechanism | Attached to | Typical use |
|---|---|---|
| IAM identity policy | A user or role: a CI role, an instance role, an ECS task execution role, an EKS node role | The CI role may push; runtime roles may only pull. AWS managed policies: AmazonEC2ContainerRegistryPullOnly (pull only, since 2024), AmazonEC2ContainerRegistryReadOnly, AmazonEC2ContainerRegistryPowerUser |
| Repository policy (resource-based) | One repository | Let another account pull (a prod account pulling from a shared tooling account), or let a service such as Lambda pull |
| Registry policy | The whole private registry | Allow cross-account replication into it, or scope pull through cache |
The Docker CLI does not speak IAM. So you first call aws ecr get-login-password, which calls the GetAuthorizationToken API and returns a token valid for 12 hours, scoped to the permissions of the IAM principal that asked for it, and pipe it into docker login --username AWS. ECS, EKS and Lambda do this for you with their roles. If a pipeline that has run for 13 hours suddenly fails to push, the token expired.
A pull needs three actions: ecr:GetAuthorizationToken (on *), then ecr:BatchGetImage (the manifest) and ecr:GetDownloadUrlForLayer (the layers) on the repository. A push needs the upload actions too. When something is refused, the slides’ rule holds: permission errors mean a policy is missing, either IAM on the caller or the repository policy for another account.
#Tags, digests and tag immutability
A tag (1.4.2, latest, prod) is a movable label that points to an image. A digest (sha256:…) is the hash of the image manifest itself: it never changes and can never point to anything else.
By default, ECR tags are mutable: pushing a new image as 1.4.2 silently moves the tag, and the old image becomes untagged. That causes nasty surprises: two tasks of the same service, started an hour apart, run different code although both say 1.4.2.
Turn on tag immutability for the repository (--image-tag-mutability IMMUTABLE): pushing an existing tag again fails with ImageTagAlreadyExistsException, so 1.4.2 means the same bytes forever. Rules of thumb:
- Tag images with something unique: a version, the Git commit hash, a build number. Do not deploy
latest; it is just a tag, not “the newest”. - For full certainty, deploy by digest (
checkout-example@sha256:…). ECS already resolves a tag to a digest when a deployment starts, so all tasks of one deployment run the same image. - A newer option lets an immutable repository exclude a few tags by wildcard (for example keep
latestmovable) withIMMUTABLE_WITH_EXCLUSION, and a mutable repository protect a few tags the other way round.
#Image scanning: basic and enhanced
The notes’ exam trick: “store Docker images privately and scan for vulnerabilities” → Amazon ECR. ECR has two kinds of scanning, configured at the registry level with repository filters:
| Basic scanning | Enhanced scanning | |
|---|---|---|
| Engine | ECR’s own, AWS native technology with a CVE database | Amazon Inspector |
| What it checks | Operating system packages | OS packages and programming language packages (npm, pip, Maven…) |
| When | On push, or manually (each image at most once per 24 hours) | On push and continuously: images are rescanned when new CVEs are published |
| Findings | In ECR (console, describe-image-scan-findings), plus an EventBridge event when a scan completes | In Inspector and ECR, an EventBridge event for each new finding, Security Hub |
| Cost | Free | Paid, through Inspector |
Basic scanning is a snapshot: an image that was clean on the day it was pushed is not re-checked when a new vulnerability appears next week. Enhanced scanning keeps watching, which is what a payment company needs for images that stay in production for months. Inspector itself is covered in lesson 21.7.
A scan does not block anything by itself. To stop a vulnerable image from reaching production, let the pipeline read the findings (or react to the EventBridge event) and fail the deployment on critical or high findings.
#Lifecycle policies
A CI pipeline that pushes on every commit fills a repository with thousands of old images, and storage is billed per GB-month. A lifecycle policy cleans up automatically, with rules such as:
- expire untagged images older than 7 days (the leftovers of moved tags);
- keep only the last 30 images tagged
prod-*(imageCountMoreThan); - expire images tagged
dev-*14 days after they were pushed (sinceImagePushed).
Rules are evaluated together and applied by priority (lower number wins); expired images are removed within 24 hours of matching, and each action is logged in CloudTrail. Always run the lifecycle policy preview first: a careless rule can expire the image production still runs.
#Replication: cross-Region and cross-account
ECR can replicate images from your private registry to other Regions and other accounts. You configure it once at the registry level (up to 25 destinations, optionally only for repositories with a given name prefix):
- Cross-Region: the Singapore registry copies every new image to Mumbai, so a disaster-recovery cluster there pulls locally and fast, with no cross-Region transfer at launch time.
- Cross-account: the tooling account where CI pushes copies images into the production account. The destination registry must have a registry policy allowing the source account to replicate (
ecr:ReplicateImage, andecr:CreateRepositoryif repositories should be created automatically).
Things to remember: only images pushed after replication is configured are copied (older ones need a backfill); repository names stay the same; most images arrive within 30 minutes; repository policies and lifecycle policies are not replicated (use repository creation templates to give new repositories the right settings); and replication never deletes anything in the destination.
#Pull through cache
A pull through cache rule makes your private registry a caching proxy for an upstream registry. You pull 111122223333.dkr.ecr.ap-southeast-1.amazonaws.com/docker-hub/library/nginx:1.27; the first time, ECR fetches the image from Docker Hub, stores it in a repository it creates for you, and returns it; later pulls come from your registry. ECR checks the upstream for a newer version of that tag at most once every 24 hours, and keeps serving the cached copy if the upstream is unreachable.
Supported upstreams include ECR Public, the Kubernetes registry and Quay (no credentials), Docker Hub, GitHub Container Registry, Azure Container Registry and others (credentials stored in a Secrets Manager secret whose name starts with ecr-pullthroughcache/), and another ECR registry. Why bother:
- Docker Hub rate limits. Hundreds of Fargate tasks behind one NAT gateway share one public IP, and Docker Hub counts anonymous pulls per IP: a scale-out during the sale can hit the limit and fail with
toomanyrequests. Through the cache, Docker Hub sees one pull a day. - Private subnets. After the first pull, tasks pull through the ECR VPC endpoints with no internet access (the very first pull needs a route to the internet).
- Scanning and control. Cached images live in your registry, so they are scanned and governed like your own.
What Amazon ECR does with an image
A CI pipeline pushes the checkout image to a private repository in Singapore. Follow scanning, tag immutability, replication, pull through cache and lifecycle cleanup.
Scenario
A one-time check of OS packages.
The pipeline pushes checkout:1.4.2. The repository has scan on push turned on, so ECR scans the image as soon as it arrives.
#Pulling images from a private subnet
Containers usually run in private subnets, with no public IP. To pull an image they still need to reach three things:
| Step | API | Reached at |
|---|---|---|
| 1. Get the login token | GetAuthorizationToken | ECR API, com.amazonaws.<region>.ecr.api |
| 2. Get the image manifest | BatchGetImage | Docker registry API, com.amazonaws.<region>.ecr.dkr |
| 3. Download the layers | GetDownloadUrlForLayer gives a URL in Amazon S3 | S3 |
There are two ways to give a private subnet that reach (lesson 3.4 and lesson 22.2):
- A NAT gateway in a public subnet. Simple, and it also reaches Docker Hub and other registries. But every byte of every layer goes through it and is billed per GB processed, and the subnet has outbound internet access, which some security teams forbid.
- VPC endpoints, no internet at all: two interface endpoints,
ecr.apiandecr.dkr(with private DNS turned on, and a security group allowing HTTPS on 443 from the tasks), plus an S3 gateway endpoint on the subnets’ route tables, because the layers come from S3. The S3 gateway endpoint is free and carries most of the bytes.
ECS adds a few more: Fargate tasks that send logs with the awslogs driver need the CloudWatch Logs endpoint (logs), and tasks that read secrets need the Secrets Manager or SSM endpoints; EC2 container instances also need the ECS endpoints to talk to the ECS service (lesson 14.2).
The classic mistake: someone creates the two ECR interface endpoints, removes the NAT gateway, and forgets the S3 gateway endpoint. Login and manifest work, then every layer download times out and the task stops with CannotPullContainerError.
Pulling an image from a private subnet
A Fargate task in a private subnet must reach three things to start: the ECR API for a token, the registry for the manifest, and S3 for the layers. See which routes work and which end in CannotPullContainerError.
Scenario
Works, but every layer byte is billed through NAT.
The task starts. With the task execution role, it asks the ECR API for a login token. The private subnet’s default route points to a NAT gateway, so the call goes out through it.
Try the combinations yourself:
Will this task pull its image?
A Fargate task of the checkout service must pull 111122223333.dkr.ecr.ap-southeast-1.amazonaws.com/checkout-example:1.4.2. Set up its network and permissions, then try the pull.
Subnet
NAT gateway route
ecr.api interface endpoint
ecr.dkr interface endpoint
S3 gateway endpoint
Execution role can pull from ECR
Tag 1.4.2 exists in the repository
Press “Try the pull” to follow the three steps.
#What goes wrong
| Symptom | Likely cause | Fix |
|---|---|---|
CannotPullContainerError … i/o timeout | No route to ECR or S3: private subnet without NAT or endpoints, or a security group on the interface endpoints that blocks 443 | NAT gateway, or the ecr.api + ecr.dkr interface endpoints and the S3 gateway endpoint |
| Login and manifest work, layer downloads time out | S3 gateway endpoint missing (layers are in S3) | Add the S3 gateway endpoint to the private route tables |
CannotPullContainerError … AccessDenied / 403 | The pulling role (execution role, node role, instance role) lacks GetAuthorizationToken, BatchGetImage or GetDownloadUrlForLayer; or another account has no repository policy | Attach a pull policy; add a repository policy for the other account |
CannotPullContainerError … not found | Wrong repository name, Region or tag; the image was expired by a lifecycle policy | Fix the URI; check the lifecycle preview; deploy by immutable tag or digest |
no matching manifest for linux/arm64 or exec format error | Image built only for x86_64 running on Graviton (arm64), or the other way round | Build a multi-architecture image (docker buildx --platform linux/amd64,linux/arm64) |
toomanyrequests from Docker Hub during a scale-out | Anonymous pull rate limit, all tasks sharing one NAT IP | ECR pull through cache, or base images from ECR Public |
no basic auth credentials on push, or a pipeline fails after 12 hours | Not logged in, or the 12-hour token expired | Run get-login-password before each push; use the ECR credential helper |
Push fails with ImageTagAlreadyExistsException | Tag immutability is on and the tag exists | Use a new tag per build (that is the point) |
| Two tasks of one service run different code with the same tag | Mutable tag overwritten between launches | Tag immutability; unique tags; deploy by digest |
| Uploaded files disappear after a deployment | Data written to the container’s writable layer | Volumes or managed storage (EFS, S3, a database) |
| Production image is still vulnerable a month after a clean scan | Basic scan is a one-time snapshot | Enhanced scanning (Inspector, continuous); rebuild and redeploy regularly |
| The registry bill keeps growing | Every CI build kept forever | Lifecycle policy: expire untagged and old builds (or archive) |
#Containers and ECR in system design
- One artefact, many environments. Build once, push once, and promote the same digest from test to staging to production by adding tags or replicating to the production account. What you tested is exactly what runs.
- Separate accounts for build and run. CI pushes to a tooling account; production pulls through cross-account replication or a repository policy with read-only access. A compromised production role can never push.
- Multi-Region. Replicate images to every Region you run in (or your DR Region), so a scale-out or failover never depends on another Region’s registry.
- Private by default. Tasks in private subnets with ECR and S3 endpoints, no NAT for image pulls, and base images through pull through cache.
- Secure supply chain. Scan on push, enhanced scanning for what runs, immutable tags, signing, and a lifecycle policy so old vulnerable images do not linger.
- When not to containerize. A single small script that runs on an event is often simpler as a Lambda function; a static website belongs in S3 and CloudFront.
#Try it yourself
Create a repository, build and push an image, scan it, and add a lifecycle policy. Output is shortened.
# 1. A private repository with immutable tags and scan on push
aws ecr create-repository --repository-name checkout-example \
--image-tag-mutability IMMUTABLE \
--image-scanning-configuration scanOnPush=true --region ap-southeast-1
# "repositoryUri": "111122223333.dkr.ecr.ap-southeast-1.amazonaws.com/checkout-example"
REG=111122223333.dkr.ecr.ap-southeast-1.amazonaws.com
# 2. Log Docker in to the registry (token valid 12 hours)
aws ecr get-login-password --region ap-southeast-1 | \
docker login --username AWS --password-stdin $REG
# Login Succeeded
# 3. Build for both CPU architectures, tag with a version, push
docker buildx build --platform linux/amd64,linux/arm64 \
-t $REG/checkout-example:1.4.2 --push .
# => pushing layers ... done
# => pushing manifest for .../checkout-example:1.4.2@sha256:9f86d081...
# 4. Pushing the same tag again is refused (immutability)
docker push $REG/checkout-example:1.4.2
# tag invalid: The image tag '1.4.2' already exists in the 'checkout-example' repository
# and cannot be overwritten because the repository is immutable.
# 5. Read the scan findings
aws ecr describe-image-scan-findings --repository-name checkout-example \
--image-id imageTag=1.4.2 --region ap-southeast-1
# "imageScanStatus": { "status": "COMPLETE" },
# "findingSeverityCounts": { "HIGH": 1, "MEDIUM": 3 }
# 6. Lifecycle: expire untagged after 7 days, keep the last 30 release images
aws ecr put-lifecycle-policy --repository-name checkout-example \
--lifecycle-policy-text file://lifecycle.json --region ap-southeast-1
# 7. Run it locally, then look at its layers
docker run -d -p 8080:3000 $REG/checkout-example:1.4.2
docker image history $REG/checkout-example:1.4.2
lifecycle.json:
{
"rules": [
{ "rulePriority": 1, "description": "Expire untagged after 7 days",
"selection": { "tagStatus": "untagged", "countType": "sinceImagePushed",
"countUnit": "days", "countNumber": 7 },
"action": { "type": "expire" } },
{ "rulePriority": 2, "description": "Keep the last 30 releases",
"selection": { "tagStatus": "tagged", "tagPatternList": ["1.*"],
"countType": "imageCountMoreThan", "countNumber": 30 },
"action": { "type": "expire" } }
]
}
For a private subnet, the three endpoints (replace the IDs with yours):
for svc in ecr.api ecr.dkr; do
aws ec2 create-vpc-endpoint --vpc-id vpc-0abc1234example --vpc-endpoint-type Interface \
--service-name com.amazonaws.ap-southeast-1.$svc --private-dns-enabled \
--subnet-ids subnet-0aaa1111example subnet-0bbb2222example \
--security-group-ids sg-0ccc3333example
done
aws ec2 create-vpc-endpoint --vpc-id vpc-0abc1234example --vpc-endpoint-type Gateway \
--service-name com.amazonaws.ap-southeast-1.s3 --route-table-ids rtb-0ddd4444example
#Summary
Key takeaways
- Docker packages code with its runtime, libraries and settings so it runs the same on any machine: the restaurant in a shipping container. Formats are open OCI standards.
- An image is a read-only, layered package built from a Dockerfile; a container is a running instance with its own writable layer, which is lost when the container is removed.
- Build → push → pull → run. Registries: Docker Hub (public, rate-limited), ECR private (IAM), ECR Public (gallery.ecr.aws). Layers are cached, so only changes are pushed and pulled.
- Containers vs VMs: containers share the host kernel, start in about a second and pack densely; VMs each run their own OS. EC2 is a VM; Fargate gives each task its own micro-VM.
- Docker by hand on EC2 (the empty plot of land) works for one app, not for 100 containers: no placement, no restart after a host failure, no rolling deploys, no scaling. Orchestration = ECS (AWS’s own) or EKS (Kubernetes), on EC2 or Fargate, pulling from ECR; App Runner is the simplest option.
- ECR: private and public repositories, stored in S3, encrypted, IAM identity policies plus repository policies (other accounts) and a registry policy (replication);
get-login-passwordgives a 12-hour token; permission errors mean a policy is missing. - Tags move, digests do not. Use tag immutability and unique tags; deploy by digest for certainty.
- Scanning: basic (OS packages, on push or manual) vs enhanced (Amazon Inspector, OS and language packages, continuous). “Store images privately and scan for vulnerabilities” → ECR.
- Lifecycle policies expire (or archive) old images; replication copies new images cross-Region and cross-account; pull through cache proxies Docker Hub and others.
- Private subnet pulls need
ecr.api+ecr.dkrinterface endpoints and an S3 gateway endpoint (or a NAT gateway). Otherwise:CannotPullContainerError.
Answer on your own first, then read the explanation, even when you got it right.
Self-checkQuestion
What problem does packaging an application as a container image solve first?
Self-checkQuestion
Which statement correctly separates an image from a container?
Self-checkQuestion
A developer changes one line of application code and rebuilds an image whose Dockerfile copies package.json and runs npm ci before copying the code. What happens on build and push?
Self-checkQuestion
Which name refers to an image on Docker Hub?
Self-checkQuestion
Which two statements about containers compared with virtual machines are correct?
Choose 2
Self-checkQuestion
You run one container per service on EC2 by hand with docker run --restart unless-stopped. At 3 a.m. the instance fails a hardware check and is stopped. What happens to its containers?
Self-checkQuestion
What does container orchestration (ECS or EKS) provide that docker run on EC2 does not?
Self-checkQuestion
A company needs serverless compute for containers that works with both ECS and EKS, with no EC2 instances to patch. Which service is it?
Self-checkQuestion
An exam question asks for a fully managed service to store Docker images privately and scan them for vulnerabilities. Which is it?
Self-checkQuestion
How long is the token from aws ecr get-login-password valid?
Self-checkQuestion
Which two statements about Amazon ECR are correct?
Choose 2
Self-checkQuestion
A team pushes checkout:1.4.2, then later pushes a different build with the same tag by mistake. ECS tasks started before and after the push run different code. What prevents this?
Self-checkQuestion
What is an image digest?
Self-checkQuestion
What does the tag latest mean in ECR?
Self-checkQuestion
A bank wants every container image that runs in production to be checked again whenever a new vulnerability is published, including vulnerabilities in npm and Python packages. Which configuration fits?
Self-checkQuestion
Which two are true of ECR basic scanning?
Choose 2
Self-checkQuestion
An image was scanned on push in March with no findings and has run in production since. In May, a critical OpenSSL CVE is published. With basic scanning only, what does ECR do?
Self-checkQuestion
A CI pipeline pushes an image on every commit, about 60 builds a day of 250 MB of new layers each, and nothing is ever deleted. Roughly how much storage does a month add, and what does it cost per month at USD 0.10 per GB-month once it is stored?
Self-checkQuestion
With a lifecycle policy that keeps only the last 30 builds (each with 250 MB of layers of its own) plus 1 GB of base layers they all share (stored once in the repository), about how much does the repository store, and what is the monthly storage cost at USD 0.10 per GB-month?
Self-checkQuestion
Production in account 444455556666 must pull images from a repository in the tooling account 111122223333. The production task execution role already allows ecr:BatchGetImage on that repository ARN, but pulls fail with AccessDenied. What is missing?
Self-checkQuestion
A company wants every image pushed in Singapore to be available automatically in Mumbai for a disaster-recovery cluster. What should it configure?
Self-checkQuestion
Which two statements about ECR replication are correct?
Choose 2
Self-checkQuestion
During an Eid sale, 300 Fargate tasks in private subnets behind one NAT gateway scale out and many fail with toomanyrequests while pulling nginx from Docker Hub. What is the best long-term fix?
Self-checkQuestion
Fargate tasks in a private subnet with no NAT gateway must pull from ECR. Which two kinds of endpoint are required? (Choose two.)
Choose 2
Self-checkQuestion
After a security review removed the NAT gateway, new ECS tasks fail. Logs show the login and the manifest request succeeded, but layer downloads time out. What was forgotten?
Self-checkQuestion
An ECS service’s tasks stop with CannotPullContainerError: AccessDenied. The subnet has a NAT gateway and the image URI is correct. What is the most likely cause?
Self-checkQuestion
A Fargate cluster pulls 400 GB of image layers a day through a NAT gateway billed at USD 0.045 per GB processed. About how much does the data processing cost per 30-day month, and how much of it would an S3 gateway endpoint save?
Self-checkQuestion
A new Graviton (arm64) node group starts failing with exec format error for an image that works on the old x86 instances. What is the fix?
Self-checkQuestion
A team wants an image stored in ECR that the whole internet can pull without AWS credentials, listed in a public catalog. What should it use?
Self-checkQuestion
Which statement about ECR is NOT true?