Debug Distroless and Scratch Containers Live with kubectl debug
Attach a full-featured ephemeral debug container to a running minimal Kubernetes pod, share its process namespace, and inspect live processes and the filesystem without touching the image or restarting anything.
What you'll build
You'll use kubectl debug to inject an ephemeral busybox (and later netshoot) container into a running scratch-based pod, share the target's PID namespace, and dig into its processes and filesystem using only tools from the debug image. The original container keeps running untouched.
Prerequisites
- Kubernetes 1.25+ (ephemeral containers are GA; GKE, EKS, and AKS enable them by default from that version)
kubectlwithin one minor version of your cluster: runkubectl versionand check both client and serverupdatepermission onpods/ephemeralcontainersin your target namespace- Cluster nodes with outbound access to pull images from Docker Hub (or a mirror)
Step 1: Deploy a minimal target pod
We need a container with no shell and no package manager. The Kubernetes pause image is a static binary in a scratch-based image and fits perfectly as a stand-in for any distroless production app.
kubectl run minimal-app \
--image=registry.k8s.io/pause:3.9 \
--restart=Never
kubectl get pod minimal-app
It'll reach Running in seconds.
Step 2: Confirm the problem
kubectl exec -it minimal-app -- /bin/sh
OCI runtime exec failed: exec failed: unable to start container process:
exec: "/bin/sh": stat /bin/sh: no such file or directory: unknown
error: command terminated with exit code 126
Same result with /bin/bash or any other shell. Nothing is there to run. This is the distroless debugging wall.
Step 3: Inject an ephemeral debug container
kubectl debug adds an ephemeral container to the live pod spec. The --target flag is what makes it useful: it tells the container runtime to run your debug container inside the same PID namespace as the named container, so you can see its processes.
kubectl debug -it minimal-app \
--image=busybox:1.36 \
--target=minimal-app \
-- sh
Wait a moment for the image pull, then you'll drop into a shell. Without --target, your debug container gets its own isolated PID namespace and the target's processes are invisible. Don't skip it.
kubectl run names the container the same as the pod (minimal-app), so --target=minimal-app is correct here. For other pods, verify with:
kubectl get pod minimal-app -o jsonpath='{.spec.containers[*].name}'
Step 4: Inspect processes and filesystem
Inside the busybox shell:
ps aux
PID USER TIME COMMAND
1 root 0:00 /pause
13 root 0:00 sh
19 root 0:00 ps aux
PID 1 is the target container's process. The mount namespace is not shared via --target, only the PID namespace. Access the target's filesystem through /proc:
ls /proc/1/root/
For the pause image this is nearly empty, but for a real distroless Go or Python app you'd see your binary, config files, certificates, everything.
# Read the target process's environment variables
cat /proc/1/environ | tr '\0' '\n'
# Check open file descriptors
ls -la /proc/1/fd/
# Resolve the running binary path
readlink /proc/1/exe
The network namespace is shared automatically because all containers in a pod share the pod's network stack. No extra flags needed for that.
Step 5: Richer network debugging with netshoot
For real network diagnosis, swap in nicolaka/netshoot. It ships ss, tcpdump, dig, curl, nmap, and more. Exit the current session first, then:
kubectl debug -it minimal-app \
--image=nicolaka/netshoot:latest \
--target=minimal-app \
-- bash
# Listening ports and active connections
ss -tlnp
# DNS resolution against the in-cluster resolver
dig kubernetes.default.svc.cluster.local
# Live packet capture on the pod interface
tcpdump -i eth0 -n port 8080
Pin to a specific netshoot version tag in CI or production tooling rather than :latest.
Verify it works
From a second terminal while the session is active:
kubectl describe pod minimal-app
Look for the Ephemeral Containers section near the bottom:
Ephemeral Containers:
debugger-xxxxx:
Image: busybox:1.36
State: Running
...
Two behaviors to internalize: ephemeral containers are appended permanently to the pod spec and can't be removed without deleting the pod. They also can't restart if they exit. If you close the shell and need another session, just run kubectl debug again; a new ephemeral container gets appended.
Troubleshooting
"ephemeral containers are disabled for this cluster"
You're on Kubernetes older than 1.25, or --feature-gates=EphemeralContainers=false was explicitly set on the API server. Upgrade the cluster or remove that flag from your API server config.
RBAC error: cannot update resource pods/ephemeralcontainers
This subresource needs its own explicit grant, separate from basic pod permissions. Check your access:
kubectl auth can-i update pods/ephemeralcontainers
If it returns no, ask your cluster admin to add update on pods/ephemeralcontainers to your role.
ps aux shows only your own shell, not the target process
You either omitted --target or the container name is wrong. Verify the container name and re-run with the correct --target value.
Debug image fails to pull on the node
Cluster nodes in restricted environments may not reach Docker Hub. Mirror busybox:1.36 or nicolaka/netshoot to your internal registry (ECR, Artifact Registry, Harbor) and reference that URL instead.
Next steps
- Copy mode for crashlooping pods:
kubectl debug -it <pod> --copy-to=debug-copy --set-image=app=<new-image>clones the pod with a different image. Useful when the original container is in CrashLoopBackOff and there's nothing live to attach to. --profile=sysadmin(kubectl 1.27+): Grants the ephemeral container elevated privileges. With it, you can runnsenter -t 1 -m -- shinside the debug container to fully enter the target's mount namespace, or attachstraceto the target PID.- Scoped RBAC: Create a
debug-roleClusterRole granting onlyupdate pods/ephemeralcontainers, bound to a short-lived service account. Keeps audit logs clean and limits blast radius. - The Kubernetes ephemeral containers reference covers the full spec fields if you need to automate injection via the API directly rather than through
kubectl debug.
Mariana covers the fast-moving world of machine learning and generative AI, with a particular focus on how these technologies are reshaping development workflows. When she isn't stress-testing the latest foundation models, she's usually at a local hackathon.
Discussion 0
No comments yet
Be the first to weigh in.