Chapter 66: Docker and Kubernetes Together: Dockerfile to Registry to Cluster, Jenkins and Kompose
66.3 Deploy the image to Kubernetes: config, secrets, probes and pull secrets
Why: the problem
The image is in a registry; now Kubernetes must run it the way the Dockerfile intended. Every promise from 66.1 maps to a field in the manifest. Port 8080 becomes containerPort and a Service. Settings read from environment variables come from a ConfigMap and a Secret. The non-root UID becomes runAsNonRoot and runAsUser in securityContext. The /healthz and /ready endpoints become liveness and readiness probes. Clean shutdown on SIGTERM pairs with terminationGracePeriodSeconds. If any of these is missing, the app may run, but it will fail in surprising ways during a rollout or a node drain. Docker run flags translate almost one to one. docker run -p 8080:8080 becomes a Service; -e APP_ENV=prod becomes env or envFrom a ConfigMap; --env-file .env becomes a Secret; --memory 256m becomes resources.limits.memory; --read-only becomes readOnlyRootFilesystem with an emptyDir for /tmp; --user 10001 becomes runAsUser. Learning this mapping is the core skill of moving from Docker to Kubernetes, and it is also exactly what Kompose tries to automate in 66.5. Private registries need credentials. On EKS with ECR, node IAM roles handle it. Everywhere else, you create a docker-registry Secret and reference it with imagePullSecrets, either in the pod spec or attached to the ServiceAccount so every pod using that account gets it. The Secret value in this lesson is a placeholder; real tokens come from your CI or from External Secrets, never from a file in Git. We deploy the orders image from localhost:5001 to the ch66 kind cluster with a ConfigMap, a placeholder Secret, probes, resources and a hardened securityContext, expose it with a Service, then roll out version 1.0.1 and roll back.
What: every term in plain English
- Deployment — Keeps N replicas of a pod template running and manages rollouts.
- Service — Stable virtual IP and DNS name in front of matching pods.
- ConfigMap — Non-secret settings injected as env vars or files.
- Secret — Sensitive values, base64 in the API; encrypt at rest and keep out of Git.
- envFrom — Loads every key of a ConfigMap or Secret as environment variables.
- readinessProbe — Pod receives traffic only while it passes.
- livenessProbe — Container is restarted if it fails repeatedly.
- imagePullSecrets — Registry credentials the kubelet uses to pull private images.
- securityContext — runAsNonRoot, runAsUser, readOnlyRootFilesystem, dropped capabilities.
- rollout undo — Returns a Deployment to its previous ReplicaSet.
How: step by step
- Create namespace, ConfigMap and placeholder Secret. Config ready.
- Write the Deployment with probes and securityContext. Manifest ready.
- Add a Service and test with port-forward. App answers.
- Create a registry pull Secret and attach it to the ServiceAccount. Private pulls.
- Roll out 1.0.1. New ReplicaSet.
- Roll back. Previous version.
- Map docker run flags to fields. Translation table.
- Clean up. Namespace gone.
A useful habit is to write the docker run command and the manifest side by side the first few times. docker run -d -p 8080:8080 -e APP_ENV=staging --env-file secrets.env --user 10001 --read-only --tmpfs /tmp --memory 256m --health-cmd ... localhost:5001/orders:1.0.0-a1b2c3d contains almost every field of the Deployment above, just in a different shape. Two things have no docker run equivalent and are where Kubernetes really adds value: replicas with a rolling update strategy, and readiness gating so traffic only reaches pods that are ready. Teams at Zomato and PhonePe start new services by copying a golden Deployment template like this one, with probes, resources and securityContext already filled in, and change only the name, image and config. That keeps every service hardened by default and lets reviewers focus on what is actually different.
Example 1: Namespace, ConfigMap, Secret and Deployment
Goal. Namespace, ConfigMap, Secret and Deployment.
deploy.sh
kubectl create namespace orders
kubectl -n orders create configmap orders-config --from-literal=APP_ENV=staging --from-literal=LOG_LEVEL=info
kubectl -n orders create secret generic orders-secret --from-literal=DB_PASSWORD=change-me-placeholder
orders-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders
namespace: orders
spec:
replicas: 3
selector:
matchLabels: {app: orders}
template:
metadata:
labels: {app: orders}
spec:
terminationGracePeriodSeconds: 30
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile: {type: RuntimeDefault}
containers:
- name: orders
image: localhost:5001/orders:1.0.0-a1b2c3d
ports: [{containerPort: 8080, name: http}]
envFrom:
- configMapRef: {name: orders-config}
- secretRef: {name: orders-secret}
resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {memory: 256Mi}
readinessProbe:
httpGet: {path: /ready, port: http}
periodSeconds: 5
livenessProbe:
httpGet: {path: /healthz, port: http}
initialDelaySeconds: 10
periodSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: [ALL]}
volumeMounts: [{name: tmp, mountPath: /tmp}]
volumes: [{name: tmp, emptyDir: {}}]
What each line does
change-me-placeholder— Not a real secret; CI or External Secrets supplies the real value.envFrom— Same variables the app read from docker run -e.runAsUser 10001— Matches USER 10001 in the Dockerfile.readOnlyRootFilesystem— Equivalent of docker run --read-only; /tmp is an emptyDir.Probes— Use the /ready and /healthz endpoints built in 66.1.
Green output
$ bash deploy.sh && kubectl apply -f orders-deploy.yaml
namespace/orders created
configmap/orders-config created
secret/orders-secret created
deployment.apps/orders created
$ kubectl -n orders rollout status deploy/orders
deployment "orders" successfully rolled out
$ kubectl -n orders get pods
NAME READY STATUS RESTARTS AGE
orders-6f9c8d7b5c-4kq2x 1/1 Running 0 21s
orders-6f9c8d7b5c-8tz7m 1/1 Running 0 21s
orders-6f9c8d7b5c-r2w9d 1/1 Running 0 21s
Expected result. Three hardened replicas pulled the image from the local registry and passed readiness.
Example 2: Service, port-forward and a private pull Secret
Goal. Service, port-forward and a private pull Secret.
svc.yaml
apiVersion: v1
kind: Service
metadata:
name: orders
namespace: orders
spec:
selector: {app: orders}
ports: [{name: http, port: 80, targetPort: http}]
pull.sh
kubectl apply -f svc.yaml
kubectl -n orders port-forward svc/orders 8080:80 &
sleep 2; curl -s localhost:8080/version; curl -s localhost:8080/orders
kubectl -n orders create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=ci-bot \
--docker-password="$REGISTRY_TOKEN"
kubectl -n orders patch serviceaccount default -p '{"imagePullSecrets":[{"name":"regcred"}]}'
kubectl -n orders get sa default -o jsonpath='{.imagePullSecrets}'
What each line does
targetPort: http— Refers to the named container port, so port changes stay in one place.port-forward— Quick test without Ingress; ch65 covers real exposure.$REGISTRY_TOKEN— Read from your CI environment, never typed into a file.patch serviceaccount— Every pod using default SA in orders gets regcred automatically.registry.example.com— Placeholder private registry host.
Green output
$ bash pull.sh
service/orders created
Forwarding from 127.0.0.1:8080 -> 8080
{"app":"orders","version":"1.0.0","env":"staging"}
[{"id":1,"item":"masala dosa","qty":2}]
secret/regcred created
serviceaccount/default patched
[{"name":"regcred"}]
Expected result. The Service answers with the ConfigMap's APP_ENV, and the namespace is ready to pull from a private registry.
Example 3: Roll out a new version and roll back
Goal. Roll out a new version and roll back.
rollout.sh
cd orders-app
sed -i 's/VERSION = "1.0.0"/VERSION = "1.0.1"/' app.py
git commit -qam "1.0.1"
TAG=1.0.1-$(git rev-parse --short HEAD)
docker build -t localhost:5001/orders:$TAG . && docker push localhost:5001/orders:$TAG
kubectl -n orders set image deploy/orders orders=localhost:5001/orders:$TAG
kubectl -n orders annotate deploy/orders kubernetes.io/change-cause="orders $TAG" --overwrite
kubectl -n orders rollout status deploy/orders
kubectl -n orders rollout history deploy/orders
kubectl -n orders rollout undo deploy/orders
kubectl -n orders get deploy orders -o jsonpath='{.spec.template.spec.containers[0].image}'
What each line does
New tag— New commit means new immutable tag.set image— Changes the pod template and starts a rolling update.change-cause— Shows in rollout history so on-call knows what each revision is.rollout undo— Switches back to the previous ReplicaSet in seconds.jsonpath— Confirms which image is live.
Green output
$ bash rollout.sh
deployment.apps/orders image updated
Waiting for deployment "orders" rollout to finish: 1 out of 3 new replicas have been updated...
deployment "orders" successfully rolled out
REVISION CHANGE-CAUSE
1 none
2 orders 1.0.1-9f8e7d6
deployment.apps/orders rolled back
localhost:5001/orders:1.0.0-a1b2c3d
Expected result. Kubernetes replaced pods gradually using the readiness probe and returned to 1.0.0 with one command.
Every command, step by step (kind lab with kubectl)
-
Namespace
kubectl create namespace ordersWhat it does: Created.
namespace/orders created -
Config
kubectl -n orders create configmap orders-config --from-literal=APP_ENV=stagingWhat it does: Config.
configmap/orders-config created -
Secret
kubectl -n orders create secret generic orders-secret --from-literal=DB_PASSWORD=change-me-placeholderWhat it does: Placeholder.
secret/orders-secret created -
Deploy
kubectl apply -f orders-deploy.yamlWhat it does: Running.
deployment.apps/orders created -
Status
kubectl -n orders rollout status deploy/ordersWhat it does: Ready.
successfully rolled out -
Service
kubectl apply -f svc.yamlWhat it does: Exposed.
service/orders created -
Test
curl -s localhost:8080/versionWhat it does: Version.
{"version":"1.0.0"} -
Update
kubectl -n orders set image deploy/orders orders=localhost:5001/orders:1.0.1-9f8e7d6What it does: Rolling.
image updated -
Undo
kubectl -n orders rollout undo deploy/ordersWhat it does: Back.
rolled back -
Clean
kubectl delete namespace ordersWhat it does: Gone.
namespace "orders" deleted
When it is red 1
ErrImagePull / ImagePullBackOff
Cause. Wrong name or tag, registry unreachable, or missing credentials.
Fix, step by step
- kubectl describe pod; check the Events.
- Check imagePullSecrets or node IAM role.
When it is red 2
CreateContainerConfigError: secret "orders-secret" not found
Cause. Secret missing in this namespace.
Fix, step by step
- Create it in the same namespace as the Deployment.
When it is red 3
container has runAsNonRoot and image will run as root
Cause. Image has no numeric USER.
Fix, step by step
- Set USER 10001 in the Dockerfile or runAsUser in the pod.
When it is red 4
Read-only file system
Cause. App writes outside mounted volumes.
Fix, step by step
- Mount an emptyDir at the path it writes, such as /tmp.
When it is red 5
Readiness probe failed: connection refused
Cause. Wrong port or app slow to start.
Fix, step by step
- Check containerPort and add a startupProbe.
Practice tasks
Easy Task 1: Namespace
Why: Create.
What to do: Create orders.
- kubectl.
Solution
kubectl get ns ordersEasy Task 2: Config
Why: Add.
What to do: ConfigMap and Secret.
- Placeholder value.
Solution
kubectl -n orders get cm,secretEasy Task 3: Deploy
Why: Apply.
What to do: Apply manifest.
- Three replicas.
Solution
kubectl -n orders get podsMedium Task 4: Service
Why: Expose.
What to do: Service and port-forward.
- curl.
Solution
kubectl -n orders get svcMedium Task 5: Pull
Why: Secret.
What to do: regcred on SA.
- patch.
Solution
kubectl -n orders get sa default -o yamlMedium Task 6: Rollout
Why: Update.
What to do: set image to new tag.
- change-cause.
Solution
kubectl -n orders rollout history deploy/ordersHard Task 7: Undo
Why: Back.
What to do: Roll back and verify image.
- jsonpath.
Solution
kubectl -n orders get deploy orders -o wideHard Task 8: Swiggy
Why: Main task.
What to do: menu service end to end.
- Hardened.
Solution
kubectl -n menu get allHomework: try at home
Deploy Swiggy's menu service image from your local registry into namespace menu with a ConfigMap, a placeholder Secret, probes, resources, non-root and read-only settings, a Service, then roll out a new commit tag and roll back.
Recap
- Each Dockerfile promise maps to a manifest field: port, env, user, probes, shutdown.
- docker run flags translate to Service, ConfigMap, Secret, resources and securityContext.
- Private images need imagePullSecrets or node IAM.
- Use new immutable tags for each rollout and record change-cause.
- rollout undo is the fastest recovery.
FAQ
Q. Should config live in the image?
No. The same image runs in every environment; ConfigMaps and Secrets change per environment.
Q. Are Kubernetes Secrets encrypted?
Only base64 by default in the API; enable encryption at rest, restrict RBAC and use External Secrets or a vault for real values.
Q. Why not use hostPort instead of a Service?
hostPort ties pods to node ports and breaks scheduling; Services and Ingress are the standard.
Q. Do I need a startupProbe?
For slow-starting apps, yes; it delays liveness checks until the app is up.
Q. How do I see the env vars a pod actually got?
kubectl exec into the pod and run env, or kubectl describe pod to see which ConfigMaps and Secrets are referenced. Avoid printing Secret values in shared terminals or CI logs.
Q. Does a ConfigMap change restart pods?
No. Env vars are read at start, so run kubectl rollout restart, or use a checksum annotation in Helm so a config change changes the pod template and triggers a rollout automatically.
Q. What does maxUnavailable 0 and maxSurge 1 mean?
Kubernetes adds one new pod at a time and removes an old one only after the new one is ready, so capacity never drops during the rollout.
Interview questions
-
How do you move a Docker Compose app to Kubernetes?
Model answer: Map each service to a Deployment and Service, env to ConfigMaps and Secrets, volumes to PVCs, and add probes and resources.
-
Difference between readiness and liveness?
Model answer: Readiness gates traffic; liveness restarts a stuck container.
-
How do you handle registry credentials?
Model answer: Node IAM on EKS, otherwise docker-registry Secrets attached to ServiceAccounts, filled by CI or External Secrets.
-
How do you make a rollout safe?
Model answer: Readiness probes, maxUnavailable 0, immutable tags, change-cause, and quick rollback.
Quick quiz
Q1. Which object replaces docker run -e?
ConfigMap or env in the pod spec.
Q2. What field matches USER 10001?
runAsUser: 10001.
Q3. How do pods pull from a private registry?
imagePullSecrets or node IAM role.
Q4. Which probe controls traffic?
readinessProbe.
Q5. How do you roll back?
kubectl rollout undo.
Samajla ka?