Practical guide to Kubernetes probes

Probes provide an essential configuration element to design self-healing and robust deployments.

8 minutes reading time

Written by

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

A container that is still running is not the same as a container that is still working. A process can be alive in every sense the kernel cares about while the application inside it has wedged on a deadlock, lost its database connection, or filled its heap. Kubernetes, left to its own devices, sees a running process and assumes all is well, so traffic keeps flowing to a pod that can no longer serve it. 

In this guide, we'll walk through the three Kubernetes probes, the methods they use to check health, and how to tune them so your deployments heal themselves instead of quietly failing.

Prerequisites

To follow along, you'll need:

  • A Civo account
  • A Kubernetes cluster created on Civo
  • The latest kubectl utility to interact with the cluster
  • The KUBECONFIG file pointing kubectl at your cluster, downloadable from the cluster page on your Civo dashboard

If you do not have a cluster yet, you can create one from the Civo dashboard and download the kubeconfig in a couple of minutes.

Why probes matter

Kubernetes has become the de-facto standard for deploying cloud-native applications, driven in large part by the shift to microservices. That style of system, with lightweight, loosely coupled, autonomous services, suits distributed applications well. The trade-off is that managing those applications at scale is hard, especially when many components depend on one another and you need the system to stay correct even during partial failures.

By default, the only health signal Kubernetes has is whether the container's main process is running. That is a weak signal. An application can return errors on every request, refuse new connections, or hang indefinitely while its process stays up. Probes give Kubernetes an application-specific health signal so it can act: restart a wedged container, stop routing traffic to one that is not ready, or wait patiently for a slow boot to finish.

Before we add any probes, let's set up a namespace and a plain deployment to work against.

kubectl create namespace probe-demo
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: probe-demo
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
EOF

This is a healthy baseline with no probes. Everything below builds on it.

The three probe types

Kubernetes gives us three probes, each answering a different question:

  • Liveness probe: Constantly checks whether the container is healthy and functional. If it detects an issue, by default it restarts the container.
  • Readiness probe: Checks whether the container is ready to accept incoming requests. If it is, requests are sent to the container; if not, the pod is removed from the Service's endpoints.
  • Startup probe: Determines whether a container has started. While it runs, the liveness and readiness probes are held off so a slow boot does not trigger a premature restart.

Which probe when

If you remember nothing else, remember this:

  • Use a liveness probe to restart a container that has wedged but is still running. If it fails, the kubelet kills and restarts the container.
  • Use a readiness probe to decide whether a container should receive traffic. If it fails, the pod is pulled from Service endpoints but is not restarted.
  • Use a startup probe for slow-starting apps. While it runs, liveness and readiness are held off, so a long boot does not trigger premature restarts.

Probe methods

Every probe type checks health using one of four methods. The first three have been around since the early days of Kubernetes; the fourth, gRPC, is the modern addition we'll cover at the end of this section.

Exec

An exec probe runs a command inside the container. If the command exits with status 0, the probe succeeds; any non-zero exit is a failure. Here we check that nginx's default index page is present on disk.

kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: probe-demo
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
exec:
command:
- ls
- /usr/share/nginx/html/index.html
EOF

The file exists in the stock image, so ls exits 0 and the probe passes.

TCP

A TCP probe succeeds if the kubelet can open a TCP connection to the given port. Note that the manifest below points the probe at port 8080, while nginx listens on 80. That mismatch is deliberate: the connection will fail, and you can watch the kubelet restart the container as a result.

kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: probe-demo
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
tcpSocket:
port: 8080
EOF

Point the probe at port 80 instead and it will pass.

HTTP

An HTTP probe sends a GET request to a path and port. Any response code from 200 to 399 counts as success; anything else is a failure. Let's start by asking for a path that does not exist so we can see a failure.

kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: probe-demo
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /non-existing-endpoint
port: 80
EOF

That path returns a 404, so the probe fails and the container restarts. Point it at / and it succeeds.

kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: probe-demo
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /
port: 80
EOF

Tip: name your ports

Instead of repeating a port number across the manifest, name the container port and reference it by name in the probe. This keeps the probe in sync if the port ever changes.

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: named-port-probe
namespace: probe-demo
spec:
containers:
- name: app
image: registry.k8s.io/e2e-test-images/agnhost:2.40
args: ["liveness"]
ports:
- name: health
containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 5
periodSeconds: 5
EOF

The probe references the named port and resolves with no error:

Liveness: http-get http://:health/healthz delay=5s timeout=1s period=5s

gRPC

If your service speaks gRPC, you don't need to bolt on an HTTP endpoint or ship a separate probe binary. Since Kubernetes 1.27 you can point a probe straight at the gRPC health checking endpoint with the grpc field. This method is generally available on every supported cluster and needs no feature gate.

The example below uses the maintained agnhost grpc-health-checking test image, which implements the gRPC health checking protocol, so it runs with no extra setup.

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: grpc-liveness
namespace: probe-demo
spec:
containers:
- name: agnhost
image: registry.k8s.io/e2e-test-images/agnhost:2.40
args: ["grpc-health-checking"]
ports:
- containerPort: 5000
- containerPort: 8080
livenessProbe:
grpc:
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
EOF

The grpc block takes an optional service field. Leave it unset to check the server's overall health, or set it to a specific registered service name. With the probe passing, the pod stays healthy with no restarts:

NAME READY STATUS RESTARTS AGE
grpc-liveness 1/1 Running 0 2m10s

The gRPC liveness probe: pod Running with 0 restarts, no feature gate needed on current Kubernetes.

Seeing liveness restart a container

A liveness probe earns its keep when a container that was healthy goes bad. Let's force that. The deployment below starts nginx, then immediately deletes its index page in a postStart hook. The HTTP liveness probe against / will start failing and the kubelet will restart the container.

kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: probe-demo
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /
port: 80
lifecycle:
postStart:
exec:
command: ["/bin/bash", "-c", "rm -f /usr/share/nginx/html/index.html"]
EOF

Watch the restart count climb:

kubectl get pods -n probe-demo -w

The liveness probe is not a cure for every problem. It helps when a restart can actually clear the fault, such as an intermittent hang or a corrupted in-memory state. If the underlying issue is permanent, restarting just produces a crash loop, so reach for liveness when a fresh start has a real chance of fixing things.

Adding a readiness probe

Restarting a broken container is good, but while it is broken we should also stop sending it traffic. That is the readiness probe's job. The deployment below keeps the same self-sabotaging postStart hook and adds a readiness probe alongside the liveness probe.

kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: probe-demo
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /
port: 80
lifecycle:
postStart:
exec:
command: ["/bin/bash", "-c", "rm -f /usr/share/nginx/html/index.html"]
readinessProbe:
httpGet:
path: /
port: 80
EOF

When the readiness probe fails, the pod is removed from the Service's endpoints, so clients are not routed to it. The container is not restarted by readiness alone; that is the liveness probe's responsibility. Combining the two gives you the robust pattern most production deployments want: unhealthy pods stop receiving traffic and unhealthy containers get restarted.

Adding a startup probe

Some applications are slow to boot. If a liveness probe starts checking before the app has finished starting, it can kill the container during a perfectly normal, if lengthy, startup. The startup probe solves this. While it runs, both the liveness and readiness probes are disabled, and only once it succeeds do the other two take over.

kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: probe-demo
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /
port: 80
lifecycle:
postStart:
exec:
command: ["/bin/bash", "-c", "rm -f /usr/share/nginx/html/index.html"]
readinessProbe:
httpGet:
path: /
port: 80
startupProbe:
httpGet:
path: /
port: 80
EOF

This is the key point that trips people up: when a startup probe is configured, it disables the other two probes until it succeeds. Give it a generous failureThreshold and periodSeconds so even a slow application has time to come up.

Advanced probe configuration

Every probe shares the same five tunables. Sensible defaults are built in, but tuning them is what makes a probe fit your application instead of fighting it.

ParameterDescriptionMinimumPurpose

initialDelaySeconds

0

0

The time after the container starts before any probe runs

periodSeconds

10

1

How often the probe runs

timeoutSeconds

1

1

How long to wait for a probe response

successThreshold

1

1

Consecutive successes needed to mark the probe healthy

failureThreshold

3

1

Consecutive failures needed to mark the probe unhealthy

A common combination is a longer initialDelaySeconds to cover startup, a periodSeconds short enough to catch failures quickly, and a failureThreshold high enough to ride out a brief blip without overreacting.

Failing fast with a probe-level grace period

By default, when a liveness probe fails, the container gets the pod's full terminationGracePeriodSeconds to shut down. For a process that is already hung, that wait is wasted. You can set a probe-level terminationGracePeriodSeconds so a liveness failure overrides the pod value and restarts faster. This field is valid on liveness and startup probes only; the API server rejects it on readiness probes.

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: liveness-fast-restart
namespace: probe-demo
spec:
terminationGracePeriodSeconds: 600
containers:
- name: app
image: registry.k8s.io/e2e-test-images/agnhost:2.40
args: ["liveness"]
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 1
# a liveness failure overrides the pod's 600s grace period
terminationGracePeriodSeconds: 5
EOF

The agnhost liveness image starts returning 500s on /healthz shortly after boot. With failureThreshold set to 1 and a probe-level grace period of 5 seconds, the wedged container is killed and restarted promptly instead of waiting out the 600-second pod grace period:

Liveness: http-get http://:8080/healthz ... #failure=1
Warning Unhealthy Liveness probe failed: HTTP probe failed with statuscode: 500
Normal Killing Container app failed liveness probe, will be restarted
RESTARTS 3

A failing liveness probe with a probe-level terminationGracePeriodSeconds restarting the container promptly.

Cleaning up

When you are finished experimenting, remove everything you created:

kubectl delete pod grpc-liveness liveness-fast-restart named-port-probe -n probe-demo
kubectl delete namespace probe-demo

Summary

We covered how to configure probes in Kubernetes, the three probe types and the questions each one answers, the four methods (exec, TCP, HTTP, and gRPC) they use to check health, and the advanced settings that let you tune probe behavior to match your application. Used together, a liveness probe restarts containers that have wedged, a readiness probe keeps traffic away from pods that are not ready, and a startup probe protects slow-booting apps from premature restarts. Add the five tunables and a probe-level grace period on top, and you have the building blocks for highly available, self-healing deployments on Civo.

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

Jubril Oyetunji is a DevOps engineer and technical writer with a strong focus on cloud-native technologies and open-source tools. His work centers on creating practical tutorials that help developers better understand platforms such as Kubernetes, NGINX, Rust, and Go.

As a contract technical writer, Jubril authored an extensive library of technical guides covering cloud-native infrastructure and modern development workflows. Many of his tutorials achieved strong search rankings, helping developers around the world learn and adopt emerging technologies.

View author profile