跳到主要内容

容器运行与生命周期

镜像是只读模板,容器是由镜像创建的运行实例。本篇使用 Nginx 和 Alpine 做实验,学习容器从创建到删除的完整生命周期。

完成后,你将能够:

  • 区分 runstartstoprestartrm
  • 选择前台、后台或交互式运行方式;
  • 查看日志、进程、状态和退出码;
  • 注入配置并限制 CPU、内存;
  • 理解主进程、停止信号和健康检查。

1. 容器状态与主进程

每个容器都有一个主进程,也叫 PID 1:

  • 主进程运行,容器就是 running
  • 主进程正常结束,容器变成 exited (0)
  • 主进程异常结束,通常会得到非零退出码;
  • 容器停止后,其可写层和配置仍存在,删除容器才会移除它们。

Docker 容器不是一台需要一直“开机”的小型虚拟机。不要通过在后台硬塞一个无意义进程来保持容器运行,应该让应用本身作为主进程运行。

2. 三种常见运行方式

2.1 前台运行

# 前台启动 Nginx;日志占用当前终端
docker run --name web-foreground \
--publish 8080:80 \
nginx:1.27-alpine

Ctrl+C 会向容器发送中断信号并停止它。检查状态并删除:

# -a 同时显示已经停止的容器
docker ps -a --filter name=web-foreground

# 删除已停止的实验容器
docker rm web-foreground

2.2 后台运行

Web 服务通常在后台运行:

# -d 让容器在后台运行,命令返回容器 ID
docker run --detach \
--name web \
--publish 8080:80 \
nginx:1.27-alpine

# 查看正在运行的 web 容器
docker ps --filter name=web

2.3 交互式临时容器

学习命令或临时检查网络时,常用 -it --rm

# -i 保持标准输入,-t 分配终端,--rm 在退出后自动删除容器
docker run --interactive --tty --rm \
alpine:3.20 sh

# 下面两条在容器 shell 中执行;查看系统信息后退出
cat /etc/os-release
exit

退出后执行 docker ps -a,不会看到这个 Alpine 容器,因为 --rm 已自动删除它。

3. run、create 和 start 的区别

docker run 实际组合了“创建”和“启动”两步:

# 只创建容器,不启动;此时状态为 Created
docker create --name created-demo alpine:3.20 \
sh -c 'echo "container started"; sleep 10'

# 启动已存在的容器,并附着输出;不会创建第二个容器
docker start --attach created-demo

# 删除已经退出的实验容器
docker rm created-demo

记住以下规则:

  • docker run IMAGE:创建一个新容器并立即启动;
  • docker start NAME:启动一个已存在且已停止的容器;
  • 同名容器存在时,再次 docker run --name 同名 ... 会报名称冲突;
  • 想用新镜像或新参数替换容器时,通常删除旧容器再创建。

4. 查看状态、日志和进程

确保前面的 web 容器正在运行,然后执行:

# 查看容器列表;--no-trunc 可以显示完整命令
docker ps --no-trunc --filter name=web

# 查看最近 20 行日志,并持续输出后续日志;按 Ctrl+C 只退出查看
docker logs --follow --tail 20 web

# 显示容器内当前进程,不要求镜像中安装 ps
docker top web

# 提取状态、启动时间和退出码;运行中退出码通常为 0
docker inspect web \
--format 'status={{.State.Status}} started={{.State.StartedAt}} exit={{.State.ExitCode}}'

# 查看一次 CPU、内存、网络和块设备 I/O 快照
docker stats --no-stream web

docker logs 显示的是容器主进程写到标准输出和标准错误的内容。应用应把常规日志输出到这两个流,而不是只写容器内文件。

5. 停止、重启和删除

# 先发送 SIGTERM,最多等待 10 秒,再发送 SIGKILL
docker stop --time 10 web

# 确认容器仍存在,但状态变成 Exited
docker ps -a --filter name=web

# 按原有端口、环境变量和挂载配置重新启动
docker start web

# 停止后立即重新启动;常用于让进程重新读取配置
docker restart --time 10 web

# 强制删除运行中的实验容器:先停止,再删除
docker rm --force web

应用应该处理 SIGTERM,停止接收新请求、完成正在处理的任务并退出。只有应用没有及时退出时,Docker 才会在超时后发送 SIGKILL

6. 传入环境变量

先用命令行传入一个非敏感配置:

# 容器输出环境变量后退出;--rm 自动清理
docker run --rm \
--env APP_ENV=development \
alpine:3.20 \
sh -c 'echo "APP_ENV=$APP_ENV"'

变量较多时使用环境文件。创建一个仅用于实验的 demo.env

# 环境文件采用 KEY=VALUE 格式,不写 export
APP_ENV=development
LOG_LEVEL=debug
# 从 demo.env 注入变量,并在容器中打印指定项
docker run --rm \
--env-file ./demo.env \
alpine:3.20 \
sh -c 'env | grep -E "^(APP_ENV|LOG_LEVEL)="'

环境变量会出现在容器配置中,不适合承载长期密钥。不要提交包含密码的 .env;生产环境使用平台提供的 secret 机制。

7. 覆盖默认命令

镜像通常定义默认命令。把命令写在镜像名后面,可以覆盖镜像的 CMD

# alpine 默认启动 shell;这里改为输出一句话后退出
docker run --rm alpine:3.20 \
echo 'hello from container'

# 查看镜像默认的 Entrypoint 和 Cmd
docker image inspect alpine:3.20 \
--format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}'

不要混淆两类参数:镜像名前的是 Docker 参数,镜像名后的是容器内命令或该命令的参数。

8. 进入运行中的容器

重新启动一个 Nginx 实验容器:

# 后台启动容器,本节不发布端口
docker run --detach --name inspect-web nginx:1.27-alpine

# 在运行中的容器里额外启动 sh;exit 不会停止 Nginx 主进程
docker exec --interactive --tty inspect-web sh

# 以下命令在容器 shell 中执行,然后退出
nginx -v
ls -la /usr/share/nginx/html
exit

还可以从外部执行单条命令或复制文件:

# 在容器内执行只读检查,不进入交互 shell
docker exec inspect-web cat /etc/nginx/conf.d/default.conf

# 把默认首页复制到当前宿主机目录
docker cp inspect-web:/usr/share/nginx/html/index.html ./nginx-index.html

# 查看容器可写层相对镜像有哪些新增、修改或删除
docker diff inspect-web

# 清理实验容器
docker rm --force inspect-web

docker exec 中的临时修改只属于当前容器,重建后会丢失。正式修改应进入代码、Dockerfile 或挂载配置,而不是手工改线上容器。

9. 限制资源

默认情况下,容器可能使用宿主机的大部分资源。为服务设置合理上限:

# 限制最多使用 0.5 个 CPU、128 MiB 内存和 100 个进程
docker run --detach \
--name limited-web \
--cpus 0.5 \
--memory 128m \
--pids-limit 100 \
nginx:1.27-alpine

# 查看 Docker 记录的内存与 CPU 限制
docker inspect limited-web \
--format 'memory={{.HostConfig.Memory}} nano_cpus={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}}'

# 清理实验容器
docker rm --force limited-web

内存上限过低会导致进程被 OOM Kill。限制值应根据实际观测设置,而不是盲目照抄示例。

10. 自动重启策略

常用策略如下:

策略行为
no默认值,不自动重启
on-failure[:次数]仅在非零退出码时重启,可限制次数
always无论退出原因都重启;手动停止后,Engine 重启时还会启动
unless-stopped自动重启,但手动停止后不会因 Engine 重启而启动
# 为长期服务设置 unless-stopped;它不等同于健康检查或高可用
docker run --detach \
--name restart-demo \
--restart unless-stopped \
nginx:1.27-alpine

# 查看容器当前重启策略
docker inspect restart-demo \
--format '{{.HostConfig.RestartPolicy.Name}}'

# 清理容器;删除后重启策略自然不再生效
docker rm --force restart-demo

11. 健康检查

“进程仍在运行”不代表“服务能正常响应”。Dockerfile 可以定义健康检查:

# 每 30 秒请求一次本机健康端点,连续 3 次失败标记为 unhealthy
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1/ || exit 1

也可以在运行时为支持 wget 的 Nginx Alpine 镜像添加检查:

# 启动带健康检查的容器;start-period 给服务预留启动时间
docker run --detach \
--name healthy-web \
--health-cmd='wget -q -O /dev/null http://127.0.0.1/ || exit 1' \
--health-interval=10s \
--health-timeout=3s \
--health-start-period=5s \
--health-retries=3 \
nginx:1.27-alpine

# 等待几秒后查看 Status 列中的 healthy 状态
docker ps --filter name=healthy-web

# 查看每次健康检查的退出码和输出
docker inspect healthy-web \
--format '{{json .State.Health}}'

# 清理实验容器
docker rm --force healthy-web

Docker Engine 会记录 healthyunhealthy,但不会仅因为容器不健康就自动重启它。应用仍需合理的超时、重试和外部监控。

12. 本篇练习

  1. 创建一个后台 Nginx 容器,停止后再用 docker start 启动,确认容器 ID 不变;
  2. 运行一个命令以退出码 7 结束,再从 docker inspect 中找到这个退出码;
  3. 为 Nginx 设置 64 MiB 内存限制,并用 docker stats --no-stream 查看;
  4. 比较 docker attachdocker exec 的帮助信息,思考为什么日常调试通常使用后者。

下一篇将解释容器网络、端口映射,以及两个容器如何通过名称互相访问。