Three commands for working with what is already running: read its output, get inside it, and reach it from your machine.
kubectl logs payments-api-7d4f-x8k2
kubectl logs payments-api-7d4f-x8k2 -f # follow
kubectl logs payments-api-7d4f-x8k2 --tail=100
kubectl logs payments-api-7d4f-x8k2 --since=15m
kubectl logs payments-api-7d4f-x8k2 --timestampskubectl logs payments-api-7d4f-x8k2 --previousWhen a container is crash-looping, plain kubectl logs shows the instance that has just started, which during a backoff has produced nothing yet. --previous shows the run that actually failed, and that is where the error is.
If you learn one thing about debugging Pods, this is it. A CrashLoopBackOff with apparently empty logs is nearly always someone forgetting --previous.
kubectl logs <pod> -c api # one container
kubectl logs <pod> --all-containers # all of themWith more than one container in a Pod, kubectl logs refuses to guess and errors until you pick one.
kubectl logs -l app=payments-api --tail=50 --prefix
kubectl logs -l app=payments-api -f --max-log-requests=10--prefix labels each line with its Pod name, which is essential when reading three replicas at once. Following many Pods is capped by --max-log-requests (default 5).
For serious multi-Pod log reading, stern remains better than anything built in.
kubectl logs reads files on the node written by the container runtime from the container's stdout and stderr. Two consequences:
--previous reaches back exactly one container generation, not further. Anything you need to keep requires a log shipper.kubectl exec -it payments-api-7d4f-x8k2 -- sh
kubectl exec -it payments-api-7d4f-x8k2 -c api -- bash
kubectl exec payments-api-7d4f-x8k2 -- env # single command, no TTY
kubectl exec payments-api-7d4f-x8k2 -- ls /configThe -- separator matters. Everything after it goes to the container, so without it flags like -l are interpreted by kubectl instead.
Use sh rather than bash by default. Most slim images have no bash.
Things worth checking from inside a container:
kubectl exec <pod> -- env | sort # what config did it actually get?
kubectl exec <pod> -- cat /config/app.yaml # is the ConfigMap mounted?
kubectl exec <pod> -- wget -qO- localhost:8080/healthz # does the probe endpoint work?
kubectl exec <pod> -- nslookup payments-db # does DNS resolve?That first one settles a great many arguments. Checking what the container actually received, rather than what the manifest says, is usually faster than reasoning about the manifest.
kubectl exec needs a shell inside the container. Distroless and scratch images deliberately have none:
OCI runtime exec failed: exec failed: unable to start container process:
exec: "sh": executable file not found in $PATH
The answer is an ephemeral debug container. A temporary container added to the running Pod, sharing its network namespace:
kubectl debug -it payments-api-7d4f-x8k2 \
--image=nicolaka/netshoot \
--target=api--target=api shares the target container's process namespace so you can see its processes. nicolaka/netshoot carries curl, dig, tcpdump, netstat and most of what you would want.
The Pod is unchanged when you exit. Nothing is rebuilt, nothing restarts.
To debug a Pod that will not start at all, copy it with a different command:
kubectl debug payments-api-7d4f-x8k2 -it --copy-to=debug-pod --container=api -- shAnd for a node:
kubectl debug node/node-1 -it --image=nicolaka/netshootkubectl port-forward pod/payments-api-7d4f-x8k2 8080:8080
kubectl port-forward service/payments-api 8080:80
kubectl port-forward deployment/payments-api 8080:8080Tunnels a local port through the API server to something in the cluster. This is the right way to reach an internal service from a laptop, no Ingress, no NodePort, no exposing anything.
# Reach a database locally
kubectl port-forward -n payments service/postgres 5432:5432
psql -h localhost -U paymentsForwarding to a Service picks one Pod and stays with it; it is not load balanced. Forwarding to a Deployment picks one of its Pods.
Notes worth knowing: 0.0.0.0:8080:8080 as the address binds all interfaces rather than just localhost, the connection drops when the Pod is replaced, and it only handles TCP.
kubectl cp payments-api-7d4f-x8k2:/var/log/app.log ./app.log
kubectl cp ./fixture.json payments-api-7d4f-x8k2:/tmp/fixture.json
kubectl cp <pod>:/path ./local -c api # pick a containerkubectl cp requires tar inside the container, so it fails on distroless images just as exec does.
kubectl get pods. What state is it in?kubectl describe pod <name>. Do the events explain it?kubectl logs <name> --previous. What did the failing run say?kubectl exec <name> -- env. Did it get the configuration you expected?kubectl debug, if there is no shell, or you need network tools.Most problems are answered by steps 2 and 3.
| Task | Command |
|---|---|
| Logs | kubectl logs <pod> |
| Crashed container logs | kubectl logs <pod> --previous |
| Follow | kubectl logs <pod> -f |
| One container | kubectl logs <pod> -c <container> |
| Across Pods | kubectl logs -l app=<label> --prefix --tail=50 |
| Shell | kubectl exec -it <pod> -- sh |
| One command | kubectl exec <pod> -- env |
| Debug a distroless Pod | kubectl debug -it <pod> --image=nicolaka/netshoot --target=<c> |
| Debug a node | kubectl debug node/<node> -it --image=nicolaka/netshoot |
| Local access | kubectl port-forward service/<n> 8080:80 |
| Copy a file out | kubectl cp <pod>:/path/file ./file |
kubectl logs --previous is the crash-debugging command. Plain logs shows the container that just started.--previous reaches back exactly one generation.-- separates kubectl's flags from the container's command.kubectl exec <pod> -- env answers most "why is it misconfigured" questions immediately.kubectl debug with an ephemeral container.port-forward is how you reach an internal Service without exposing it.<MCQExercise exercise={{ id: 'k8s-execlogs-previous', title: 'Empty logs on a crash loop', description: 'A Pod is in CrashLoopBackOff. kubectl logs returns nothing at all. What is the explanation?', options: [ { id: 'a', text: 'Plain kubectl logs reads the container starting now, which has produced no output yet, --previous reads the run that crashed' }, { id: 'b', text: 'Logs are unavailable for Pods in CrashLoopBackOff' }, { id: 'c', text: 'The container writes to a log file, so nothing reaches stdout' }, { id: 'd', text: 'The log files were rotated away by the container runtime' }, ], correctId: 'a', explanation: 'During a backoff Kubernetes waits before the next attempt, so the current container instance is either not started or only moments old and has written nothing. The output you want belongs to the terminated instance, which --previous retrieves. This is the single most common stumbling block when debugging crash loops.', wrongExplanations: { b: 'Logs remain fully available in that state. It is which container instance you are reading that changes.', c: 'This is a real and separate problem worth ruling out, but it would give empty output consistently rather than only during a crash loop, and --previous would be empty too.', d: 'Rotation applies to long-running containers producing large volumes. A container that dies seconds after starting has nothing to rotate.', }, }} />
<MCQExercise exercise={{ id: 'k8s-execlogs-distroless', title: 'No shell in the container', description: 'kubectl exec into a distroless production container fails with "exec: sh: executable file not found in $PATH". What is the right approach?', options: [ { id: 'a', text: 'Use kubectl debug to attach an ephemeral container with a shell and tooling, sharing the Pod's namespaces' }, { id: 'b', text: 'Rebuild the image on a base that includes a shell and redeploy it' }, { id: 'c', text: 'Use kubectl cp to copy a shell binary into the container first' }, { id: 'd', text: 'Use kubectl logs, since exec is impossible for distroless images' }, ], correctId: 'a', explanation: 'kubectl debug adds a temporary container to the running Pod with an image of your choosing, sharing its network namespace and, with --target, its process namespace. You get curl, dig and tcpdump against the live workload without changing the image, restarting anything, or weakening the security properties distroless was chosen for.', wrongExplanations: { b: 'This works but discards the running state you were trying to inspect, requires a deploy, and permanently enlarges the attack surface of a production image.', c: 'kubectl cp itself needs tar inside the container, which distroless images also lack, so the copy fails before you get anywhere.', d: 'Logs are useful but limited to what the application chose to print. Ephemeral containers make exec-style debugging entirely possible.', }, }} />
kubectl exec -it <pod> -- sh, adding -c <container> when the Pod has more than one. Use sh rather than bash, since most slim images do not include bash. The -- separator is required so that everything after it is passed to the container instead of being interpreted by kubectl. For a Pod with several containers, kubectl will not guess and returns an error until you name one.
Use kubectl debug -it <pod> --image=nicolaka/netshoot --target=<container>. This attaches an ephemeral container to the running Pod that shares its network namespace, and with --target its process namespace, giving you a full toolkit against the live workload without modifying the image or restarting anything. It is the standard answer for distroless and scratch images, and the Pod returns to normal when you exit.
It opens a tunnel from a local port, through the API server, to a port on a Pod or Service in the cluster, so kubectl port-forward service/payments-api 8080:80 makes the Service reachable at localhost:8080. This is the correct way to reach an internal service or database from a laptop without creating an Ingress or changing the Service type. Forwarding to a Service selects a single Pod and is not load balanced, and the connection drops when that Pod is replaced.
Because the application writes to a file inside the container rather than to stdout or stderr. The container runtime only captures those two streams, and that is all kubectl logs can read. The fix is to configure the application to log to stdout, ideally as structured JSON, which also makes logs collectable by a shipper such as Promtail or Fluent Bit. Writing to files inside containers means losing them entirely when the Pod is removed.
Yes, with a label selector: kubectl logs -l app=payments-api --tail=50 --prefix. The --prefix flag labels each line with its Pod name, which is necessary to make sense of interleaved output. Following multiple Pods is capped by --max-log-requests, five by default. For sustained multi-Pod log watching, stern handles it considerably better, with colouring per Pod and automatic pickup of new Pods as they appear.
Learn how kubeconfig works, switch clusters with kubectl config use-context, set a default namespace, merge kubeconfig files and avoid the wrong cluster.
Learn what a Kubernetes Pod is, what its containers share, why pod IPs change, how to write one, and how to read the statuses you see when debugging.
Write a Kubernetes Pod manifest with confidence. Walks through apiVersion, metadata, the container spec, ports and securityContext with a full example.