Debug Kubernetes Pods with kubectl exec and kubectl debug

Use kubectl exec for running containers with a shell, then kubectl debug ephemeral containers and --copy-to for distroless images, crash loops, and isolated debug Pods.

Published

Updated

Read time 8 min read

Reviewed byDeepak Prasad

kubectl exec and kubectl debug for interactive Pod troubleshooting and ephemeral debug containers
Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package kubectl 1.36.3
Applies to Bash-compatible Linux or macOS shell with kubectl configured; any compatible Kubernetes cluster
Cert prep CKA · CKAD · CKS
Lab environment Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm
Privilege Normal user (no sudo required on the workstation)
Scope kubectl exec for commands and shells, multi-container selection, limits of exec, ephemeral debug containers with --target, distroless and pause-style images, kubectl debug --copy-to and --set-image, general debug profile, cleanup, and common errors. Does not cover node or control-plane debugging, CNI administration, packet capture depth, custom debug image builds, or production RBAC design.
Related guides Kubernetes Pod troubleshooting workflow
Kubernetes Pods and Pod lifecycle
Kubernetes init containers

kubectl exec is the fast path when the container is running and the image already has the tools you need. When the image is minimal or the process exits before you can attach, kubectl debug adds an ephemeral toolbox container or clones the Pod with a safer command. Ephemeral debug images often include the curl command for quick HTTP checks from inside the Pod network namespace.

Use the Pod troubleshooting workflow when you still need phase, events, and logs before opening an interactive session.


When to Use exec or debug

Situation Method
Container is running and contains required tools kubectl exec
Multi-container Pod requires access to one container kubectl exec -c <name>
Image has no shell or diagnostic tools Ephemeral debug container
Application crashes too quickly Copy the Pod and change its command
Original Pod must remain unchanged kubectl debug --copy-to

Prepare the Lab Namespace

Create a running nginx Pod and a pause-style Pod that has no shell:

bash
kubectl apply -f - <<'YAML'
apiVersion: v1
kind: Namespace
metadata:
  name: debug-lab
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  namespace: debug-lab
spec:
  containers:
    - name: nginx
      image: nginx:1.27.0
---
apiVersion: v1
kind: Pod
metadata:
  name: pausepod
  namespace: debug-lab
spec:
  containers:
    - name: pausepod
      image: registry.k8s.io/pause:3.9
---
apiVersion: v1
kind: Pod
metadata:
  name: mc
  namespace: debug-lab
spec:
  containers:
    - name: nginx
      image: nginx:1.27.0
    - name: side
      image: busybox:1.36
      command: ["sh", "-c", "while true; do sleep 30; done"]
YAML

Wait until nginx, pausepod, and mc report Ready:

bash
kubectl wait --for=condition=Ready pod/nginx pod/pausepod pod/mc -n debug-lab --timeout=120s
kubectl get pods -n debug-lab

Run a Command with kubectl exec

Run a single command without opening a shell:

bash
kubectl exec nginx -n debug-lab -- ls /etc/nginx

Sample output:

output
conf.d
fastcgi_params
mime.types
modules
nginx.conf

Use -- before the container command so kubectl does not confuse flags:

bash
kubectl exec nginx -n debug-lab -- printenv HOSTNAME

Sample output:

output
nginx

Check that the web server answers locally inside the Pod network namespace:

bash
kubectl exec nginx -n debug-lab -- curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:80

Sample output:

output
200

These checks confirm the process and port work from inside the Pod before you blame Service routing.


Open an Interactive Shell

Attach an interactive terminal when you need to explore manually:

bash
kubectl exec -it nginx -n debug-lab -- bash -c "echo shell-ok"

Sample output:

output
shell-ok

For a full shell session, omit the inline command:

bash
kubectl exec -it nginx -n debug-lab -- bash

Type exit or press Ctrl+D to leave the session. The nginx container keeps running.

If bash is missing, try sh. An error such as executable file not found in $PATH means the image does not ship that shell.


Select a Container in a Multi-Container Pod

List container names first:

bash
kubectl get pod mc -n debug-lab -o jsonpath='{.spec.containers[*].name}{"\n"}'

Sample output:

output
nginx side

Target the sidecar:

bash
kubectl exec mc -n debug-lab -c side -- hostname

Sample output:

output
mc

Without -c, kubectl may default to the first container or print a Defaulted container notice. Sidecar layouts are easier to reason about with explicit names—see Kubernetes sidecar example.


Understand the Limits of kubectl exec

exec only works against a running container runtime sandbox. The selected container must currently be running. During CrashLoopBackOff, it may run briefly between backoff periods, but attaching with exec is timing-dependent and often unreliable.

exec is also a poor fit when:

  • The container is in Error or a completed Job Pod
  • The process exits before you attach
  • The image has no shell (pause, distroless, or statically linked binaries only)
  • Tools such as curl, ps, or dig are not in the image
  • You should not install packages into the application container filesystem

When exec is not viable, use logs and events first (kubectl logs, Events and describe), then kubectl debug.


Add an Ephemeral Debug Container

Add a short-lived toolbox container to an existing Pod:

bash
kubectl debug pausepod -n debug-lab --image=busybox:1.36 --target=pausepod --container=debugger -- sleep 3600

Sample output:

output
Targeting container "pausepod". If you don't see processes from this container it may be because the container runtime doesn't support this feature.

Wait until the ephemeral container is running:

bash
kubectl wait pod/pausepod \
  -n debug-lab \
  --for='jsonpath={.status.ephemeralContainerStatuses[?(@.name=="debugger")].state.running.startedAt}' \
  --timeout=60s

Confirm the ephemeral container appears on the Pod spec:

bash
kubectl describe pod pausepod -n debug-lab | grep -A12 'Ephemeral Containers:'

Sample output:

output
Ephemeral Containers:
  debugger:
    Container ID:  containerd://abc123...
    Image:         busybox:1.36
    Image ID:      docker.io/library/busybox@sha256:...
    Port:          <none>
    Host Port:     <none>
    Command:
      sleep
      3600
    State:          Running
      Started:      Sun, 26 Jul 2026 22:00:00 +0530

Key points:

  • The ephemeral container joins the existing Pod and shares its network namespace.
  • --target=pausepod sets the process namespace target for tools that support it.
  • Ephemeral containers are for troubleshooting, not for shipping application code.

Attach interactively through the existing debugger container:

bash
kubectl exec -it pausepod -n debug-lab -c debugger -- sh

Debug a Distroless or Minimal Image

Minimal images often run without sh. Attempting exec shows the failure clearly:

bash
kubectl exec pausepod -n debug-lab -- sh -c "echo hi"

Sample output:

output
error: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "...": OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH

Do not install packages into pausepod. Add the ephemeral debugger from the previous section, then run tests from that container:

  • wget against http://127.0.0.1:<port>

A separate toolbox image containing curl would be required for curl-specific commands.

  • nslookup or cat /etc/resolv.conf for DNS
  • ls /proc/1/root when process-namespace sharing works for the target

The pause container keeps running unchanged while you inspect from debugger.


Create a Copy of a Pod for Debugging

Add crash Pod to lab YAML or create it before the copy step:

bash
kubectl run crash -n debug-lab --image=busybox:1.36 --restart=Never -- sh -c 'exit 1'
kubectl wait pod/crash -n debug-lab --for=jsonpath='{.status.phase}'=Failed --timeout=60s

Because the existing crash container already uses BusyBox, --image is unnecessary when changing that container's command. Create a debug copy with --container plus a replacement command:

bash
kubectl debug crash -n debug-lab --copy-to=crash-debug --container=crash -- sh -c 'sleep 600'
kubectl wait --for=condition=Ready pod/crash-debug -n debug-lab --timeout=60s

The original crash Pod stays failed while the copy runs:

bash
kubectl get pod crash crash-debug -n debug-lab

Sample output:

output
NAME          READY   STATUS    RESTARTS   AGE
crash         0/1     Error     0          62s
crash-debug   1/1     Running   0          5s

--copy-to is ideal when you need a long-lived shell but cannot change the production Pod.


Change Images in the Copied Pod

Swap the application image on the copy without touching the original:

bash
kubectl debug nginx -n debug-lab --copy-to=nginx-debug --set-image=nginx=busybox:1.36 --container=nginx -- sh -c "sleep 600"

Verify the copied Pod uses the debug image:

bash
kubectl get pod nginx-debug -n debug-lab -o jsonpath='{.spec.containers[0].image}{"\n"}'

Sample output:

output
busybox:1.36

This creates a new Pod object. It does not update the Deployment, ReplicaSet, or Pod template on the controller.


Choose a Debug Profile

kubectl debug applies a built-in security profile to ephemeral and copied containers. The default is general:

bash
kubectl debug nginx -n debug-lab --profile=general --image=busybox:1.36 -- sleep 3600

Profiles adjust the debug container security context and capabilities. Available profiles include general, baseline, restricted, netadmin, and sysadmin. Use general for this lab. Privileged node debugging profiles are out of scope for this Pod-focused chapter.

Common Problems

Symptom Likely cause Fix
unable to upgrade connection API connectivity, RBAC, or container not running Confirm Pod phase, kubectl get pod, and network path to the API server
container not found Wrong -c name or container not started yet List containers with kubectl get pod -o jsonpath; wait for Running
executable file not found No shell or binary in the image Use kubectl debug with busybox or run the binary by full path if it exists
Ephemeral debugger cannot see app processes Wrong --target or runtime lacks shared process namespace Match --target to the application container name; check runtime support
Changes disappear after restart Container filesystem is ephemeral Reproduce on a copied debug Pod; fix manifests instead of patching live containers
you must specify image on debug Missing --image when adding a container Pass --image=busybox:1.36 (or your toolbox image)

What's Next


References


Summary

You practiced the two interactive paths Kubernetes offers for Pod debugging. kubectl exec remains the right choice when the container is running and the image already includes a shell and tools such as curl. Multi-container Pods need -c, and pause-style or distroless images usually force you past exec entirely.

kubectl debug covers the gaps: ephemeral containers add a toolbox to a running Pod without rebuilding the application image, while --copy-to and --set-image clone a Pod so you can keep a crashing workload alive with sleep or swap in busybox for inspection. Neither path updates the controller template—copies and ephemeral containers are deliberately isolated from production rollout semantics.

When exec fails with executable file not found, treat that as a signal to debug externally rather than installing packages into the app container. Pair interactive sessions with the inspection order in the Pod troubleshooting workflow so you attach only after events and logs point at the right container.


Frequently Asked Questions

1. What is the difference between kubectl exec and kubectl debug?

kubectl exec runs a command inside an existing container process namespace. kubectl debug adds an ephemeral debug container to a running Pod or copies the Pod spec to a new debug Pod with a different image or command.

2. When should I use an ephemeral debug container?

Use an ephemeral container when the application image has no shell or diagnostic tools and you need curl, ps, or wget without changing the original container image or restarting the Pod.

3. Why does kubectl exec report executable file not found?

The binary you requested is not in the container image. Distroless, minimal, and pause-style images often have no shell. Run a one-off command if the binary exists, or add an ephemeral debug container with busybox or netshoot.

4. Can I remove an ephemeral debug container after troubleshooting?

You cannot delete a single ephemeral container from a running Pod. Delete and recreate the Pod, or use kubectl debug --copy-to so the original Pod stays untouched.

5. Does kubectl debug change the Deployment Pod template?

No. An ephemeral container changes only the selected Pod instance. --copy-to creates a separate standalone Pod and does not update the source Deployment, ReplicaSet, or Pod template. Kubectl constructs the copied Pod with a new name and does not copy the original controller ownership references.

6. Are changes inside kubectl exec permanent?

Filesystem changes inside a running container last until the container restarts or the Pod is replaced. They do not update the image or the controller template.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)