Docker 基础体系 · 第 4/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker 容器生命周期:创建、启动、信号、退出、重启与清理
Docker 容器不是一个“轻量级虚拟机”,而是由一个或多个 Linux 进程、镜像文件系统、可写层、命名空间、控制组(cgroup)以及 Docker 管理元数据共同组成的运行实例。
理解容器生命周期,不能只记住几个命令:
docker create
docker start
docker run
docker stop
docker kill
docker restart
docker rm
这些命令分别改变了哪些状态、谁接收信号、退出码从哪里来、重启策略何时生效、容器删除后哪些数据仍然存在,才是排查容器问题的核心。
本文聚焦 Linux 容器。Windows 容器的进程模型、信号语义和隔离实现不同,不能直接套用本文结论。
一、先建立运行模型:容器到底是什么
1. 镜像、容器与进程不是同一个对象
可以将一个容器抽象为:
容器实例 =
镜像提供的只读文件系统层
+ 容器自己的可写层
+ 运行配置
+ 隔离环境
+ 一个被 Docker 管理的主进程
其中:
- 镜像是不可变的内容集合,通常由多个只读层组成;
- 容器是基于镜像创建的运行实例,有自己的元数据和可写层;
- 容器进程是容器实际运行的 Linux 进程;
- 卷和绑定挂载通常位于容器可写层之外;
- 网络、PID、Mount、UTS、IPC 等命名空间决定进程能看到哪些系统资源;
- cgroup限制或统计 CPU、内存、进程数等资源。
因此:
删除容器 != 删除镜像
删除容器 != 自动删除所有卷
容器退出 != 容器对象消失
容器进程退出后,容器通常仍然保留,状态变为 exited,这样 Docker 才能继续提供退出码、日志、配置和检查结果。
2. Linux 容器的主进程
Docker 为每个容器指定一个容器内的主进程,通常称为 PID 1。这里的 PID 1 是容器 PID 命名空间中的编号,不一定是宿主机上的 PID 1。
例如:
docker run --name demo alpine sleep 300
在容器内部:
docker exec demo ps
可能看到:
PID USER TIME COMMAND
1 root 0:00 sleep 300
在宿主机上,这个 sleep 会拥有另一个宿主机 PID。PID 命名空间使容器内进程看到的进程编号与宿主机隔离,但它们仍然是同一个内核中的进程,不是虚拟机中的独立内核进程。
PID 1 有两个生命周期上的特殊点:
- Docker 默认把停止信号发送给容器的主进程;
- Linux 对 PID 1 的默认信号处理存在特殊语义,并且 PID 1 还承担孤儿子进程回收责任。
如果主进程是一个 shell,而真正的服务是 shell 启动的子进程,信号可能不会正确传递到服务进程。这是很多“docker stop 等待超时”的根源。
二、从 CLI 到进程:一次容器操作经过哪些组件
现代 Docker Engine 的典型路径可以简化为:
sequenceDiagram
participant U as 用户
participant C as Docker CLI
participant D as dockerd
participant K as containerd
participant R as runc/OCI runtime
participant L as Linux 内核
U->>C: docker create/run/start
C->>D: Engine API 请求
D->>K: 创建或启动容器任务
K->>R: 调用 OCI runtime
R->>L: 创建命名空间、cgroup、进程
L-->>R: 返回进程状态
R-->>K: 返回启动或退出结果
K-->>D: 更新容器状态
D-->>C: 返回命令结果
需要区分这些组件的职责:
- Docker CLI:解析命令并调用 Docker Engine API,本身通常不直接创建 Linux 进程;
- dockerd:Docker Engine 守护进程,管理镜像、容器配置、网络、卷、日志和生命周期;
- containerd:负责较底层的容器生命周期和任务管理;
- runc:常见的 OCI runtime,实现容器进程的创建、隔离和启动;
- Linux 内核:真正提供 namespace、cgroup、信号、进程和文件系统等机制。
这条路径解释了一个常见现象:docker run 返回成功,并不意味着应用已经完成初始化;它通常只代表容器进程已经成功创建或启动。应用是否可用,还要看应用自身日志、端口监听和健康检查。
三、创建:docker create 做了什么
1. docker create 只创建容器,不启动主进程
执行:
docker create \
--name lifecycle-demo \
alpine \
sh -c 'echo started; sleep 300'
预期输出类似:
f4d...9a2
这串内容是容器 ID。此时查询:
docker ps
通常没有输出,因为默认只显示运行中的容器。使用:
docker ps -a --filter name=lifecycle-demo
可能看到:
CONTAINER ID IMAGE COMMAND STATUS NAMES
f4d...9a2 alpine "sh -c 'echo started;..." Created lifecycle-demo
此时发生了这些事情:
- Docker 解析镜像和运行配置;
- 准备或确认镜像文件系统;
- 创建容器元数据;
- 创建容器可写层等存储结构;
- 记录命令、环境变量、挂载、网络和重启策略;
- 不启动容器内主进程。
所以 docker create 后:
docker logs lifecycle-demo
通常没有任何输出,因为 echo started 还没有执行。
2. docker create 的配置多数在创建时确定
以下选项通常属于容器配置的一部分:
docker create \
--name app \
--env APP_ENV=prod \
--restart unless-stopped \
--stop-signal SIGTERM \
--stop-timeout 20 \
--memory 512m \
image:tag \
command
创建后可以查看配置:
docker inspect app
但不能把 docker start 当作重新指定命令的机制。docker start 主要是启动已经创建的容器;如果要改变镜像、命令、环境变量或挂载,通常应删除并重新创建,或者使用适合的 docker update 选项修改有限的运行配置。
3. 创建不等于镜像拉取一定已经完成
如果本地不存在指定镜像,Docker 可能在创建前拉取镜像。若拉取失败,容器可能根本不会创建;若镜像已经存在,则直接使用本地镜像。
镜像层、容器可写层和卷的关系可以表示为:
镜像只读层
↓ 作为容器根文件系统的基础
容器可写层
↓ 容器删除时通常一起删除
卷 / 绑定挂载
↓ 通常独立于容器生命周期
宿主机或 Docker volume 存储
写入容器可写层的数据不会因为容器退出而消失,但会在容器被删除时通常消失。
四、启动:docker start 与 docker run 的区别
1. docker start 启动已有容器
继续执行:
docker start lifecycle-demo
预期输出:
lifecycle-demo
此时主进程开始运行,状态变为 running:
docker inspect -f '{{.State.Status}}' lifecycle-demo
可能输出:
running
查看日志:
docker logs lifecycle-demo
可能输出:
started
docker start 不会创建新容器。它重新使用已有容器的:
- 容器 ID;
- 文件系统和可写层;
- 环境变量;
- 挂载;
- 网络配置;
- 入口点和命令;
- 日志对象。
如果进程退出,再次执行:
docker start lifecycle-demo
会重新启动同一个容器,而不是创建第二个容器。
2. docker run 通常是创建加启动
下面的命令通常等价于“创建一个新容器,然后启动它”:
docker run --name lifecycle-demo-2 alpine \
sh -c 'echo started; sleep 300'
逻辑上可以理解为:
docker run = docker create + docker start
但 docker run 还会处理前台附着、标准输入输出、自动删除等选项,因此不能简单理解为两个命令文本的机械拼接。
例如:
docker run --rm alpine echo hello
执行过程是:
- 创建临时容器;
- 启动
echo hello; - 将输出连接到当前终端;
- 进程退出;
- 因为指定了
--rm,自动删除容器。
预期输出:
hello
随后:
docker ps -a --filter name=某个自动生成的容器名
不会看到这个容器。--rm 只影响容器对象的自动清理,不等于删除镜像,也不等于任意卷都被删除。
3. 前台与后台不改变容器进程本身
docker run alpine sleep 300
默认前台运行,当前终端会等待容器退出。
docker run -d --name bg alpine sleep 300
使用 -d 后,Docker CLI 返回容器 ID,终端不再等待进程结束,但容器内仍然运行同一个 sleep 进程。
前台或后台主要改变 CLI 如何连接容器,不改变容器的 PID 1、退出码或信号来源。
五、启动命令与 PID 1:为什么 shell 写法会影响停止
1. ENTRYPOINT、CMD 与最终命令
镜像可以通过 Dockerfile 定义:
FROM alpine
ENTRYPOINT ["sh", "-c"]
CMD ["echo hello; sleep 300"]
也可以使用 shell form:
CMD echo hello; sleep 300
这两种写法可能产生不同的进程结构。对于服务容器,通常更容易正确处理信号的是 exec form:
ENTRYPOINT ["/app/server"]
CMD ["--config", "/etc/app/config.yaml"]
此时 /app/server 更可能直接成为 PID 1。
而类似下面的写法:
CMD /app/server
通常会经过 shell。若 shell 没有用 exec 替换自己,进程结构可能类似:
PID 1 /bin/sh -c /app/server
└── PID 20 /app/server
Docker 发送 SIGTERM 给 PID 1,即 /bin/sh,不保证这个信号会传递给子进程 /app/server。
2. 用 exec 让服务成为 PID 1
如果确实需要 shell 脚本作为入口,应显式使用:
#!/bin/sh
set -eu
prepare_environment
exec /app/server --config /etc/app/config.yaml
关键是最后的 exec。它会用服务进程替换 shell,使进程结构变为:
PID 1 /app/server --config ...
这并不是 Docker 专属语法,而是 Unix 进程模型中的 execve 行为。
3. PID 1 还要负责回收子进程
当父进程退出而子进程仍在运行时,子进程会成为孤儿,并由 PID 1 接管。容器内的 PID 1 如果不正确调用 wait 回收子进程,可能积累僵尸进程。
需要复杂子进程管理的场景可以使用专门的 init 进程,例如 Docker 提供的:
docker run --init --name app image:tag
--init 会在容器内加入一个轻量 init,用于信号转发和子进程回收。它不能替代应用本身的正确关闭逻辑,也不能自动修复所有 shell 信号问题,但可以改善常见的 PID 1 行为。
六、信号:停止、强制终止与信号转发
1. docker stop 的两阶段过程
对运行中的容器执行:
docker stop lifecycle-demo
典型流程是:
Docker 向容器 PID 1 发送停止信号
↓
等待配置的宽限时间
↓
若仍未退出,发送 SIGKILL
↓
容器进入 exited
默认停止信号通常是 SIGTERM,但实际停止信号可能由以下配置决定:
docker run --stop-signal=...;- Dockerfile 中的
STOPSIGNAL; - Compose 服务中的
stop_signal; - 未指定时使用默认值。
例如:
docker run \
--name app \
--stop-signal SIGQUIT \
image:tag
停止信号是“请求应用开始优雅退出”,不是保证应用立即退出。应用必须安装信号处理器,停止接收新请求、完成或取消正在进行的工作、关闭连接和刷新必要数据。
2. 停止超时与 SIGKILL
可以指定停止等待时间:
docker stop --time 30 app
如果容器在等待时间内退出,Docker 不再发送强制信号。
如果超时仍未退出,Docker 会发送 SIGKILL。SIGKILL 不能被捕获、阻塞或忽略,因此进程会被内核立即终止,但它没有机会执行清理逻辑。
这会导致:
- 未刷新的应用缓冲区丢失;
- 正在写入的业务操作被中断;
- 临时文件或锁未清理;
- 数据库、队列客户端或连接池没有正常关闭。
停止超时不是越大越好。它必须与应用实际的关闭协议相匹配:过短会频繁触发 SIGKILL,过长会延迟故障切换和部署。
3. docker kill 是直接发信号
docker kill app
默认发送 SIGKILL。也可以指定信号:
docker kill --signal=SIGUSR1 app
docker kill 不等同于“更快的 stop”。它绕过了优雅停止等待过程,适用于进程已经失控、无法响应正常停止信号的情况,但会放弃应用清理机会。
4. docker restart 不是简单的信号操作
docker restart app
通常执行:
停止现有容器
↓
等待退出或强制终止
↓
重新启动同一个容器
因此,应用仍然必须能正确处理停止信号。若停止阶段卡住,restart 也可能先等待超时再强制杀死进程。
七、退出:状态、退出码与失败路径
1. 退出后容器仍然存在
以前面的命令为例:
docker run --name once alpine sh -c 'echo work; exit 7'
终端可能显示:
work
随后检查:
docker inspect -f \
'status={{.State.Status}} exit={{.State.ExitCode}} error={{.State.Error}}' \
once
可能得到:
status=exited exit=7 error=
退出后的容器保留:
- 最后一次状态;
- 退出码;
- 结束时间;
- 日志;
- 文件系统可写层;
- 配置和元数据。
因此可以继续诊断:
docker logs once
docker inspect once
docker diff once
2. 退出码的来源
如果主进程正常调用:
exit(7);
Docker 通常记录退出码 7。
如果进程被信号终止,Unix 中常见的外部表示是:
退出码 ≈ 128 + 信号编号
例如 SIGTERM 编号通常为 15,可能表现为 143;SIGKILL 编号通常为 9,可能表现为 137。
但这只是常见约定,不应把退出码机械地当成唯一原因。特别是:
137 = 128 + SIGKILL
它可能意味着:
- Docker stop 超时后发送了
SIGKILL; - 用户执行了
docker kill; - 内核因为 OOM 终止了进程;
- 宿主机或其他管理器发送了
SIGKILL。
因此遇到 137,应结合:
docker inspect -f \
'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} status={{.State.Status}}' \
app
以及宿主机内核日志、Docker 事件和应用日志判断原因。
3. OOM 与应用主动退出不同
如果容器受到内存限制并发生 cgroup OOM,State.OOMKilled 在常见 Docker Engine 实现中可能为 true。但该字段和退出码都应作为诊断证据的一部分,而不是唯一证据。
可以查看容器限制:
docker inspect -f \
'memory={{.HostConfig.Memory}} swap={{.HostConfig.MemorySwap}}' \
app
这里的 Memory=0 通常表示未为该容器设置 Docker 层面的固定内存限制,并不意味着宿主机内存无限,也不排除系统级内存压力。
4. “进程退出”与“容器停止”的关系
默认模型下,容器的生命周期由主进程决定:
PID 1 正常退出 → 容器 exited
PID 1 被信号终止 → 容器 exited
PID 1 仍存活但服务坏 → 容器仍 running
最后一行非常重要。一个 Web 服务线程已经死掉,但 PID 1 仍然是一个没有退出的 shell,Docker 仍会认为容器在运行。Docker 的运行状态不是业务可用性状态,这也是健康检查存在的原因。
八、健康检查不等于生命周期重启
Docker 健康检查通过一个命令或探针生成:
starting → healthy
starting → unhealthy
例如:
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD wget -q -O - http://127.0.0.1:8080/health || exit 1
健康检查失败只表示探针连续失败达到规则,不会自动让 Docker 重启容器。需要区分:
主进程退出 → 容器进入 exited,可触发 restart policy
健康检查失败 → 容器通常仍为 running,是否重启取决于外部编排器
查看健康状态:
docker inspect -f \
'{{json .State.Health}}' app
健康检查命令必须从容器自己的网络和文件系统视角执行。127.0.0.1 指向容器网络命名空间内的回环地址,不是宿主机地址。
在 Compose 中,健康检查可以影响依赖服务的启动条件,但它仍然不等于 Docker Engine 的自动重启机制。Compose 还可能根据 restart、依赖条件和编排动作执行额外管理,这些行为应与 Engine 的状态分开理解。
九、重启策略:什么情况下会自动重启
1. 四种常见策略
可以在启动时指定:
docker run -d \
--name worker \
--restart on-failure:5 \
image:tag
常见策略含义如下:
| 策略 | 行为 |
|---|---|
no |
不自动重启,默认策略 |
on-failure[:N] |
仅当退出码非 0 时重启,可限制最多尝试次数 |
always |
退出后重启;Docker 守护进程重新启动后也会尝试启动 |
unless-stopped |
类似 always,但容器被明确停止后,通常不会仅因守护进程重启而重新启动 |
on-failure 的判断对象是主进程退出码,不是健康检查状态。
2. 重启策略的状态推导
假设容器使用:
--restart on-failure:3
主进程连续异常退出:
第 1 次退出码 1 → 重启尝试 1
第 2 次退出码 1 → 重启尝试 2
第 3 次退出码 1 → 重启尝试 3
第 4 次退出码 1 → 不再自动重启
如果主进程退出码为 0:
退出码 0 → on-failure 不触发重启
always 与 unless-stopped 不以非零退出码为必要条件,正常退出也可能被重新启动。
3. 手动停止会影响自动重启
执行:
docker stop worker
通常表示操作者明确要求它停止。对于配置了重启策略的容器,Docker 不应在同一停止操作结束后立即把它自动拉起;always 和 unless-stopped 在守护进程重启后的行为也存在差异。
因此排查自动重启时,应检查:
docker inspect -f \
'policy={{json .HostConfig.RestartPolicy}} status={{.State.Status}} restartCount={{.RestartCount}}' \
worker
还可以查看事件:
docker events \
--filter container=worker \
--filter event=die \
--filter event=start \
--filter event=restart
事件能帮助区分:
- 应用自身反复崩溃;
- Docker stop 后又被外部系统启动;
- 守护进程恢复时触发启动;
- 健康检查失败但主进程没有退出。
4. 重启策略不是故障修复机制
重启策略只能回答:
主进程退出后,是否再次启动?
它不能回答:
为什么进程退出?
是否已经恢复业务?
数据是否一致?
依赖服务是否可用?
启动是否会再次失败?
如果应用启动即退出,always 可能形成不断重启的循环。此时应同时检查:
docker logs --tail=200 worker
docker inspect worker
docker events --since 10m --filter container=worker
重启策略适合处理短暂进程级故障,但不能替代故障根因分析、健康检查、数据恢复和编排层的发布策略。
十、Compose 中的生命周期表达
Compose 将多个容器的配置放在一个应用模型中,但底层仍然依赖 Docker Engine 的容器生命周期。
示例:
services:
api:
image: nginx:alpine
restart: unless-stopped
stop_signal: SIGQUIT
stop_grace_period: 20s
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1"]
interval: 10s
timeout: 3s
retries: 3
启动:
docker compose up -d
查看状态:
docker compose ps
停止:
docker compose stop
删除由 Compose 创建的容器和网络:
docker compose down
需要特别区分:
docker compose stop → 停止容器,但通常保留容器和网络
docker compose down → 停止并删除 Compose 管理的容器、网络
默认情况下,down 不删除命名卷。删除卷需要显式使用:
docker compose down -v
这可能造成数据丢失,因此必须先确认卷中是否保存数据库、上传文件或其他持久数据。
Compose 中的 stop_grace_period 表示停止时的等待窗口,stop_signal 表示发送的停止信号。它们作用于容器关闭阶段,不会让一个不响应信号的应用自动实现优雅关闭。
十一、清理:停止、删除、卷和镜像的边界
1. docker rm 删除的是容器对象
对已退出容器执行:
docker rm once
会删除:
- 容器元数据;
- 容器可写层;
- 容器自身的日志和状态记录;
- 与容器绑定的临时生命周期对象。
它不会删除基础镜像:
docker image ls
也不会默认删除命名卷。
如果容器仍在运行:
docker rm once
通常会失败,因为 Docker 不希望无提示地删除正在运行的进程。可以先停止:
docker stop once
docker rm once
或者使用强制删除:
docker rm -f once
docker rm -f 会强制终止运行中的容器,效果接近发送 SIGKILL 后删除,不会等待应用优雅退出。
2. 容器可写层与卷的删除结果不同
例如:
docker volume create app-data
docker run -d \
--name volume-demo \
-v app-data:/data \
alpine sh -c 'echo persistent >/data/value; echo temporary >/tmp/value; sleep 300'
容器停止并删除:
docker rm -f volume-demo
之后重新使用同一个命名卷:
docker run --rm -v app-data:/data alpine cat /data/value
仍可能输出:
persistent
但 /tmp/value 位于容器可写层中,容器删除后不再存在。
删除命名卷:
docker volume rm app-data
此操作会删除卷中的数据。docker rm -v 主要删除与该容器关联的匿名卷;对命名卷是否删除不能依赖模糊记忆,应根据命令和卷类型明确验证。
3. 批量清理退出容器
查看所有已停止容器:
docker ps -a --filter status=exited
确认后清理:
docker container prune
Docker 会要求确认,并删除符合条件的已停止容器。更安全的做法是先按名称、标签或创建时间筛选,不要在生产环境直接把清理命令当作无条件维护动作。
镜像清理是另一类操作:
docker image prune
它不等于容器清理。网络、卷、构建缓存也各自有独立的清理对象,例如:
docker network prune
docker volume prune
docker builder prune
尤其是 docker volume prune,可能删除未被当前容器引用但仍包含业务数据的卷,风险高于普通容器清理。
十二、完整算例:从创建到退出再到清理
下面使用一个不会长期运行的容器完整观察状态变化。
1. 创建
docker create \
--name lifecycle-lab \
--restart no \
alpine \
sh -c 'echo "PID=$$"; sleep 2; exit 7'
检查:
docker inspect -f \
'status={{.State.Status}} exit={{.State.ExitCode}}' \
lifecycle-lab
预期:
status=created exit=0
此时 exit=0 不是应用成功退出,而是容器尚未运行,尚未产生真实退出结果。具体字段初始值和展示形式可能随 Engine 版本及客户端格式有所差异,真正判断“是否已经运行过”要看状态和时间字段一起确认。
2. 启动
docker start lifecycle-lab
因为没有使用 -a,命令会启动容器后返回,不一定显示容器标准输出。查看日志:
docker logs lifecycle-lab
两秒后,预期:
PID=1
再次检查:
docker inspect -f \
'status={{.State.Status}} exit={{.State.ExitCode}}' \
lifecycle-lab
预期:
status=exited exit=7
3. 再次启动同一个容器
docker start lifecycle-lab
sleep 3
docker inspect -f \
'status={{.State.Status}} exit={{.State.ExitCode}} restartCount={{.RestartCount}}' \
lifecycle-lab
由于 --restart no,它不会自动循环;手动 docker start 会再次运行同一个容器。日志可能包含两次 PID=1,因为容器内每次启动都会重新创建一个容器 PID 1,但容器对象本身的 ID 不变。
4. 删除
docker rm lifecycle-lab
此后:
docker inspect lifecycle-lab
会报找不到容器。镜像 alpine 仍然存在,除非另行删除。
这个算例展示了关键事实:
create 只建立容器对象
start 才创建并运行容器进程
进程退出后容器保留
rm 才删除容器对象和可写层
十三、常见失败表现与诊断路径
1. docker start 成功,但容器马上退出
表现:
docker start app
docker ps -a --filter name=app
状态可能是:
Exited (1)
这说明 Docker 能够创建进程,但主进程很快退出。优先检查:
docker logs app
docker inspect app
常见原因包括:
- 配置文件不存在;
- 必需环境变量缺失;
- 端口或权限错误;
- 入口点脚本执行失败;
- 应用完成一次性任务后正常退出;
- 应用收到启动阶段的依赖错误。
不要先盲目增加 restart: always,否则可能把清晰的启动错误变成重启循环。
2. docker stop 总是等待到超时
常见路径是:
docker stop
→ SIGTERM 发给 PID 1
→ PID 1 未处理或未转发信号
→ 超时
→ SIGKILL
检查容器的入口命令:
docker inspect -f \
'path={{.Path}} args={{json .Args}} stopSignal={{.Config.StopSignal}}' \
app
再检查镜像是否通过 shell 启动服务、脚本是否使用 exec、应用是否安装了 SIGTERM/SIGQUIT 处理器。
3. 容器显示 running,但服务无法访问
这通常意味着 Docker 只观察到 PID 1 仍然存在,而不代表业务端口可用。诊断顺序可以是:
docker inspect -f '{{.State.Status}}' app
docker inspect -f '{{json .State.Health}}' app
docker logs --tail=200 app
docker exec app ps
如果没有健康检查,running 只能说明主进程存在。应通过应用探针、监听端口检查或外部监控判断业务可用性。
4. 容器不断重启
先确认是否是 Engine 重启策略:
docker inspect -f \
'policy={{json .HostConfig.RestartPolicy}} count={{.RestartCount}} exit={{.State.ExitCode}}' \
app
再结合:
docker logs --tail=200 app
docker events --since 15m --filter container=app
如果退出码是 0,而策略是 always,可能是应用的正常退出与“应持续运行”的部署目标冲突。如果退出码是 137,则需要继续判断是 OOM、stop 超时还是外部强杀。
十四、Daemon、containerd 或宿主机故障时的生命周期边界
容器进程和 Docker CLI 不是同一个进程。CLI 退出,不会自动终止后台容器:
docker run -d image:tag
命令返回后,容器仍由 Docker Engine 管理。
Docker daemon 重启时,运行中的容器通常可以继续运行,因为容器进程不等于 dockerd 进程。但 daemon 恢复后如何重新发现和管理容器,是否自动启动退出容器,取决于重启策略、运行时配置以及具体 Engine 行为。
live-restore 等能力可以减少 daemon 重启对运行中容器的影响,但它不是所有故障的保护机制,也不等于容器进程脱离 Docker 生命周期。宿主机重启、内核崩溃、cgroup 资源压力、存储损坏和网络设备故障仍可能导致容器中断。
在故障排查中要区分:
应用进程故障
容器 runtime 故障
containerd 故障
dockerd/API 故障
宿主机/内核故障
例如:
docker ps无法连接,不能直接推断容器已经停止;docker ps显示running,不能证明应用健康;docker restart成功,不能证明数据或业务状态已经恢复。
十五、规范保证、实现行为与经验判断
最后需要把不同层次的结论分开:
规范或内核层面的事实
- Linux 进程有父子关系和信号机制;
- PID namespace 提供隔离后的进程编号;
- cgroup 用于资源控制和统计;
SIGKILL不能被进程捕获或忽略;- OCI 定义容器运行时配置和生命周期接口的标准边界。
Docker 常见实现行为
docker create不启动主进程;docker run通常创建并启动新容器;docker stop先发停止信号,超时后强制终止;- 主进程退出后容器保留为
exited; - 重启策略主要依据主进程退出结果;
- 健康检查失败不会单独触发 Engine 重启。
需要结合版本和配置验证的内容
- 默认停止超时时间;
- daemon、containerd 和 runtime 的具体恢复行为;
- Compose 对依赖、重启和删除的编排细节;
- 不同日志驱动、存储驱动或 rootless 模式下的边界;
- 某些字段在
docker inspect中的初始值和展示格式。
可以用以下命令检查当前环境,而不是依赖记忆:
docker version
docker info
docker inspect <container>
docker events
容器生命周期的核心因果链可以归纳为:
镜像 + 配置
↓
create:建立容器对象和文件系统视图
↓
start:创建容器进程,PID 1 运行
↓
signal:stop/kill/外部信号影响 PID 1
↓
exit:记录退出码和状态,容器对象仍保留
↓
restart:根据策略或人工操作再次启动
↓
rm/prune:删除容器及其可写层,卷和镜像另行处理
只要沿着这条链检查“哪个对象在变化、哪个进程收到了信号、哪个状态字段记录了结果”,就能把大多数 Docker 容器启动失败、优雅关闭失败、异常重启和清理误删问题还原为可验证的生命周期事件。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker 镜像与分层:内容寻址、OverlayFS、缓存和镜像体积
- 下一篇:Dockerfile 与 BuildKit:构建上下文、缓存挂载、Secret 和可重复构建
- 延伸:Docker 与 OCI 架构:CLI、Daemon、containerd、runc 和隔离边界
- 延伸:Docker 健康检查与优雅关闭:PID 1、Probe、Stop Signal 和超时
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论