Chapter 66: Docker and Kubernetes Together: Dockerfile to Registry to Cluster, Jenkins and Kompose
66.2 Tag and push: local registry, Docker Hub and Amazon ECR
Why: the problem
An image on your laptop is invisible to a cluster. Kubernetes nodes pull images from a registry, so the next step is to give the image a full name and push it somewhere every node can reach. The full name has four parts: registry host, repository path, tag and, after pushing, a digest. 111122223333.dkr.ecr.ap-south-1.amazonaws.com/orders:1.0.0-a1b2c3d says: Amazon ECR in Mumbai, repository orders, tag 1.0.0-a1b2c3d. Tags are labels people choose, and they can move. latest is the worst tag in production: if Nykaa deploys orders:latest today and someone pushes a new latest tomorrow, a node that restarts a pod pulls different code than its neighbours, and nobody can tell which version is running. The fix is an immutable tag per build, usually the version plus the short Git commit, and ECR's tag immutability setting so that tag can never be overwritten. A digest, sha256 of the image manifest, is even stronger: it identifies the exact bytes, and Kubernetes can deploy image@sha256:... so nothing can change underneath you, as we saw in 64.2. Which registry? For learning, a local registry container wired to kind is fastest, because pushes stay on your laptop. kind load docker-image is another shortcut that copies an image straight into the nodes with no registry at all. Docker Hub is the public default and has pull rate limits for anonymous users. On AWS, Amazon ECR keeps images close to EKS nodes, scans them for vulnerabilities, and lets nodes pull through their IAM role without any password. Google Artifact Registry, Azure Container Registry, GitLab and Harbor work the same way. In this lesson we create the kind cluster with a local registry, tag with version and commit, push and read the digest, try kind load, then create an ECR repository with immutable tags and scanning, log in with a short-lived token and push.
What: every term in plain English
- Registry — Server that stores images, such as Docker Hub, ECR or a local registry:2 container.
- Repository — A named collection of image versions inside a registry, such as orders.
- Tag — Human label for an image version; can be moved unless the registry makes tags immutable.
- Digest — sha256 of the image manifest; identifies exact content and never changes.
- Fully qualified name — registry/repository:tag, for example localhost:5001/orders:1.0.0-a1b2c3d.
- kind load docker-image — Copies an image from your Docker into every kind node, skipping registries.
- Tag immutability — ECR setting that rejects pushing a tag that already exists.
- Scan on push — ECR scans each pushed image for known vulnerabilities.
- get-login-password — AWS CLI command that returns a 12-hour ECR token for docker login.
- Lifecycle policy — ECR rule that deletes old images, such as keep the last 30.
How: step by step
- Create a local registry and a kind cluster that trusts it. ch66 ready.
- Tag the image with version and commit. Immutable-style tag.
- Push to the local registry and read the digest. sha256 shown.
- Try kind load as a shortcut. Image on every node.
- Create an ECR repository with immutability and scanning. Repo ready.
- Log in to ECR with a short-lived token. Login Succeeded.
- Push to ECR and read scan results. Digest and findings.
- Add a lifecycle policy. Old images cleaned.
A simple rule keeps registries sane across teams: one repository per service, one immutable tag per commit, and promotion by copying the same digest rather than rebuilding. When Nykaa promotes catalog from staging to production, CI does not run docker build again; it pulls the tested digest and pushes it to the production repository or simply reuses it, so what was tested is byte for byte what goes live. Rebuilding for production silently picks up new base image layers and new pip packages, which is how 'it worked in staging' bugs are born. Pair this with a short lifecycle policy, scanning on push, and a pull-through cache for public base images, and the registry stops being a source of surprises during incidents. Write the digest into the release notes and the deployment annotation so anyone on call can match a running pod to its exact build in seconds.
Example 1: Local registry wired to kind
Goal. Local registry wired to kind.
kind-ch66.sh
docker run -d --restart=always -p 127.0.0.1:5001:5000 --name kind-registry registry:2
printf '%s\n' \
'kind: Cluster' \
'apiVersion: kind.x-k8s.io/v1alpha4' \
'name: ch66' \
'containerdConfigPatches:' \
' - |-' \
' [plugins."io.containerd.grpc.v1.cri".registry]' \
' config_path = "/etc/containerd/certs.d"' \
'nodes:' \
' - role: control-plane' \
' - role: worker' \
' - role: worker' > kind-ch66.yaml
kind create cluster --config kind-ch66.yaml
for n in $(kind get nodes --name ch66); do
docker exec $n mkdir -p /etc/containerd/certs.d/localhost:5001
printf '[host."http://kind-registry:5000"]\n' | docker exec -i $n cp /dev/stdin /etc/containerd/certs.d/localhost:5001/hosts.toml
done
docker network connect kind kind-registry
What each line does
registry:2— The official open-source registry, bound to 127.0.0.1 only.config_path— Tells containerd in each node to read per-registry settings from certs.d.hosts.toml— Maps localhost:5001 inside the node to the registry container on the kind network.printf into files— No heredocs; each line is one argument.docker network connect— Puts the registry on the same Docker network as the nodes.
Green output
$ bash kind-ch66.sh
Creating cluster "ch66" ...
✓ Ensuring node image (kindest/node:v1.34.0)
Set kubectl context to "kind-ch66"
$ kubectl get nodes
NAME STATUS ROLES VERSION
ch66-control-plane Ready control-plane v1.34.0
ch66-worker Ready none v1.34.0
ch66-worker2 Ready none v1.34.0
Expected result. Your laptop pushes to localhost:5001 and every node pulls the same name from the registry container, so manifests use one image name everywhere.
Example 2: Tag with version and commit, push, read the digest, try kind load
Goal. Tag with version and commit, push, read the digest, try kind load.
push-local.sh
cd orders-app
git init -q 2>/dev/null; git add -A && git commit -qm "orders 1.0.0" 2>/dev/null
SHA=$(git rev-parse --short HEAD)
TAG=1.0.0-$SHA
docker build -t localhost:5001/orders:$TAG .
docker push localhost:5001/orders:$TAG
docker inspect --format '{{index .RepoDigests 0}}' localhost:5001/orders:$TAG
curl -s http://localhost:5001/v2/orders/tags/list
kind load docker-image orders:dev --name ch66
docker exec ch66-worker crictl images | grep orders
What each line does
1.0.0-SHA— Semantic version for people plus the commit for traceability.docker push— Uploads only layers the registry does not have yet.RepoDigests— The sha256 digest the registry assigned to this exact manifest./v2/.../tags/list— Registry HTTP API listing the tags of a repository.kind load— Copies orders:dev into all three nodes; good for quick tests, but pods must use imagePullPolicy IfNotPresent.
Green output
$ bash push-local.sh
The push refers to repository [localhost:5001/orders]
5f70bf18a086: Pushed
1.0.0-a1b2c3d: digest: sha256:7d3c9a1b2e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b size: 1782
localhost:5001/orders@sha256:7d3c9a1b2e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b
{"name":"orders","tags":["1.0.0-a1b2c3d"]}
Image: "orders:dev" with ID "sha256:4c1d9e2f7a10..." not yet present on node "ch66-worker", loading...
docker.io/library/orders dev 4c1d9e2f7a10 171MB
Expected result. The image now has a traceable tag and a fixed digest in a registry the cluster can reach, and kind load showed the no-registry shortcut. In 66.3 we deploy by that tag.
Example 3: Docker Hub: log in with a token, tag and push
Goal. Docker Hub: log in with a token, tag and push.
dockerhub.sh
DOCKERHUB_USER=ravi-demo
export DOCKERHUB_TOKEN=REPLACE_WITH_ACCESS_TOKEN
printf '%s' "$DOCKERHUB_TOKEN" | docker login --username "$DOCKERHUB_USER" --password-stdin
docker tag localhost:5001/orders:1.0.0-a1b2c3d docker.io/$DOCKERHUB_USER/orders:1.0.0-a1b2c3d
docker push docker.io/$DOCKERHUB_USER/orders:1.0.0-a1b2c3d
docker buildx imagetools inspect docker.io/$DOCKERHUB_USER/orders:1.0.0-a1b2c3d | head -3
docker logout
What each line does
DOCKERHUB_TOKEN— A personal access token from Docker Hub account settings, never your password; the value here is a placeholder.--password-stdin— Keeps the token out of shell history and the process list.docker tag— Adds a second name, docker.io/USER/orders, pointing at the same image ID.imagetools inspect— Confirms the pushed tag and digest on Docker Hub.docker logout— Removes the stored credential from ~/.docker/config.json on shared machines.
Green output
$ bash dockerhub.sh
Login Succeeded
The push refers to repository [docker.io/ravi-demo/orders]
1.0.0-a1b2c3d: digest: sha256:7d3c9a1b2e4f... size: 1782
Name: docker.io/ravi-demo/orders:1.0.0-a1b2c3d
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:7d3c9a1b2e4f...
Removing login credentials for https://index.docker.io/v1/
Expected result. Same image, same digest, now public on Docker Hub. A Kubernetes node can pull it with no secret if the repository is public; for a private repository create an imagePullSecret from the same token.
Example 4: Amazon ECR: immutable tags, scanning, login and push
Goal. Amazon ECR: immutable tags, scanning, login and push.
ecr.sh
REGION=ap-south-1
ACCOUNT=111122223333
REPO=$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/orders
aws ecr create-repository --repository-name orders --region $REGION \
--image-tag-mutability IMMUTABLE \
--image-scanning-configuration scanOnPush=true \
--encryption-configuration encryptionType=KMS
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ACCOUNT.dkr.ecr.$REGION.amazonaws.com
docker tag localhost:5001/orders:1.0.0-a1b2c3d $REPO:1.0.0-a1b2c3d
docker push $REPO:1.0.0-a1b2c3d
docker push $REPO:1.0.0-a1b2c3d
aws ecr describe-image-scan-findings --repository-name orders --image-id imageTag=1.0.0-a1b2c3d --region $REGION --query "imageScanFindings.findingSeverityCounts"
lifecycle.json
{
"rules": [
{
"rulePriority": 1,
"description": "keep last 30 images",
"selection": {"tagStatus": "any", "countType": "imageCountMoreThan", "countNumber": 30},
"action": {"type": "expire"}
}
]
}
What each line does
IMMUTABLE— A second push of the same tag is rejected, so 1.0.0-a1b2c3d always means the same image.scanOnPush— Each new image is checked for known CVEs.--password-stdin— The 12-hour token never appears in shell history or process lists.Second push— Shows the immutability rule in action.lifecycle.json— Keeps storage cost flat by expiring older images.
Green output
$ bash ecr.sh
"repositoryUri": "111122223333.dkr.ecr.ap-south-1.amazonaws.com/orders"
Login Succeeded
1.0.0-a1b2c3d: digest: sha256:7d3c9a1b2e4f... size: 1782
tag invalid: The image tag '1.0.0-a1b2c3d' already exists in the 'orders' repository and cannot be overwritten because the repository is immutable.
{
"LOW": 6,
"MEDIUM": 2
}
$ aws ecr put-lifecycle-policy --repository-name orders --lifecycle-policy-text file://lifecycle.json --region ap-south-1
"lifecycle": "keep last 30 images"
Expected result. ECR accepted the first push, refused to overwrite the tag, reported no critical findings and will keep only the newest 30 images. On EKS, nodes pull from here through their IAM role with no password in the cluster.
Every command, step by step (kind lab with kubectl)
-
Registry
docker run -d -p 127.0.0.1:5001:5000 --name kind-registry registry:2What it does: Local.
Status: Downloaded newer image for registry:2 -
Cluster
kind create cluster --config kind-ch66.yamlWhat it does: Three nodes.
Set kubectl context to "kind-ch66" -
Network
docker network connect kind kind-registryWhat it does: Reachable.
(no output) -
Tag
docker build -t localhost:5001/orders:1.0.0-$SHA .What it does: Versioned.
naming to localhost:5001/orders:1.0.0-a1b2c3d -
Push
docker push localhost:5001/orders:1.0.0-a1b2c3dWhat it does: Uploaded.
digest: sha256:7d3c9a1b... -
Tags
curl -s http://localhost:5001/v2/orders/tags/listWhat it does: Listed.
{"name":"orders","tags":["1.0.0-a1b2c3d"]} -
kind load
kind load docker-image orders:dev --name ch66What it does: Copied.
loading... -
ECR repo
aws ecr create-repository --repository-name orders --image-tag-mutability IMMUTABLE ...What it does: Created.
"repositoryName": "orders" -
Login
aws ecr get-login-password | docker login --username AWS --password-stdin ...What it does: Token.
Login Succeeded -
Scan
aws ecr describe-image-scan-findings --repository-name orders --image-id imageTag=1.0.0-a1b2c3dWhat it does: Findings.
"MEDIUM": 2
When it is red 1
http: server gave HTTP response to HTTPS client
Cause. containerd treats the registry as HTTPS.
Fix, step by step
- Add hosts.toml with an http host for localhost:5001.
When it is red 2
denied: requested access to the resource is denied
Cause. Not logged in, wrong account in the name, or token expired after 12 hours.
Fix, step by step
- Run get-login-password again.
- Check the account ID and region in the name.
When it is red 3
toomanyrequests: You have reached your pull rate limit
Cause. Anonymous Docker Hub pulls.
Fix, step by step
- Log in, use a pull-through cache in ECR, or mirror base images.
When it is red 4
tag invalid ... cannot be overwritten because the repository is immutable
Cause. Pushing an existing tag.
Fix, step by step
- Expected; build a new tag from a new commit.
When it is red 5
ErrImageNeverPull or pull from docker.io after kind load
Cause. Pod uses imagePullPolicy Always or latest tag.
Fix, step by step
- Use a fixed tag and imagePullPolicy IfNotPresent.
Practice tasks
Easy Task 1: Registry
Why: Local.
What to do: Start registry:2 on 127.0.0.1:5001.
- --restart=always.
Solution
docker ps --filter name=kind-registryEasy Task 2: Cluster
Why: kind.
What to do: Create ch66 with hosts.toml.
- printf.
Solution
kubectl get nodesEasy Task 3: Push
Why: Local.
What to do: Tag with commit and push.
- git rev-parse.
Solution
curl -s http://localhost:5001/v2/orders/tags/listMedium Task 4: Digest
Why: Read.
What to do: Print RepoDigests.
- docker inspect.
Solution
docker inspect --format '{{index .RepoDigests 0}}' localhost:5001/orders:1.0.0-a1b2c3dMedium Task 5: kind load
Why: Shortcut.
What to do: Load orders:dev into nodes.
- crictl images.
Solution
docker exec ch66-worker crictl imagesMedium Task 6: ECR
Why: Create.
What to do: Immutable repo with scanning.
- KMS encryption.
Solution
aws ecr describe-repositories --repository-names ordersHard Task 7: Lifecycle
Why: Clean.
What to do: Add lifecycle.json.
- Keep 30.
Solution
aws ecr get-lifecycle-policy --repository-name ordersHard Task 8: Nykaa
Why: Main task.
What to do: Local and ECR flow for catalog.
- Second push rejected.
Solution
aws ecr list-images --repository-name catalogHomework: try at home
For Nykaa's catalog image, run a local registry for kind, push catalog:2.3.0-COMMIT and read its digest, then create an ECR repository in ap-south-1 with immutable tags, scan on push and a keep-20 lifecycle policy, push the same image and prove the second push is rejected.
Recap
- Nodes pull from registries, so every image needs a full registry/repository:tag name.
- Tag each build with version plus commit; never deploy latest.
- Digests identify exact content and can be used in manifests.
- A local registry or kind load makes kind labs fast.
- ECR adds immutable tags, scanning, lifecycle rules and password-free pulls for EKS nodes.
FAQ
Q. Should I deploy by tag or by digest?
Immutable tags are readable and safe enough with ECR immutability. Digests are strictest; many teams store both, tag for people and digest in the manifest written by CI.
Q. How do EKS nodes pull from ECR without a password?
The node IAM role has the AmazonEC2ContainerRegistryReadOnly policy, and the kubelet credential provider fetches tokens automatically.
Q. What is a pull-through cache?
An ECR feature that caches images from Docker Hub, Quay or other public registries in your account, avoiding rate limits and outages.
Q. Can I sign images?
Yes. AWS Signer with Notation or Sigstore cosign can sign images, and admission policies can block unsigned ones.
Interview questions
-
Explain image name, tag and digest.
Model answer: Name is registry and repository, tag is a movable label, digest is the content hash that never changes.
-
Why is latest dangerous in Kubernetes?
Model answer: Different nodes can pull different images under the same name, and rollbacks become impossible to reason about.
-
How do you secure a container registry?
Model answer: Private repositories, IAM or tokens, immutable tags, scanning, signing and lifecycle policies.
-
How do you speed up image pulls?
Model answer: Small images, shared base layers, registry close to nodes, and pre-pulling or caching.
Quick quiz
Q1. Why avoid latest?
It moves; nodes may run different code.
Q2. What identifies exact image bytes?
The digest.
Q3. How long is an ECR login token valid?
12 hours.
Q4. What blocks overwriting a tag in ECR?
Tag immutability.
Q5. What does kind load do?
Copies an image into kind nodes without a registry.
Samajla ka?