Application logs
kubectl logs, streaming, and multi-container pods.
Your pod keeps dying. kubectl get pods shows CrashLoopBackOff, which is Kubernetes telling you it has given up restarting a container that won't stay up and is now waiting longer between each try. You run kubectl logs on the pod and get two lines from a container that started a second ago, and nothing about why the last one died. The output that would explain the crash left with the container that produced it. A log is the running diary a program keeps while it works: every line it prints goes in, one after another. This lesson is about reading that diary well, knowing where it physically lives on the machine, and what to do once the pod (the smallest thing Kubernetes runs, one container or a few that share a fate) that wrote it is gone.
What kubectl logs is actually reading
A well-behaved container writes to stdout and stderr, the two output streams every program has by default (the same ones that print to your terminal when you run a command). It does not write to a file inside the container. Here is why that matters. The container runtime, the software on each node that actually starts and stops containers (usually containerd), captures those two streams and writes them to a file on the node's disk. When you run kubectl logs, the command never touches the container. It hits the pod's log subresource on the API server (the cluster's front door, GET /api/v1/namespaces/<ns>/pods/<pod>/log), which forwards the request to the kubelet (the Kubernetes agent running on the node that holds the pod), and the kubelet reads that file and streams it back. So kubectl logs is a remote tail of a file the runtime keeps for you. It also explains a common dead end: an app that writes to /var/log/app.log inside its own container is invisible to kubectl logs, because nothing is capturing that file.
kubectl logs web-5d8f -c app --timestamps --tail=5
2026-07-16T09:14:02.118471Z INFO config loaded from /etc/app/config.yaml2026-07-16T09:14:02.140233Z INFO connecting to postgres at db:54322026-07-16T09:14:02.361902Z INFO migrations up to date (schema v41)2026-07-16T09:14:02.362550Z INFO listening on :80802026-07-16T09:14:02.362701Z INFO readiness probe endpoint /healthz ready
The options that decide whether you find the bug
The flags are the difference between a two-minute fix and an hour of guessing. -f follows the stream live, the same as tail -f. --tail=N, --since=15m, and --since-time bound how much you pull back, which you want on a service that logs thousands of lines a minute. -c names a container in a multi-container pod. Leave it off on a pod that also runs a sidecar (a helper container sitting next to the main one) and kubectl does not error. It picks the first container in the spec, or the one named by the pod's default-container annotation if that is set, and prints a "Defaulted container ..." line so you know which one you got. Grab the wrong container's log by accident and you will stare at a perfectly healthy sidecar while your app burns. An init container (a setup step that runs to completion before the main app starts) has its own log too; you name it explicitly, since it has already finished by the time the pod shows Running. --all-containers=true merges them all. The one flag that saves you most often is --previous (short -p). After a crash the current container is a fresh restart with almost nothing in it, so --previous reads the log of the instance that just died, which is where the actual error lives.
kubectl logs web-5d8f --previous -c app
2026-07-16T09:12:58.402Z INFO connecting to postgres at db:54322026-07-16T09:12:59.905Z FATAL dial tcp 10.96.0.31:5432: connect: connection refusedpanic: database unreachable, giving up after 3 attemptsgoroutine 1 [running]:main.mustConnect(...)/src/main.go:48main.main()/src/main.go:22 +0x1a5
To tail a whole workload instead of one pod, point the selector at its label and let --prefix tag each line with the pod it came from. Kubernetes streams from up to five matching pods by default and refuses more, so it does not open a flood of concurrent streams against the API server and the kubelets at once. When you have more replicas than that, raise the cap explicitly.
kubectl logs -l app=web --prefix --tail=2 --max-log-requests=10
[pod/web-5d8f/app] 2026-07-16T09:14:03Z GET /healthz 200 0.4ms[pod/web-5d8f/app] 2026-07-16T09:14:04Z GET /orders 200 12.8ms[pod/web-7bc2/app] 2026-07-16T09:14:03Z GET /healthz 200 0.5ms[pod/web-7bc2/app] 2026-07-16T09:14:04Z POST /orders 201 22.1ms
Where the logs live, and why they vanish
The file the runtime writes has a predictable path on the node: /var/log/pods/<namespace>_<pod>_<uid>/<container>/<restart-count>.log. The number is the restart count, so 0.log is the first run of the container, 1.log the second, and --previous is really "read restart-count minus one." You can see the files directly by launching a debug pod on the node, which mounts the host filesystem under /host.
kubectl debug node/node-2 -it --image=busybox -- ls /host/var/log/pods/prod_web-5d8f_9f3a1c2b/app
Creating debugging pod node-debugger-node-2-k4p9 with container debugger on node node-2.0.log 1.log 2.log
Two limits bite here. First, the kubelet rotates these files: by default it caps each container's log at 10 MB and keeps five of them (the containerLogMaxSize and containerLogMaxFiles settings). A busy container overwrites its own older lines within minutes, so kubectl logs shows only what is still on disk, never the full history, even for a pod that is running fine. Second, --previous reaches exactly one restart back. A pod that has crash-looped forty times will show you crash forty and has no way to show you crash one. Grab the logs the moment you notice the problem.
When the pod is gone, so are its logs
Everything above reads from one node's disk. Delete the pod, or let the kubelet evict it when the node runs low on memory, or lose the node to a hardware failure, and every one of those files goes with it. That is fine for "debug this pod right now" and useless for "what did checkout log when it broke last Tuesday." The fix is central aggregation. A logging agent runs as a DaemonSet (a controller that pins one copy of a pod to every node, the way a building puts a mailroom on every floor). Each copy tails the files under /var/log/pods and ships them to a store you can search across the whole fleet: Loki, Elasticsearch or OpenSearch, or a cloud logging service. Fluent Bit and Vector are the usual agents. One habit makes this pay off: log structured lines (JSON with consistent fields like level, msg, and a request id) to stdout instead of free-form text, so you can filter and correlate across services in the store rather than grepping prose. Now the logs outlive the pods, and last week's incident still has evidence.
Previous logs vanish when the kubelet rotates. Catch CrashLoop evidence early.
Do not log secrets. Once printed, they live in aggregator storage and tickets.
Node disk pressure kills log retention first. Full disks look like silent applications. Catch CrashLoop evidence early.
Try this
Fetch logs from a multi-container pod with -c, follow briefly with --tail, and grab previous logs from a crashed container if one exists.
$ kubectl logs web-5d8f -c app --timestamps --tail=5$ kubectl logs web-5d8f --previous -c app$ kubectl logs -l app=web --prefix --tail=2 --max-log-requests=10$ kubectl debug node/node-2 -it --image=busybox -- ls /host/var/log/pods/prod_web-5d8f_9f3a1c2b/app
Takeaway
kubectl logs reads what the kubelet kept for a container. Multi-container pods need -c. Long-term search needs a log shipper.