Introduction
Running means a container process exists; it does not prove that the application is ready to receive traffic or capable of recovering. Kubernetes probes turn application-specific health checks into controller and Service behavior.
You will add all three probe types, observe readiness remove a Pod from Service endpoints, repair it, and add a graceful shutdown hook.
Keep the responsibilities distinct: a startup probe protects slow initialization, a readiness probe controls whether a Pod receives Service traffic, and a liveness probe decides when kubelet should restart a stuck container. Using one probe for every purpose often causes avoidable restarts or premature traffic.
Deploy a Probed Application
In this step, deploy NGINX with startup, readiness, and liveness checks against / on its named http port.
The manifest is longer because the three probes sit inside the container specification. Read it in this order before applying it: Deployment and labels, container and named port, then startup, readiness, and liveness. All three checks use the same HTTP endpoint in this demo, but their failure consequences differ.
Some one-line mappings and lists use YAML's compact “flow style,” such as {app: healthy-web}. Curly braces represent a mapping and square brackets represent a list; they mean the same thing as their multi-line YAML forms. Use them here only where the whole value remains easy to read.
The startup threshold permits up to 24 seconds (12 × 2) before Kubernetes considers startup failed. Once startup succeeds, readiness runs every three seconds and liveness every five seconds.
cd /home/labex/project/application-health
cat <<'EOF' > healthy-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: healthy-web
namespace: practice-health
spec:
replicas: 2
selector:
matchLabels: {app: healthy-web}
template:
metadata:
labels: {app: healthy-web}
spec:
containers:
- name: web
image: nginx:1.27-alpine
imagePullPolicy: IfNotPresent
ports:
- {name: http, containerPort: 80, protocol: TCP}
startupProbe:
httpGet: {path: /, port: http}
failureThreshold: 12
periodSeconds: 2
readinessProbe:
httpGet: {path: /, port: http}
periodSeconds: 3
livenessProbe:
httpGet: {path: /, port: http}
periodSeconds: 5
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: healthy-web
namespace: practice-health
spec:
selector: {app: healthy-web}
ports:
- {name: http, port: 80, targetPort: http, protocol: TCP}
EOF
Create the namespace and apply the manifest. rollout status waits for both replicas to satisfy readiness, not merely for their processes to start:
kubectl create namespace practice-health
kubectl apply -f healthy-web.yaml
kubectl rollout status deployment/healthy-web -n practice-health --timeout=60s
The startup probe protects slow initialization; readiness controls traffic eligibility; liveness permits restart after an unrecoverable failure.
If the rollout completes, you already know more than “the containers are running”: both Pods answered the readiness request successfully.
The final rollout line is the first health proof:
Waiting for deployment "healthy-web" rollout to finish: 0 of 2 updated replicas are available...
Waiting for deployment "healthy-web" rollout to finish: 1 of 2 updated replicas are available...
deployment "healthy-web" successfully rolled out
The intermediate lines are normal. Kubernetes reports progress as each Pod passes startup and readiness; only the last line means both replicas are available.
Connect Readiness to Service Endpoints
In this step, inspect the readiness condition and EndpointSlices.
Start with the familiar Pod table, then narrow the output to the readiness condition and restart count. The custom table makes it easy to distinguish “not ready” from “restarting”:
kubectl get pods -n practice-health -l app=healthy-web
kubectl get pods -n practice-health -l app=healthy-web -o custom-columns='POD:.metadata.name,READY_CONDITION:.status.conditions[?(@.type=="Ready")].status,RESTARTS:.status.containerStatuses[0].restartCount'
kubectl get endpointslices -n practice-health -l kubernetes.io/service-name=healthy-web -o jsonpath='{range .items[].endpoints[*]}{.addresses[0]} ready={.conditions.ready}{"\n"}{end}'
Both addresses should show ready=true. A readiness failure leaves the container running but removes its address from normal Service routing.
EndpointSlices are the bridge between a Service selector and actual Pod addresses. The Service still exists when a Pod is unready; its endpoint condition is what prevents normal traffic from being sent there.
Typical healthy evidence is:
POD READY_CONDITION RESTARTS
healthy-web-... True 0
healthy-web-... True 0
10.244.104.11 ready=true
10.244.104.12 ready=true
Pod names and IP addresses will differ. The relationship matters: two Ready=True Pods correspond to two ready=true endpoints, and neither container has restarted.
Observe a Readiness Failure
In this step, deliberately change only the readiness path to a missing URL.
This is a controlled failure. JSON Patch replaces a single nested field and therefore leaves the startup and liveness probes unchanged. NGINX returns HTTP 404 for /not-ready, which readiness treats as failure.
kubectl patch deployment healthy-web -n practice-health --type='json' -p='[{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/httpGet/path","value":"/not-ready"}]'
kubectl rollout status deployment/healthy-web -n practice-health --timeout=20s || true
kubectl get pods -n practice-health -l app=healthy-web
kubectl get endpointslices -n practice-health -l kubernetes.io/service-name=healthy-web -o jsonpath='{range .items[].endpoints[*]}{.addresses[0]} ready={.conditions.ready}{"\n"}{end}'
|| true marks the rollout timeout as expected so the shell continues. The new containers run, but readiness fails and the Deployment cannot become available.
The tested rollout produced a state like this:
error: timed out waiting for the condition
NAME READY STATUS RESTARTS
healthy-web-old-... 1/1 Running 0
healthy-web-old-... 1/1 Running 0
healthy-web-new-... 0/1 Running 0
...
10.244.104.17 ready=false
The one new Pod is Running but unready, and it has not restarted. The two old ready Pods remain because Deployment rolling updates protect availability: Kubernetes will not replace more healthy replicas while the new revision cannot become ready. The rollout timeout therefore proves both readiness behavior and the controller's safety mechanism. In EndpointSlice output, the new address shows ready=false, so normal Service routing still uses only healthy endpoints.
Repair Readiness and Verify Traffic
In this step, restore the readiness path and wait for the Service endpoints to return.
Use the inverse of the previous patch so the experiment changes only one variable. Waiting for rollout status now succeeds because the replacement Pods answer / with HTTP 200.
kubectl run health-client creates a temporary standalone Pod for the request. --image chooses BusyBox, --image-pull-policy=IfNotPresent reuses the cached image, and --restart=Never asks for a Pod rather than a controller. --rm -i keeps the command attached and deletes the Pod when it exits; the second -- separates kubectl options from the wget command that runs inside the container.
In wget -qO-, -q hides progress messages and -O- writes the response body to standard output; the pipe sends that body to head, which displays only its first lines. The URL uses the Service name healthy-web, which cluster DNS resolves inside the namespace.
kubectl patch deployment healthy-web -n practice-health --type='json' -p='[{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/httpGet/path","value":"/"}]'
kubectl rollout status deployment/healthy-web -n practice-health --timeout=60s
kubectl run health-client -n practice-health --image=busybox:1.36 --image-pull-policy=IfNotPresent --restart=Never --rm -i -- wget -qO- http://healthy-web | head
The temporary client uses cluster DNS. Limiting the displayed HTML keeps the evidence readable while a successful response proves readiness and Service routing agree.
The response begins with recognizable NGINX HTML:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
Seeing HTML after a successful rollout proves the full path: DNS resolved the Service, the Service selected a ready endpoint, and NGINX handled the request.
Deleting the temporary Pod afterward avoids leaving diagnostic clients behind in the namespace.
Add Graceful Termination
In this step, add a preStop hook and a termination grace period. The hook delays shutdown briefly so endpoint removal can propagate before the process exits.
Read the two JSON Patch operations separately. The first adds a 20-second Pod termination budget. The second adds a five-second sleep hook to container index 0. Kubernetes starts the hook when termination begins, while the remaining grace period is still counting down.
kubectl patch deployment healthy-web -n practice-health --type=json -p='[
{"op":"add","path":"/spec/template/spec/terminationGracePeriodSeconds","value":20},
{"op":"add","path":"/spec/template/spec/containers/0/lifecycle","value":{"preStop":{"sleep":{"seconds":5}}}}
]'
kubectl rollout status deployment/healthy-web -n practice-health --timeout=60s
kubectl get deployment healthy-web -n practice-health -o jsonpath='grace={.spec.template.spec.terminationGracePeriodSeconds}s preStop={.spec.template.spec.containers[0].lifecycle.preStop.sleep.seconds}s{"\n"}'
The JSON patch changes only the two named fields, so the probes and other container settings remain intact. Graceful termination complements readiness: one controls admission to traffic, while the other gives Kubernetes time to remove the Pod from traffic before shutdown completes.
The final JSONPath output should be grace=20s preStop=5s. The five-second hook fits inside the 20-second budget, leaving time for NGINX to exit normally.
The rollout may pause on messages such as 1 old replicas are pending termination because the new preStop hook deliberately adds five seconds to shutdown. The final output remains:
deployment "healthy-web" successfully rolled out
grace=20s preStop=5s
Summary
You connected startup, readiness, and liveness checks to Kubernetes behavior, observed readiness remove endpoints without stopping containers, restored traffic, and added graceful termination. These mechanisms make health actionable rather than merely visible.


