Sign in
← Kubernetes: debugging
7 min · medium · Lesson 2 of 12

livenessProbe restarts the container

How to tell a crashed app from a badly configured probe, and why you need to read logs with the --previous flag.

The Deployment is rolled out, the image is right, the app inside the container works. And yet the RESTARTS counter keeps growing, and the status flips between Running and CrashLoopBackOff.

NAME                   READY   STATUS             RESTARTS      AGE
app-7c9d8f4b6-x2k4j    0/1     CrashLoopBackOff   4 (12s ago)   2m

A common cause: it isn't a bug in the app killing the container, it's livenessProbe. That's the check kubelet uses to decide whether a container is alive. In this lesson you'll learn to tell this kind of restart apart from a real app crash, and find where the check is pointing.

What kubectl shows

There are three places worth checking, in this order.

1. kubectl get pod

The first thing you notice: RESTARTS is above zero and growing. In parentheses, kubectl shows how long it's been since the last restart.

READY 0/1 means the container isn't ready to take traffic right now: it either failed readinessProbe (the readiness check) or isn't running at all and is waiting for the next attempt. Four restarts in two minutes is already a pattern, not a one-off error.

2. kubectl describe pod

This command shows three things get doesn't:

Typical output when liveness is broken:

    State:          Running
    Last State:     Terminated
      Reason:       Error
      Exit Code:    137
    Liveness:       http-get http://:8081/ delay=0s timeout=1s period=10s #success=1 #failure=3
...
Events:
  Type     Reason     Age   From     Message
  Warning  Unhealthy  5s    kubelet  Liveness probe failed: Get "http://10.244.0.7:8081/": dial tcp 10.244.0.7:8081: connect: connection refused
  Normal   Killing    5s    kubelet  Container app failed liveness probe, will be restarted

Exit code 137 is 128 + 9: the process was terminated by the SIGKILL signal. After failureThreshold failed checks in a row (3 by default), kubelet stops the container: it sends SIGTERM first, and if the process hasn't exited within terminationGracePeriodSeconds, it finishes it off with SIGKILL. Many processes that run as PID 1 in a container don't handle SIGTERM, which is why the exit code is usually 137.

The key part of Events: connection refused at an address with port 8081. The app isn't listening on that port.

3. kubectl logs --previous

kubectl logs <pod> shows the container's current run. If it just restarted, the output will have fresh lines, not the ones from when the problem happened. The --previous flag (or -p) gives you the log of the previous run.

kubectl logs app-7c9d8f4b6-x2k4j --previous

A line like listening on 0.0.0.0:8080 next to a probe hitting :8081 settles the question of the cause.

States that cause confusion

StatusWhat it means
PendingThe Pod isn't scheduled to a node yet, or the image is still downloading. Check Events.
RunningThe container is running. Whether it's ready for traffic is a separate question, shown by READY.
CrashLoopBackOffThe container keeps exiting over and over, and kubelet increases the pause between restarts (10s, 20s, 40s, and so on, up to 5 minutes).
ErrorThe container exited with a nonzero code. With restartPolicy: Never the status stays; for a Deployment it flickers between restarts.
CompletedThe container exited with code 0. Normal for a Job, bad for a Deployment: the container gets restarted and ends up in CrashLoopBackOff.
ImagePullBackOffThe image won't download: a typo in the name, no access, no network.
CreateContainerConfigErrorThe Pod references a ConfigMap or Secret that doesn't exist.
OOMKilledThe container went over resources.limits.memory, and the kernel killed it. Shows up in Last State.

Probe vs. the app's real state

RESTARTS is growing kubectl describe pod Events: Liveness probe failed message has the address and port Last State: OOMKilled, Error not a word about the probe in events probe points the wrong way port or path doesn't match the app itself crashed fix the process, not the probe
There's only one fork: do the events show Killing caused by the liveness probe?

livenessProbe answers the question "is the container alive?" If the check fails failureThreshold times in a row, kubelet restarts the container. From there, two scenarios are possible.

First: the app really did hang. The probe did its job, and it's the app that needs fixing.

Second: the probe is misconfigured. The app is alive, but the check is pointing the wrong way: wrong port, wrong path, too short a pause for a slow startup. This case is more common. For example, the container listens on 8080 (that's how unprivileged nginx images work), but the probe still has port 80 left over from the template.

A wrong path shows up differently in Events than a wrong port. A closed port: connect: connection refused. An open port with no such path: HTTP probe failed with statuscode: 404. For httpGet, codes from 200 to 399 count as success.

A slow startup under liveness looks like an endless CrashLoopBackOff: the container doesn't manage to open its port before liveness makes its failureThreshold attempts, and it gets killed again. The fix is a startupProbe, not turning the check off. More detail in the lesson on probes.

How to read Events

kubectl describe pod ends with a table:

Events:
  Type     Reason     Age                From     Message
  Normal   Started    40s (x2 over 52s)  kubelet  Started container app
  Warning  Unhealthy  31s (x6 over 49s)  kubelet  Liveness probe failed: ...
  Normal   Killing    31s (x2 over 43s)  kubelet  Container app failed liveness probe, will be restarted
  Normal   Pulled     30s (x3 over 52s)  kubelet  Container image "busybox:1.36.1" already present on machine

kubectl merges identical events into one line: x6 over 49s means the event happened six times over 49 seconds. So the line order doesn't match the actual timeline. Go by the counters instead: how many times the container started (Started), how many times it was killed (Killing), and why. The Unhealthy reason with the words Liveness probe failed points straight at the probe.

If the container exits on its own, the picture is different. Last State shows OOMKilled (ran out of memory) or Error with the app's exit code, and there are no liveness-related Killing events. Instead you get BackOff: kubelet waiting before the next start attempt.

How it's fixed

Check the port in the probe against the port the app is actually listening on:

kubectl get deploy app -o jsonpath='{.spec.template.spec.containers[0].livenessProbe}'
{"failureThreshold":3,"httpGet":{"path":"/","port":8081,"scheme":"HTTP"},"periodSeconds":10,"successThreshold":1,"timeoutSeconds":1}

Then edit the Deployment template, not the Pod. You can't change the probe on a running Pod, and a Pod created by a Deployment gets replaced with a new one anyway.

kubectl patch deploy app --type=json \
  -p='[{"op":"replace","path":"/spec/template/spec/containers/0/livenessProbe/httpGet/port","value":8080}]'

Or use kubectl edit deploy app. The Deployment creates a new ReplicaSet, the old Pods go away, and the new ones keep RESTARTS at zero. Wait a few check cycles (periodSeconds × failureThreshold) and look again: zero after that is the sign the fix worked.

In production the Deployment usually comes from Helm, Kustomize, or Argo CD. Then kubectl patch is only an emergency fix, and the permanent one goes into the manifest.

Common mistakes

Reading logs without --previous. The current run just started; there's nothing useful in it. You need the log of the run that got killed.

Removing livenessProbe to make the restarts stop. They'll stop, but now nothing notices a real hang either. You fix the check, you don't turn it off.

Raising failureThreshold or initialDelaySeconds to huge numbers. If the probe is hitting a closed port, it will never pass. All you're doing is delaying the restart.

Confusing liveness with OOM. Exit code 137 happens in both cases. What tells them apart is the Reason in Last State (OOMKilled or Error) and the Killing events.

Editing the Pod instead of the Deployment. kubectl delete pod just gets you a new Pod with the same broken probe.

How to avoid it

The probe's port and containerPort should come from the same place. When the manifest uses a named port, the probe references the name, not the number:

ports:
  - name: http
    containerPort: 8080
livenessProbe:
  httpGet:
    path: /healthz
    port: http

That way, changing the port in one place won't leave the probe with a stale value.

Second rule: liveness and readiness check different things. Readiness pulls the Pod out of load balancing while it isn't ready, and doesn't kill the container. Liveness does.

Homework

Practice the theory on your own cluster: 1 step with a check. Set up the cluster with the Kubernetes locally track, the tasks live in the k8s-practice repository.

./run.sh start debug-probe     # set up the starting state
./run.sh verify debug-probe    # check the current step

In short

What you'll need for the tasks

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson k8s-debug-02-liveness-restarts