<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged ephemeral</title>
<link>https://coderlegion.com/tag/ephemeral</link>
<description>Powered by Question2Answer</description>
<item>
<title>Debugging Distroless Containers: kubectl debug, Ephemeral Containers, and When to Use Each</title>
<link>https://coderlegion.com/15642/debugging-distroless-containers-kubectl-debug-ephemeral-containers-and-when-to-use-each</link>
<description>
&lt;a href=&quot;https://alexandre-vazquez.com/debugging-distroless-containers/&quot; title=&quot;Debugging Distroless Containers: kubectl debug, Ephemeral Containers, and When to Use Each&quot; rel=&quot;nofollow&quot;&gt;&lt;img width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;https://mlk5tkvgfeoa.i.optimole.com/cb:s8WW.68/w:1536/h:1024/q:mauto/f:best/https://alexandre-vazquez.com/wp-content/uploads/2026/04/debugging-distroless-containers.png&quot; class=&quot;webfeedsFeaturedVisual wp-post-image&quot; alt=&quot;Developer inspecting a distroless container with magnifying glass&quot; style=&quot;display: block; margin: auto; margin-bottom: 5px;max-width: 100%;&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; title=&quot;Debugging Distroless Containers: kubectl debug, Ephemeral Containers, and When to Use Each 6&quot;&gt;&lt;/a&gt;&amp;lt;!--
SEO Title: Debugging Distroless Containers: kubectl debug, Ephemeral Containers, and When to Use Each
Meta Description: Distroless containers have no shell, no tools, no debugger. Learn every technique to debug them: kubectl debug, ephemeral containers, copy-to strategy, cdebug, and node-level access &amp;mdash; with RBAC patterns for dev and production.
Focus Keywords: kubectl debug distroless,debug distroless container kubernetes,distroless container debugging,kubectl debug ephemeral container
Suggested Slug: debugging-distroless-containers
--&amp;gt;


&lt;p&gt;The container works fine in CI. It deploys successfully to staging. Then something goes wrong in production and you type the command you always type: &lt;code&gt;kubectl exec -it my-pod -- /bin/bash&lt;/code&gt;. The response is immediate: &lt;code&gt;OCI runtime exec failed: exec failed: unable to start container process: exec: &quot;/bin/bash&quot;: stat /bin/bash: no such file or directory&lt;/code&gt;.&lt;/p&gt;



&lt;p&gt;You try &lt;code&gt;/bin/sh&lt;/code&gt;. Same error. You try &lt;code&gt;ls&lt;/code&gt;. Same error. The container image is distroless &amp;mdash; it ships only your application binary and its runtime dependencies, with no shell, no package manager, no debugging tools of any kind. This is intentional and correct from a security standpoint. It is also a significant operational challenge the first time you face it in production.&lt;/p&gt;



&lt;p&gt;This article covers every practical technique for debugging distroless containers in Kubernetes: &lt;strong&gt;kubectl debug with ephemeral containers&lt;/strong&gt; (the standard approach), &lt;strong&gt;pod copy strategy&lt;/strong&gt; (for Kubernetes versions without ephemeral container support, or when you need to modify the running pod spec), &lt;strong&gt;debug image variants&lt;/strong&gt; (the pragmatic developer shortcut), &lt;strong&gt;cdebug&lt;/strong&gt; (a purpose-built tool that simplifies the process), and &lt;strong&gt;node-level debugging&lt;/strong&gt; (the last resort with the most power). For each technique I will explain what it can and cannot do, what Kubernetes version or RBAC permissions it requires, and in which scenario &amp;mdash; developer in local, platform engineer in staging, ops in production &amp;mdash; it is the appropriate choice.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Why Distroless Breaks the Normal Debugging Workflow&lt;/h2&gt;



&lt;p&gt;Traditional container debugging assumes you can exec into the container and use shell tools: &lt;code&gt;ps&lt;/code&gt;, &lt;code&gt;netstat&lt;/code&gt;, &lt;code&gt;strace&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, a text editor. Distroless images remove all of this by design. The Google distroless project, Chainguard&amp;rsquo;s Wolfi-based images, and the broader minimal image ecosystem deliberately exclude everything that is not required to run the application. The result is a dramatically smaller attack surface: no shell means no RCE via shell injection, no package manager means no easy escalation path, fewer binaries means fewer CVEs in the image scan.&lt;/p&gt;



&lt;p&gt;The tradeoff is operational: when something goes wrong, you cannot use the tools that the process itself is not allowed to run. A Java application in &lt;code&gt;gcr.io/distroless/java17-debian12&lt;/code&gt; has the JRE and nothing else. A Go binary compiled with CGO disabled and shipped in &lt;code&gt;gcr.io/distroless/static-debian12&lt;/code&gt; has literally only the binary and the necessary CA certificates and timezone data. There is no &lt;code&gt;wget&lt;/code&gt; to download a debug binary, no &lt;code&gt;apt&lt;/code&gt; to install one, no &lt;code&gt;bash&lt;/code&gt; to run a script.&lt;/p&gt;



&lt;p&gt;Kubernetes solves this at the platform level with &lt;strong&gt;ephemeral containers&lt;/strong&gt;, added as stable in Kubernetes 1.25. The principle is that a debug container &amp;mdash; which can have a full shell and any tools you want &amp;mdash; can be injected into a running pod and share its process namespace, network namespace, and filesystem mounts without modifying the original container or restarting the pod.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Option 1: kubectl debug with Ephemeral Containers&lt;/h2&gt;



&lt;p&gt;Ephemeral containers are the canonical solution. Since Kubernetes 1.25 (stable), &lt;code&gt;kubectl debug&lt;/code&gt; can inject a temporary container into a running pod. The container shares the target pod&amp;rsquo;s network namespace by default, and with &lt;code&gt;--target&lt;/code&gt; it can also share the process namespace of a specific container, allowing you to inspect its running processes and open file descriptors.&lt;/p&gt;



&lt;p&gt;The basic invocation is:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;kubectl debug -it my-pod \
  --image=busybox:latest \
  --target=my-container&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The &lt;code&gt;--target&lt;/code&gt; flag is the critical piece. Without it, the ephemeral container gets its own process namespace. With it, it shares the process namespace of the specified container &amp;mdash; meaning you can run &lt;code&gt;ps aux&lt;/code&gt; and see the application&amp;rsquo;s processes, use &lt;code&gt;ls -la /proc/&amp;lt;pid&amp;gt;/fd&lt;/code&gt; to inspect open file descriptors, and read the application&amp;rsquo;s environment via &lt;code&gt;cat /proc/&amp;lt;pid&amp;gt;/environ&lt;/code&gt;.&lt;/p&gt;



&lt;p&gt;For a more capable debug environment, replace &lt;code&gt;busybox&lt;/code&gt; with a richer image:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;kubectl debug -it my-pod \
  --image=nicolaka/netshoot \
  --target=my-container&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;code&gt;nicolaka/netshoot&lt;/code&gt; includes &lt;code&gt;tcpdump&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;dig&lt;/code&gt;, &lt;code&gt;nmap&lt;/code&gt;, &lt;code&gt;ss&lt;/code&gt;, &lt;code&gt;iperf3&lt;/code&gt;, and dozens of other network diagnostic tools, making it the standard choice for network debugging scenarios.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;What You Can and Cannot Do&lt;/h3&gt;



&lt;p&gt;Ephemeral containers share the pod&amp;rsquo;s network namespace and, when &lt;code&gt;--target&lt;/code&gt; is used, the process namespace. This gives you:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;Full visibility into the application&amp;rsquo;s network traffic from inside the pod (tcpdump, ss, netstat)&lt;/li&gt;
&lt;li&gt;Process inspection via &lt;code&gt;/proc/&amp;lt;pid&amp;gt;&lt;/code&gt; &amp;mdash; open files, memory maps, environment variables, CPU/memory usage&lt;/li&gt;
&lt;li&gt;Access to the pod&amp;rsquo;s DNS resolution context &amp;mdash; exactly the same &lt;code&gt;/etc/resolv.conf&lt;/code&gt; the application sees&lt;/li&gt;
&lt;li&gt;Ability to make outbound network calls from the same network namespace (testing service endpoints, DNS resolution)&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;What you do &lt;em&gt;not&lt;/em&gt; get with ephemeral containers:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;&lt;strong&gt;Access to the application container&amp;rsquo;s filesystem.&lt;/strong&gt; The ephemeral container has its own root filesystem. You cannot &lt;code&gt;cat /app/config.yaml&lt;/code&gt; from the application container&amp;rsquo;s filesystem unless you access it via &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/root/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ability to remove the container once added.&lt;/strong&gt; Ephemeral containers are permanent until the pod is deleted. This is by design &amp;mdash; the Kubernetes API does not allow removing them after creation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Volume mount modifications via CLI.&lt;/strong&gt; You cannot add volume mounts to an ephemeral container via &lt;code&gt;kubectl debug&lt;/code&gt; (though the API spec supports it, the CLI does not expose this).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource limits.&lt;/strong&gt; Ephemeral containers do not support resource requests and limits in the &lt;code&gt;kubectl debug&lt;/code&gt; CLI, though this is evolving.&lt;/li&gt;
&lt;/ul&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Accessing the Application Filesystem&lt;/h3&gt;



&lt;p&gt;The most common surprise for developers new to ephemeral containers is that they cannot directly browse the application container&amp;rsquo;s filesystem. The workaround is the &lt;code&gt;/proc&lt;/code&gt; filesystem:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# Find the application's PID
ps aux

# Browse its filesystem via /proc
ls /proc/1/root/app/
cat /proc/1/root/etc/config.yaml

# Or set the root to the application's root
chroot /proc/1/root /bin/sh  # only if /bin/sh exists in the app image&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/root&lt;/code&gt; path is a symlink to the container&amp;rsquo;s root filesystem as seen from the process namespace. Because the ephemeral container shares the process namespace with &lt;code&gt;--target&lt;/code&gt;, the application&amp;rsquo;s PID is typically 1, and &lt;code&gt;/proc/1/root&lt;/code&gt; gives you full read access to its filesystem.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;RBAC Requirements&lt;/h3&gt;



&lt;p&gt;Ephemeral containers require the &lt;code&gt;pods/ephemeralcontainers&lt;/code&gt; subresource permission. This is separate from &lt;code&gt;pods/exec&lt;/code&gt;, which controls &lt;code&gt;kubectl exec&lt;/code&gt;. A common mistake is to grant &lt;code&gt;pods/exec&lt;/code&gt; for debugging purposes without realizing that ephemeral containers require an additional grant:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ephemeral-debugger
rules:
- apiGroups: [&quot;&quot;]
  resources: [&quot;pods/ephemeralcontainers&quot;]
  verbs: [&quot;update&quot;, &quot;patch&quot;]
- apiGroups: [&quot;&quot;]
  resources: [&quot;pods/attach&quot;]
  verbs: [&quot;create&quot;, &quot;get&quot;]
- apiGroups: [&quot;&quot;]
  resources: [&quot;pods&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;In production environments, this permission should be tightly scoped: time-limited via &lt;code&gt;RoleBinding&lt;/code&gt; rather than permanent &lt;code&gt;ClusterRoleBinding&lt;/code&gt;, restricted to specific namespaces, and ideally gated behind an approval workflow. The debug container runs as root by default, which can create privilege escalation paths if the application container runs as a non-root user with shared process namespace &amp;mdash; the debug container can attach to the application&amp;rsquo;s processes with higher privileges.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Option 2: kubectl debug &amp;ndash;copy-to (Pod Copy Strategy)&lt;/h2&gt;



&lt;p&gt;When you need to modify the pod&amp;rsquo;s container spec &amp;mdash; replace the image, change environment variables, add a sidecar with a shared filesystem &amp;mdash; the &lt;code&gt;--copy-to&lt;/code&gt; flag creates a full copy of the pod with your modifications applied:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;kubectl debug my-pod \
  -it \
  --copy-to=my-pod-debug \
  --image=my-app:debug \
  --share-processes&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;This creates a new pod named &lt;code&gt;my-pod-debug&lt;/code&gt; that is a copy of &lt;code&gt;my-pod&lt;/code&gt; but with the container image replaced by &lt;code&gt;my-app:debug&lt;/code&gt;. If &lt;code&gt;my-app:debug&lt;/code&gt; is your application image built with debug tooling included (or a debug variant from your registry), this lets you interact with the exact same binary in the exact same configuration as the original pod.&lt;/p&gt;



&lt;p&gt;A more common use of &lt;code&gt;--copy-to&lt;/code&gt; is to attach a debug container alongside the existing application container while keeping the original image unchanged:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;kubectl debug my-pod \
  -it \
  --copy-to=my-pod-debug \
  --image=busybox \
  --share-processes \
  --container=debugger&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;This creates the copy-pod with both the original containers and a new &lt;code&gt;debugger&lt;/code&gt; container sharing the process namespace. Unlike ephemeral containers, this approach supports volume mounts and resource limits, and the debug pod can be deleted cleanly when you are done.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Limitations of the Copy Strategy&lt;/h3&gt;



&lt;p&gt;The pod copy approach has a critical limitation: &lt;strong&gt;it is not debugging the original pod&lt;/strong&gt;. It creates a new pod that may behave differently because:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;It does not share the original pod&amp;rsquo;s &lt;strong&gt;in-memory state&lt;/strong&gt; &amp;mdash; if the issue is a goroutine leak or heap corruption that has been accumulating for hours, the fresh copy will not exhibit it immediately&lt;/li&gt;
&lt;li&gt;It creates a new Pod UID, which means any admission webhooks, network policies, or pod-level security contexts that depend on pod identity may apply differently&lt;/li&gt;
&lt;li&gt;If the original pod is crashing (&lt;code&gt;CrashLoopBackOff&lt;/code&gt;), the copy will also crash &amp;mdash; this technique does not help for crash debugging unless you also change the entrypoint&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;For crash debugging specifically, combine &lt;code&gt;--copy-to&lt;/code&gt; with a modified entrypoint to keep the container alive:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;kubectl debug my-crashing-pod \
  -it \
  --copy-to=my-pod-debug \
  --image=busybox \
  --share-processes \
  -- sleep 3600&lt;/code&gt;&lt;/pre&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Option 3: Debug Image Variants&lt;/h2&gt;



&lt;p&gt;The most pragmatic approach &amp;mdash; and the one most appropriate for developer workflows &amp;mdash; is to maintain a debug variant of your application image that includes shell tooling. Both the Google distroless project and Chainguard provide this pattern officially.&lt;/p&gt;



&lt;p&gt;Google distroless images have a &lt;code&gt;:debug&lt;/code&gt; tag that adds BusyBox to the image:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# Production image
FROM gcr.io/distroless/java17-debian12

# Debug variant &amp;mdash; identical but with BusyBox shell
FROM gcr.io/distroless/java17-debian12:debug&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Chainguard images follow a similar convention with &lt;code&gt;:latest-dev&lt;/code&gt; variants that include apk, a shell, and common utilities:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# Production (zero shell, minimal footprint)
FROM cgr.dev/chainguard/go:latest

# Development/debug variant
FROM cgr.dev/chainguard/go:latest-dev&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;If you build your own base images, the recommended approach is to use multi-stage builds and maintain separate build targets:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .

# Production: static distroless image
FROM gcr.io/distroless/static-debian12 AS production
COPY --from=builder /app/myapp /myapp
ENTRYPOINT [&quot;/myapp&quot;]

# Debug variant: same binary, with shell tools
FROM gcr.io/distroless/static-debian12:debug AS debug
COPY --from=builder /app/myapp /myapp
ENTRYPOINT [&quot;/myapp&quot;]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;In your CI/CD pipeline, build both targets and push &lt;code&gt;my-app:${VERSION}&lt;/code&gt; (production) and &lt;code&gt;my-app:${VERSION}-debug&lt;/code&gt; (debug variant) to your registry. The debug image is never deployed to production by default, but it exists and is ready to be used with &lt;code&gt;kubectl debug --copy-to&lt;/code&gt; when needed.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Security Considerations for Debug Variants&lt;/h3&gt;



&lt;p&gt;Debug image variants defeat much of the security benefit of distroless if they are used in production, even temporarily. Track usage carefully: log when debug images are deployed, require explicit approval, and ensure they are removed after the debugging session. In regulated environments, consider whether deploying a debug variant to production namespaces is permitted by your security policy &amp;mdash; in many cases it is not, and you must use ephemeral containers (which add a debug process to the pod without modifying the application image) instead.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Option 4: cdebug&lt;/h2&gt;



&lt;p&gt;&lt;code&gt;cdebug&lt;/code&gt; is an open-source CLI tool that simplifies distroless debugging by wrapping &lt;code&gt;kubectl debug&lt;/code&gt; with more ergonomic defaults and additional capabilities. Its primary value is in making ephemeral container debugging feel like a native shell experience:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# Install
brew install cdebug
# or: go install github.com/iximiuz/cdebug@latest

# Debug a running pod
cdebug exec -it my-pod

# Specify a namespace and container
cdebug exec -it -n production my-pod -c my-container

# Use a specific debug image
cdebug exec -it my-pod --image=nicolaka/netshoot&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;What &lt;code&gt;cdebug&lt;/code&gt; adds over raw &lt;code&gt;kubectl debug&lt;/code&gt;:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;&lt;strong&gt;Automatic filesystem chroot.&lt;/strong&gt; &lt;code&gt;cdebug exec&lt;/code&gt; automatically sets the filesystem root of the debug container to the target container&amp;rsquo;s filesystem, so you browse &lt;code&gt;/&lt;/code&gt; and see the application&amp;rsquo;s files &amp;mdash; not the debug image&amp;rsquo;s files. This addresses the most common friction point with &lt;code&gt;kubectl debug&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker integration.&lt;/strong&gt; &lt;code&gt;cdebug exec&lt;/code&gt; works identically for Docker containers (&lt;code&gt;cdebug exec -it &amp;lt;container-id&amp;gt;&lt;/code&gt;), making it the same muscle memory for local and cluster debugging.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No RBAC complications&lt;/strong&gt; for Docker-based local development &amp;mdash; useful for developer workflows before the code reaches Kubernetes.&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;The tradeoff: &lt;code&gt;cdebug&lt;/code&gt; is a third-party dependency and requires installation. In environments with strict tooling policies (regulated industries, air-gapped clusters), it may not be an option. In those cases, the raw &lt;code&gt;kubectl debug&lt;/code&gt; workflow with &lt;code&gt;/proc/1/root&lt;/code&gt; filesystem navigation is the baseline.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Option 5: Node-Level Debugging&lt;/h2&gt;



&lt;p&gt;When everything else fails &amp;mdash; the pod is in &lt;code&gt;CrashLoopBackOff&lt;/code&gt; too fast to attach to, the issue is a kernel-level problem, or you need tools like &lt;code&gt;strace&lt;/code&gt; that require elevated privileges &amp;mdash; node-level debugging gives you direct access to the container&amp;rsquo;s processes from the host node.&lt;/p&gt;



&lt;p&gt;&lt;code&gt;kubectl debug node/&lt;/code&gt; creates a privileged pod on the target node that mounts the node&amp;rsquo;s root filesystem under &lt;code&gt;/host&lt;/code&gt;:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;kubectl debug node/my-node-name \
  -it \
  --image=nicolaka/netshoot&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;From this privileged pod, you can use &lt;code&gt;nsenter&lt;/code&gt; to enter the namespaces of any container running on the node:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# Find the container's PID on the node
# (from within the node debug pod)
crictl ps | grep my-container
crictl inspect &amp;lt;container-id&amp;gt; | grep pid

#&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15642/debugging-distroless-containers-kubectl-debug-ephemeral-containers-and-when-to-use-each</guid>
<pubDate>Fri, 24 Apr 2026 21:16:04 +0000</pubDate>
</item>
</channel>
</rss>