Docker 基础体系 · 第 23/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker Inspect、Exec 与调试:Namespace、环境、进程和最小镜像

Docker 容器调试的核心问题通常不是“应该再执行哪条命令”,而是先确认:你正在观察哪个对象、哪个进程、哪个 Linux namespace,以及某个配置是否真的进入了运行中的进程。

docker inspectdocker exec 和 Linux 的 /proc 分别处在不同层次:

  • docker inspect 读取 Docker Engine 保存的对象元数据和运行时状态;
  • docker exec 请求 Engine 在一个已运行的容器中创建新进程;
  • /proc 反映某个进程实际看到的环境、文件描述符、namespace 和状态;
  • docker top 或宿主机上的进程工具观察进程列表,但观察视角取决于 PID namespace;
  • 最小镜像可能没有 Shell、psip 或包管理器,因此调试方法必须与镜像内容区分开。

以下内容以现代 Docker Engine、BuildKit 和 Compose 规范为基础,讨论 Linux 容器。Windows 容器使用不同的隔离机制,不能直接套用本文的 namespace 和 /proc 结论。

一、先区分 Docker 对象、配置和运行时状态

Docker 中至少有四个容易混淆的层次:

  1. Image(镜像):不可变内容和默认配置的模板;
  2. Container(容器):基于镜像创建的可变运行实例;
  3. 进程:容器中的具体 Linux 进程,例如 PID 1 和通过 docker exec 创建的 Shell;
  4. Docker Engine 状态:Engine 记录的容器状态、网络端点、挂载、日志驱动等信息。

可以用下面的关系表示:

flowchart TD
    I[Image 镜像<br/>RootFS 与默认配置] --> C[Container 容器<br/>配置与可变层]
    C --> R[Runtime State<br/>运行状态、宿主机 PID、网络端点]
    R --> P1[PID 1]
    R --> PE[exec 创建的进程]
    P1 --> PR[/proc 中的实际进程状态]
    PE --> PR

镜像中的 ENV, CMD, ENTRYPOINT, WORKDIR 是默认值。创建容器时,Docker 将镜像默认值与 docker run 或 Compose 配置合并,形成容器配置。容器启动后,容器中的进程还可以自行修改自己的环境、工作目录、文件描述符和子进程状态。

因此,下面三个问题不是同一个问题:

镜像默认配置是什么?
容器创建时配置了什么?
当前 PID 1 实际运行成了什么?

对应的观察方法也不同:

docker image inspect IMAGE
docker inspect CONTAINER
docker exec CONTAINER ...

docker inspect 的输出通常是 JSON 数组,即使只查询一个对象也会看到数组结构:

docker inspect alpine:3.20
docker inspect my-container

如果只需要某个字段,应使用格式化输出,避免人工阅读大段 JSON:

docker inspect \
  --format '{{.State.Status}} pid={{.State.Pid}} exit={{.State.ExitCode}}' \
  my-container

这里的字段含义是:

  • .State.Status:Engine 记录的状态,例如 createdrunningexited
  • .State.Pid:容器当前运行时 PID 1 在宿主机 PID namespace 中的 PID;容器停止后通常为 0
  • .State.ExitCode:容器主进程退出码;
  • .Config:容器创建时使用的配置;
  • .HostConfig:宿主机侧配置,例如资源限制、挂载和网络模式;
  • .NetworkSettings:Docker 网络层记录的网络信息;
  • .Mounts:挂载信息。

字段是 Go 模板路径,不同对象的字段并不完全相同。对镜像使用容器专属的 .State 会失败,因此先确定对象类型,再选择字段。

二、docker inspect 能证明什么,不能证明什么

1. Inspect 读取的是 Engine 视角

例如:

docker run -d \
  --name inspect-demo \
  --env APP_MODE=debug \
  --env FEATURE_X=off \
  alpine:3.20 \
  sleep 1000000

查看容器配置:

docker inspect --format '{{json .Config.Env}}' inspect-demo

可能得到:

["APP_MODE=debug","FEATURE_X=off","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]

查看状态:

docker inspect \
  --format 'status={{.State.Status}} running={{.State.Running}} pid={{.State.Pid}}' \
  inspect-demo

典型输出:

status=running running=true pid=27184

这个 PID 是宿主机看到的 PID,不是容器内看到的 PID。因为容器可能使用独立的 PID namespace,所以同一个进程可以同时拥有:

宿主机 PID namespace:27184
容器 PID namespace:1

可以从容器内部验证:

docker exec inspect-demo sh -c 'printf "container PID 1: "; cat /proc/1/comm'

预期类似:

container PID 1: sleep

宿主机上则可以查看该进程:

PID=$(docker inspect -f '{{.State.Pid}}' inspect-demo)
ps -o pid,ppid,stat,comm,args -p "$PID"

2. Inspect 中的环境不是任意时刻的完整事实

docker inspect .Config.Env 表示容器配置中的环境变量。它不能保证:

  • PID 1 仍然保留所有变量;
  • 应用没有调用 unsetenvsetenv
  • docker exec 创建的进程与 PID 1 使用完全相同的环境;
  • 某个变量不是由应用启动脚本后来计算出来的;
  • 秘密没有出现在环境变量中。

直接观察 PID 1 的环境:

docker exec inspect-demo sh -c \
  'tr "\0" "\n" < /proc/1/environ | sort'

/proc/1/environ 使用 NUL 字节分隔环境变量,因此不能直接用普通换行读取。tr 将 NUL 转成换行只是为了显示。

对比一个新建的 exec 进程:

docker exec inspect-demo sh -c \
  'printf "shell PID=%s\n" "$$"; tr "\0" "\n" < /proc/$$/environ | sort'

通常它会继承容器的默认环境,但不能把这种继承理解为永久规范保证。docker exec 还可以向新进程追加或覆盖环境:

docker exec \
  --env APP_MODE=temporary \
  --env DEBUG=1 \
  inspect-demo \
  sh -c 'printf "APP_MODE=%s DEBUG=%s\n" "$APP_MODE" "$DEBUG"'

这里的修改只作用于这次 exec 创建的进程及其子进程,不会修改容器配置,也不会改变 PID 1 的环境。

3. 环境变量的来源有优先关系,但要分清 Compose 插值

一个常见模型是:

镜像 ENV 默认值
    ↓
容器创建时的 docker run -e / Compose environment
    ↓
exec 时的 --env 覆盖或追加
    ↓
程序启动逻辑再次修改

不过 Compose 还有一个容易混淆的步骤:变量插值。例如:

services:
  app:
    image: alpine:3.20
    command: ["sh", "-c", "sleep 1000000"]
    environment:
      APP_MODE: "${APP_MODE:-production}"

${APP_MODE:-production} 是 Compose 在创建容器前解析的配置表达式,不是容器内 Shell 在运行时解析的表达式。可以先查看 Compose 最终模型:

docker compose config

再查看已创建容器的实际配置:

docker compose ps
docker inspect <实际容器名> \
  --format '{{json .Config.Env}}'

这两个步骤分别回答:

Compose 将要提交给 Engine 的配置是什么?
Engine 已经保存到容器对象中的配置是什么?

若应用仍然表现异常,还要进入进程内部检查 /proc/<pid>/environ,因为启动脚本可能覆盖配置,或者应用的配置优先级并不由环境变量决定。

三、Namespace:exec 到底进入了什么环境

1. Namespace 是 Linux 的隔离视图

Linux namespace 不是“容器目录”,而是内核为进程提供的一组资源视图。常见类型包括:

  • PID namespace:进程编号和进程树;
  • Mount namespace:挂载点和根文件系统视图;
  • Network namespace:网络设备、路由表、端口空间;
  • UTS namespace:主机名和域名;
  • IPC namespace:System V IPC、POSIX 消息队列等;
  • User namespace:用户和组 ID 映射;
  • Cgroup namespace:进程看到的 cgroup 层级视图。

普通 Linux 容器通常至少使用 PID、Mount、Network 和 UTS 等隔离。具体配置取决于启动参数、Docker 默认行为、rootless 模式和安全配置。

docker exec 创建的进程会加入目标容器的运行时 namespace,因此它通常会看到:

  • 容器自己的根文件系统;
  • 容器的 PID namespace;
  • 容器的网络接口和路由;
  • 容器的主机名;
  • 容器的挂载视图;
  • 容器所在的 cgroup 资源控制范围。

exec 并不是“打开一个已经存在的 Shell”。它会创建一个新的进程,并使用指定的用户、工作目录和环境启动该进程。

docker exec \
  --user 0 \
  --workdir /tmp \
  --env CHECK=1 \
  inspect-demo \
  sh -c 'id; pwd; printf "CHECK=%s\n" "$CHECK"'

参数的作用分别是:

  • --user 0:以容器内 UID 0 启动该进程;
  • --workdir /tmp:设置该进程初始工作目录;
  • --env CHECK=1:只为该次 exec 设置环境变量;
  • sh -c ...:让容器内的 Shell 解释整段命令。

如果容器没有运行,docker exec 会失败:

docker stop inspect-demo
docker exec inspect-demo true

典型错误类似:

container ... is not running

原因在于 exec 依赖一个仍然存在的容器运行时和 namespace。它不能对已经退出的容器“重新进入”。退出容器的文件系统和配置仍可通过 docker inspectdocker logsdocker cp 等方式检查,但不能使用普通 docker exec

2. docker exec 不会替换 PID 1

容器的生命周期通常由 PID 1 决定。下面的容器会因 sleep 结束而退出:

docker run --name short-lived alpine:3.20 sleep 2
docker inspect --format '{{.State.Status}} exit={{.State.ExitCode}}' short-lived

sleep 仍运行时执行:

docker exec short-lived sh

只会创建一个子进程。退出这个 Shell 不会停止 sleep,Shell 也不会成为 PID 1。

相反,如果应用是 Shell 脚本:

FROM alpine:3.20
COPY start.sh /start.sh
ENTRYPOINT ["/start.sh"]

start.sh 内容是:

#!/bin/sh
some-server

则服务器可能成为 Shell 的子进程,而不是 PID 1。更稳妥的写法是:

#!/bin/sh
set -eu
exec some-server

exec 在这里是 Shell 内建命令,作用是用 some-server 替换 Shell 进程。这样信号和退出码更直接地到达真正的服务进程。Docker 的 docker exec 与脚本中的 Shell exec 是两个不同概念,不能混为一谈。

3. Namespace 不是完整的安全边界

Namespace 隔离了资源视图,但容器安全还依赖:

  • Linux capabilities;
  • seccomp;
  • AppArmor 或 SELinux;
  • user namespace;
  • cgroup 和设备权限;
  • Docker socket 的访问控制;
  • 挂载目录权限;
  • 内核本身的安全性。

例如,容器中的 UID 0 在默认配置下通常具有容器内的 root 身份,但这不等于宿主机文件系统上的无限制 root。另一方面,如果把宿主机的 /var/run/docker.sock 挂载进容器,能够访问该 socket 的进程往往可以请求 Engine 创建特权容器,这会显著扩大权限边界。

因此,下面的判断是不充分的:

容器里 id 显示 uid=0,所以它一定能访问宿主机。
容器里看不到宿主机进程,所以容器一定安全。

正确做法是同时查看运行用户、Capabilities、挂载、网络模式和安全配置:

docker inspect \
  --format 'user={{.Config.User}} privileged={{.HostConfig.Privileged}} pidmode={{.HostConfig.PidMode}} networkmode={{.HostConfig.NetworkMode}}' \
  inspect-demo

docker inspect \
  --format '{{json .HostConfig.CapDrop}}' \
  inspect-demo

docker inspect \
  --format '{{json .Mounts}}' \
  inspect-demo

--privileged--pid=host--network=host 等选项会改变隔离边界。调试时临时使用也应记录、验证并及时撤销,而不应把它们作为“让命令能运行”的默认修复。

四、网络调试:Inspect 的地址不是应用可达性的证明

查看容器网络信息:

docker inspect \
  --format '{{range .NetworkSettings.Networks}}{{.NetworkID}} {{.IPAddress}} {{.Gateway}} {{.MacAddress}}{{"\n"}}{{end}}' \
  inspect-demo

这个结果来自 Docker 网络对象和端点配置,可以帮助确认:

  • 容器连接了哪些网络;
  • Engine 分配的 IP 地址;
  • 网关和 MAC 地址;
  • 是否连接到了预期的 Compose 网络。

但它不能证明应用正在监听,也不能证明端口从客户端可达。至少要区分三层:

应用是否 listen
    ↓
容器 namespace 内是否能连接
    ↓
其他容器或宿主机是否能通过发布端口连接

例如,启动一个监听服务:

docker run -d \
  --name http-demo \
  -p 18080:8080 \
  python:3.12-alpine \
  python -m http.server 8080 --bind 0.0.0.0

确认进程:

docker exec http-demo sh -c 'cat /proc/1/cmdline | tr "\0" " "; echo'

确认容器内连接时,镜像可能没有 curl。可以使用已有工具,或者从另一个包含客户端工具的容器加入同一个网络:

docker run --rm \
  --network container:http-demo \
  curlimages/curl:8.10.1 \
  http://127.0.0.1:8080/

--network container:http-demo 让临时容器加入目标容器的 network namespace,因此其中的 127.0.0.1 指向同一个网络命名空间。它不表示共享目标容器的根文件系统。

宿主机测试发布端口:

curl http://127.0.0.1:18080/

如果容器内 127.0.0.1:8080 可达,但宿主机 127.0.0.1:18080 不可达,应继续检查端口发布、宿主机防火墙、监听地址和 Docker 网络,而不是只看 .NetworkSettings.IPAddress

五、进程调试:PID、状态、信号和退出码

1. 用 docker top/proc 查看进程

docker top inspect-demo

docker top 通常调用宿主机的进程查询能力显示容器进程,但输出格式依赖宿主机工具和运行时。它适合快速确认进程是否存在,不应把某一列的格式当成跨平台 API。

容器内查看 PID namespace 中的进程:

docker exec inspect-demo sh -c '
  for p in /proc/[0-9]*; do
    pid=${p##*/}
    printf "%s " "$pid"
    tr "\0" " " < "$p/cmdline" 2>/dev/null || true
    echo
  done
'

读取 /proc/<pid>/cmdline 时参数之间也是 NUL 分隔。对于内核线程、已经退出的进程或权限受限的进程,读取可能为空或失败,因此脚本中的 || true 是为了避免单个进程造成整个检查流程退出。

查看 PID 1 的状态:

docker exec inspect-demo sh -c '
  grep -E "^(Name|State|Pid|PPid|Uid|Gid|Threads):" /proc/1/status
'

关键字段包括:

  • State:运行、睡眠、僵尸等状态;
  • PPid:父进程 PID;
  • UidGid:进程身份;
  • Threads:线程数。

若容器状态是 running,但服务不可用,可能是 PID 1 仍然存在而实际工作线程已经卡住、子进程退出,或者服务只监听了错误的地址。单看 State.Running=true 不能证明应用健康。

2. 信号行为与 PID 1

停止容器时,Docker 会向容器主进程发送停止信号,等待一段时间后再强制终止。实际信号和超时时间受停止参数、镜像配置和 Engine 行为影响。

可以检查镜像和容器配置中的停止信号:

docker inspect \
  --format 'stopSignal={{.Config.StopSignal}} stopTimeout={{.Config.StopTimeout}}' \
  inspect-demo

Shell 作为 PID 1 时,信号转发、子进程回收和退出码处理常常与直接运行服务不同。即使 docker exec 能进入容器,也不能据此推断服务能够正确响应 SIGTERM。

exec 进程发送信号只影响该进程。例如:

docker exec inspect-demo sh -c 'trap "echo got-term >&2; exit 42" TERM; sleep 1000' &

这个 Shell 是新进程,不是容器主进程。停止容器时,Docker 关心的是 PID 1 的生命周期,而不是这个临时调试进程的退出码。

3. 退出码必须和日志、事件一起解释

查看退出码和错误:

docker inspect \
  --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{json .State.Error}}' \
  my-container

docker logs --timestamps my-container
docker events --filter container=my-container

几种常见情况:

  • ExitCode=0:进程正常退出,不代表服务“应该一直运行”;
  • 非零退出码:应用或启动脚本报告错误;
  • OOMKilled=true:容器曾被内核因内存压力杀死;
  • State.Error 非空:可能是启动、运行时或挂载层面的错误;
  • 没有日志:应用可能尚未启动、日志写到了文件、日志驱动不可用,或容器根本没有进入预期代码路径。

OOM 诊断还要查看资源限制:

docker inspect \
  --format 'memory={{.HostConfig.Memory}} swap={{.HostConfig.MemorySwap}} oomkilldisable={{.HostConfig.OomKillDisable}}' \
  my-container

数值通常以字节表示;0 常表示未设置该项,但具体字段语义应结合 Engine 版本和配置解释。容器被杀死后,进入容器检查 /proc 已经没有意义,应优先保存 inspect、日志、事件和宿主机内核日志。

六、为什么最小镜像经常“无法调试”

最小镜像的目标是减少运行时内容,而不是提供完整运维环境。典型镜像可能缺少:

/bin/sh
ps
ip
curl
wget
ping
ca-certificates
包管理器
调试符号

因此下面的命令失败并不一定表示服务失败:

docker exec my-distroless sh

可能的错误是:

exec: "sh": executable file not found in $PATH

这只能证明目标镜像中找不到 sh,不能证明容器未运行。应先用 Engine 层信息确认状态:

docker inspect \
  --format 'status={{.State.Status}} pid={{.State.Pid}} image={{.Config.Image}}' \
  my-distroless

如果镜像有 /proc,可以不依赖 ps

docker exec my-distroless \
  sh -c 'cat /proc/1/status'

但这仍然要求镜像有 Shell。没有 Shell 时,若镜像中存在可执行工具,可以直接执行其绝对路径;否则需要使用外部调试策略。

1. 在构建阶段提供调试变体

可以把构建逻辑拆为生产目标和调试目标:

# syntax=docker/dockerfile:1

FROM alpine:3.20 AS runtime
COPY app /app
ENTRYPOINT ["/app"]

FROM runtime AS debug
RUN apk add --no-cache busybox-extras curl iproute2

使用 BuildKit 构建:

docker buildx build --target runtime -t example/app:runtime .
docker buildx build --target debug -t example/app:debug .

这里的 debug 镜像包含诊断工具,但生产镜像仍保持较小。调试变体必须尽量保持相同的应用、用户、工作目录、证书和配置,否则“调试镜像能运行”可能只是环境差异造成的假象。

2. 用临时容器共享部分 namespace

网络问题可以使用:

docker run --rm \
  --network container:TARGET \
  curlimages/curl:8.10.1 \
  http://127.0.0.1:PORT/

进程问题可以使用:

docker run --rm -it \
  --pid container:TARGET \
  alpine:3.20 \
  sh

但这里有重要边界:

  • --pid=container:TARGET 共享 PID namespace,不自动共享根文件系统;
  • --network=container:TARGET 共享 Network namespace,不自动共享文件系统;
  • 临时容器的 Mount namespace 默认仍不同;
  • 目标容器必须处于运行状态;
  • 查看其他进程可能受到权限、hidepid、Capabilities 或安全策略限制;
  • 使用不同用户运行调试容器可能无法读取目标进程的 /proc 文件。

如果目标服务的文件位于自己的容器可写层中,临时容器通常看不到这些文件。共享卷也只能提供卷中的内容,不能把目标容器的整个 rootfs 变成临时容器的 rootfs。

3. 使用宿主机 nsenter 时要理解风险

先取得宿主机 PID:

PID=$(docker inspect -f '{{.State.Pid}}' TARGET)

在 Linux 宿主机上,管理员可以使用 nsenter 进入目标进程的 namespace:

sudo nsenter -t "$PID" -m -u -i -n -p -- sh

这条命令的含义是:

  • -t "$PID":以目标进程为 namespace 参照;
  • -m:Mount namespace;
  • -u:UTS namespace;
  • -i:IPC namespace;
  • -n:Network namespace;
  • -p:PID namespace;
  • -- sh:在进入后的环境中运行 Shell。

这不是 Docker API 的跨平台保证,而是宿主机 Linux 工具提供的诊断手段。它需要足够的宿主机权限,并且命令可能直接修改容器的文件系统、网络或进程状态。生产环境中应限制使用范围,并在操作前确认目标 PID 没有因容器重启而发生变化。

容器重启后,旧的宿主机 PID 对应的进程已经结束:

旧 PID → 旧 namespace → 已失效
新 PID → 新 namespace → 当前容器

因此,获取 PID、检查状态、进入 namespace 之间存在竞态。高风险操作前应再次执行 docker inspect 验证容器 ID、状态和 PID。

七、Compose 中的 execrun 和容器身份

对于 Compose 服务,常用命令是:

docker compose exec SERVICE sh

它进入该服务的一个已运行容器。如果服务有多个副本,应使用服务容器列表确认目标:

docker compose ps

Compose 的 exec 默认可能分配 TTY,脚本中通常需要:

docker compose exec -T SERVICE command

-T 禁用伪终端,适合 CI 或管道处理。

不要把下面两条命令混为一谈:

docker compose exec web sh
docker compose run --rm web sh

第一条在已有 web 容器中创建新进程;第二条通常创建一个新的临时容器。新容器可能有不同的:

  • 容器 ID;
  • IP 地址;
  • 网络端点;
  • 可写层;
  • 启动命令;
  • 依赖服务连接时序。

因此,在排查“线上服务看不到端口”时用 compose run 验证,结论未必适用于原容器。要检查实际运行实例,应优先使用 compose execdocker inspectdocker logs

八、一个从启动失败到运行时异常的完整调试流程

假设服务名为 api,先确认 Compose 和容器状态:

docker compose config
docker compose ps -a

docker compose config 用于确认变量插值、合并后的服务配置和最终命令;ps -a 同时显示已经退出的容器。

取得真实容器名称:

CID=$(docker compose ps -q api)
test -n "$CID" || {
  echo "api 没有对应容器" >&2
  exit 1
}

查询状态和退出信息:

docker inspect "$CID" \
  --format '
status={{.State.Status}}
running={{.State.Running}}
exit={{.State.ExitCode}}
oom={{.State.OOMKilled}}
error={{json .State.Error}}
pid={{.State.Pid}}
'

然后读取日志和最近事件:

docker logs --timestamps "$CID"
docker events --since 10m --filter container="$CID"

如果容器已经退出,重点检查:

  • .Config.Entrypoint.Config.Cmd
  • .Config.WorkingDir
  • .Config.User
  • .Mounts
  • .HostConfig 中的权限、资源和网络配置;
  • 日志中的第一条错误,而不是最后一条连带错误。

可以把启动命令格式化出来:

docker inspect "$CID" \
  --format 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} workdir={{printf "%q" .Config.WorkingDir}} user={{printf "%q" .Config.User}}'

如果容器仍在运行,再进入容器检查实际视图:

docker exec "$CID" sh -c '
  id
  printf "cwd="
  readlink /proc/$$/cwd
  printf "hostname="
  hostname
  printf "pid1="
  cat /proc/1/comm
'

sh 不存在,就不要反复尝试进入 Shell;转而使用:

  • 直接执行镜像中已知存在的程序;
  • 调试构建目标;
  • 共享 network 或 PID namespace 的临时容器;
  • 宿主机 nsenter
  • 应用自身的健康检查、诊断端点和结构化日志。

网络验证应区分容器内和外部路径:

docker inspect "$CID" \
  --format '{{range .NetworkSettings.Networks}}{{.NetworkID}} ip={{.IPAddress}} gateway={{.Gateway}}{{"\n"}}{{end}}'

如果应用端口为 8080,先从同一 network namespace 测试监听,再从同一 Docker 网络中的其他容器测试服务名,最后从宿主机测试发布端口。每一步失败所代表的故障层不同,不能用最后一层的失败推断前面一定错误。

九、常见误解及其反例

误解一:docker inspect 显示 running,服务就一定健康

反例:

PID 1 是一个不会退出的 Shell;
真正的子进程已经退出;
容器状态仍为 running。

验证方法是检查进程树、应用健康检查和监听状态,而不是只看 .State.Running

误解二:docker exec 中看到的环境就是应用的环境

反例:

exec 进入的是新建 Shell;
启动脚本已经修改或删除了变量;
应用由不同用户或不同工作目录启动。

应分别检查:

docker inspect --format '{{json .Config.Env}}' CONTAINER
docker exec CONTAINER sh -c 'tr "\0" "\n" < /proc/1/environ'
docker exec CONTAINER sh -c 'tr "\0" "\n" < /proc/$$/environ'

误解三:容器 IP 能从任何地方访问

容器 IP 只在对应网络的可达范围内有意义。宿主机端口发布、跨网络通信、云平台安全组和防火墙都可能改变路径。127.0.0.1 更不是“本机指宿主机”,它表示当前 network namespace 的 loopback 接口。

误解四:临时调试容器已经进入目标容器

docker run --rm --network container:TARGET debug-image sh

这只共享网络 namespace。它看不到目标容器的 rootfs,也不一定看到目标进程。若要看进程,需要额外使用 --pid=container:TARGET,并满足权限和目标仍运行等条件。

误解五:最小镜像应该内置所有排障工具

这会增加攻击面、镜像体积和补丁维护范围。生产镜像可以保持最小,但必须通过日志、指标、健康检查、调试变体或受控的临时工具提供诊断路径。诊断能力应在架构上设计,而不是故障发生后临时猜测。

十、调试结果的边界与生产取舍

inspect 是 Engine 对象模型的观察接口,适合确认配置、状态、挂载、网络和资源限制;它不等于应用内部状态。

exec 是向运行中容器请求创建进程的控制操作,适合验证容器内视图,但它会改变被观察环境,例如创建新的进程、打开文件、消耗资源,甚至执行修改操作。

/proc 是进程视角的内核接口,最接近“这个进程实际看到了什么”,但它受 namespace、权限、安全策略和进程生命周期影响。

最可靠的判断通常来自多个证据的交叉:

Compose 最终配置
    +
容器 inspect 配置与状态
    +
日志和事件
    +
PID 1 与子进程的 /proc
    +
容器内或同 namespace 的网络验证
    +
宿主机资源与内核事件

这些证据必须绑定到同一个容器 ID 和同一段生命周期。容器重建或重启后,容器 ID、宿主机 PID、IP 地址、可写层和进程环境都可能变化。记录对象 ID、检查时间和命令结果,才能避免把旧容器的现象误归因于新容器。

当镜像很小、容器已退出或 Docker Daemon 本身不可用时,docker exec 可能完全不是正确工具。此时应回到 Docker 对象生命周期:先确认 Engine 是否可连接,再确认容器是否创建成功、是否启动、主进程为何退出,最后选择与目标故障层匹配的检查手段。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。