Distroless 镜像是 Google 推出的一类极简容器镜像,里面只包含运行应用所必需的运行时库,没有 shell、没有 apt、没有 ps 和 curl 这些常用工具。它的好处显而易见:攻击面小、体积小、CVE 少。但代价也很直接,当你想执行 kubectl exec -it pod -- sh 进去看看情况时,会直接收到一个错误,告诉你容器里根本没有可执行文件。这篇文章就来聊聊,面对一个没有 shell 的镜像,我们到底有哪些靠谱的调试手段。

为什么 Distroless 进不去:先理解镜像结构
先说清楚原理。kubectl exec 或 docker exec 的本质是在目标容器内启动一个新进程,这要求容器文件系统里存在对应的可执行文件。Distroless 镜像裁掉了 busybox、bash、coreutils 这些组件,所以 exec 一个 sh 时,容器运行时会报类似 exec: "sh": executable file not found in $PATH 的错误。
常见的 Distroless 基础镜像有 gcr.io/distroless/static、gcr.io/distroless/base、gcr.io/distroless/cc 等,分别对应静态编译程序、需要 glibc 的程序、需要 C++ 运行时的程序。如果你的应用是 Go 静态编译的,用 static 就够了,里面连 libc 都没有,更别提调试工具。
理解了这一点,调试思路就很清楚了:既然不能在目标容器内部跑工具,那就想办法从外部观察它,或者把带有完整工具链的容器挂到它旁边、共享它的进程空间和网络空间。下面介绍的几种方法基本都是围绕这两条路展开的。
方法一:用临时容器挂到目标 Pod(推荐的 k8s 方案)
Kubernetes 从 1.23 开始正式支持临时容器(Ephemeral Container)。它的设计初衷就是解决这种场景:目标容器缺工具、缺 shell、甚至已经 CrashLoopBackOff 无法 exec,此时往 Pod 里塞一个临时容器,共享同一个网络和 IPC 命名空间,就能对原容器进行诊断。
操作方式有两种,一种是直接用 debug 命令:
# 在目标 Pod 中启动一个带 busybox 的调试容器 kubectl debug -it my-pod --image=busybox:1.36 --target=my-app # 针对 CrashLoopBackOff 的 Pod,复制一份带调试容器的副本 kubectl debug my-pod --image=busybox:1.36 --copy-to=my-pod-debug --share-processes --interactive
参数 --target 指定目标容器名,Kubernetes 会把临时容器的进程命名空间和目标容器关联起来,这样你在 busybox 里用 ps aux 能看到目标进程,用 ls /proc/1/root 甚至可以浏览目标容器的文件系统。
也可以用声明式写法,通过 patch 注入 ephemeralContainers 字段:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
ephemeralContainers:
- name: debugger
image: busybox:1.36
command: ["sleep", "3600"]
targetContainerName: my-app
stdin: true
tty: true
需要注意,shareProcessNamespace 和 --share-processes 能看到对方进程的前提是容器运行时支持,containerd 和 CRI-O 都没问题。另外临时容器不会随 Pod 重启而重启,也不会被 kubelet 重建,用完即销毁,对生产环境影响很小。
方法二:Docker 环境下的 nsenter 与共享命名空间
如果你用的是普通 Docker 而不是 Kubernetes,思路是类似的:先找到目标容器的进程 PID,再用宿主机上的 nsenter 进入它的命名空间执行命令。这样执行的命令来自宿主机的工具集,跟容器内有没有 shell 完全无关。
# 查看目标容器主进程在宿主机上的 PID
docker inspect --format '{{.State.Pid}}' my-container
# 假设输出为 12345,进入该进程的网络命名空间执行命令
nsenter -t 12345 -n ss -tlnp
# 同时进入 mount、pid、net 命名空间,得到一个接近容器内部的 shell
nsenter -t 12345 -m -p -n -u -i /bin/bash
这种方式特别适合排查网络问题,比如确认容器内监听的端口、检查 iptables 规则的影响。因为它直接复用了宿主机的工具,功能比 busybox 还全。
另一种 Docker 原生做法是启动一个调试容器,通过 --pid 和 --network 参数挂到目标容器的命名空间:
docker run -it --rm \ --pid container:my-container \ --network container:my-container \ --cap-add SYS_PTRACE --security-opt seccomp=unconfined \ nicolaka/netshoot
这里的 --cap-add SYS_PTRACE 很关键,如果想用 strace 或 gdb 跟踪目标进程,没有 ptrace 权限是做不了的。netshoot 这个镜像内置了大量网络诊断工具,是处理容器网络问题的利器。
方法三:日志、探针与反向构建调试镜像
上面两种方法都是运行时注入,属于事后补救。更工程化的做法是在开发和生产之间架一座调试镜像的桥梁。常见方案是在 CI 中维护两个变体:生产用 Distroless,排查问题时用同一个应用二进制加一个包含 shell 的基础镜像(比如 alpine 或 debian-slim)构建 debug 版本。Docker 的多阶段构建可以复用编译产物,避免维护两套逻辑:
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./...
# 生产镜像
FROM gcr.io/distroless/static AS production
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
# 调试镜像
FROM debian:bookworm-slim AS debug
COPY --from=builder /app /app
RUN apt-get update && apt-get install -y --no-install-recommends \
curl procps strace && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/app"]
除了换镜像,还应该把力气花在可观测性上。Distroless 场景下,结构化日志输出到 stdout、暴露 Prometheus 指标、配置合理的 liveness 和 readiness 探针,这些能帮你在一开始就不需要进容器排障。Go 应用还可以借助 pprof 端点抓取 CPU 和内存 profile,定位性能问题比在容器里装 top 高效得多。
最后提醒一个细节:如果排查的核心是应用崩溃,优先看 kubectl describe pod 中的退出码和 events。退出码 139 是段错误、137 是被 OOM Kill 或 SIGKILL,这些信息往往已经足够定位方向,不一定非要进入容器内部。
各方案对比与选择建议
简单总结一下:Kubernetes 环境优先用 kubectl debug 临时容器,侵入性最小;纯 Docker 环境用 nsenter 或 netshoot 挂命名空间,功能最强;长期工程则靠多阶段构建维护 debug 镜像变体,加上完善的日志和指标体系,把进容器的需求降到最低。Distroless 的安全收益值得保留,调试难题完全可以靠这些外部手段化解。
Distroless镜像Docker调试Kubernetes排查修改时间:2026-09-07 02:56:35