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 有两个生命周期上的特殊点:

  1. Docker 默认把停止信号发送给容器的主进程;
  2. 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

此时发生了这些事情:

  1. Docker 解析镜像和运行配置;
  2. 准备或确认镜像文件系统;
  3. 创建容器元数据;
  4. 创建容器可写层等存储结构;
  5. 记录命令、环境变量、挂载、网络和重启策略;
  6. 不启动容器内主进程。

所以 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 startdocker 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

执行过程是:

  1. 创建临时容器;
  2. 启动 echo hello
  3. 将输出连接到当前终端;
  4. 进程退出;
  5. 因为指定了 --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. ENTRYPOINTCMD 与最终命令

镜像可以通过 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,但实际停止信号可能由以下配置决定:

  1. docker run --stop-signal=...
  2. Dockerfile 中的 STOPSIGNAL
  3. Compose 服务中的 stop_signal
  4. 未指定时使用默认值。

例如:

docker run \
  --name app \
  --stop-signal SIGQUIT \
  image:tag

停止信号是“请求应用开始优雅退出”,不是保证应用立即退出。应用必须安装信号处理器,停止接收新请求、完成或取消正在进行的工作、关闭连接和刷新必要数据。

2. 停止超时与 SIGKILL

可以指定停止等待时间:

docker stop --time 30 app

如果容器在等待时间内退出,Docker 不再发送强制信号。

如果超时仍未退出,Docker 会发送 SIGKILLSIGKILL 不能被捕获、阻塞或忽略,因此进程会被内核立即终止,但它没有机会执行清理逻辑。

这会导致:

  • 未刷新的应用缓冲区丢失;
  • 正在写入的业务操作被中断;
  • 临时文件或锁未清理;
  • 数据库、队列客户端或连接池没有正常关闭。

停止超时不是越大越好。它必须与应用实际的关闭协议相匹配:过短会频繁触发 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,可能表现为 143SIGKILL 编号通常为 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 不触发重启

alwaysunless-stopped 不以非零退出码为必要条件,正常退出也可能被重新启动。

3. 手动停止会影响自动重启

执行:

docker stop worker

通常表示操作者明确要求它停止。对于配置了重启策略的容器,Docker 不应在同一停止操作结束后立即把它自动拉起;alwaysunless-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、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。