容器立即退出问题的排查与解决方案

前言

最近在工作中帮助部门员工进行问题处理时,发现了多次容器退出的问题,也对大家进行了培训,发现大家对这块的技术知识没有系统的理解,本文是对大家的培训资料总结。

1. 核心技术概念

1.1 容器与主进程的关系

Docker 的设计哲学是“一个容器一个进程”。每个容器内部都有一个作为主进程的程序。Docker 守护进程会监控这个主进程:

  • 进程运行:容器保持运行状态。
  • 进程退出:容器进入退出(Exited)状态。
1.2 ENTRYPOINTCMD 指令

这两个 Dockerfile 指令共同定义了容器启动时执行的命令。

  • CMD: 提供容器运行的默认命令。如果用户在 docker run 命令后指定了其他命令,CMD 的内容会被覆盖。
  • ENTRYPOINT: 配置容器的主可执行程序。docker run 后的参数会被当作 ENTRYPOINT 指定程序的参数来传递,而不是覆盖它。
1.3 执行形式(Execution Forms)

ENTRYPOINTCMD 都有两种书写形式,它们的行为有本质区别:

 - Shell 形式 (`shell` form)
   - 语法: CMD command param1
   - 行为: 命令在  /bin/sh -c 的子 shell 中执行。这允许使用 shell 变量替换和脚本逻辑,但也会导致主进程是 sh 而不是你的应用,可能引发信号处理问题。
-  Exec 形式 (`exec` form)
   - 语法: CMD ["executable", "param1", "param2"]
   - 行为: 直接执行指定的程序,不经过 shell。这是官方推荐的最佳实践,因为:
     - 容器的主进程(PID 1)就是你的应用程序,而不是 shell。
     - 来自 docker stop 的 SIGTERM 等信号能被应用程序正确接收和处理。
1.4 ENTRYPOINTCMD 的结合使用 (Exec 形式)

这是构建灵活且可预测容器镜像的黄金法则。当 ENTRYPOINT 和 CMD 都使用 exec 形式时,CMD 的内容会作为默认参数附加到 ENTRYPOINT 命令之后。

  • 目的:将容器的核心功能(ENTRYPOINT)与默认行为(CMD)分离。

  • 行为:CMD 中定义的数组会作为参数传递给 ENTRYPOINT 中定义的命令。

  • 示例: 假设 Dockerfile 中有如下定义:

    1   ENTRYPOINT ["ping", "-c", "3"]
    

2 CMD [“localhost”]




  - 默认行为:当容器以 `docker run <image_name>` 启动时,实际执行的命令是 `ping -c 3 localhost`。
  - 参数覆盖:用户可以轻松覆盖 `CMD` 提供的默认参数。如果用户运行 `docker run <image_name> google.com`,则实际执行的命令变为 `ping -c 3 google.com`。`ENTRYPOINT` 的部分保持不变,只有 `CMD` 的部分被替换了。

- 最佳实践:使用 `ENTRYPOINT` 定义你的主程序,使用 `CMD` 定义该程序的默认参数。这使得镜像的行为清晰,并且易于通过 `docker run` 的命令行参数进行修改。

#### 2. 常见问题原因分析

##### 2.1  主进程非持续运行:

- 原因:`CMD` 或 `ENTRYPOINT` 执行的是一个短暂的任务,如 `echo "hello world"` 或一个执行完毕就退出的脚本。
- 示例:CMD echo "Setup complete"

##### 2.2  将应用作为后台进程启动:

- 原因:启动脚本使用 & 或 nohup 将主服务(如 nginx)放到后台运行。这会导致启动脚本本身(作为主进程)立即执行完毕并退出,从而关闭容器。
- 示例:`CMD ./start-server.sh &`

##### 2.3  脚本错误:

- 原因:`ENTRYPOINT` 或 `CMD` 中引用的启动脚本存在错误,例如:
      - 语法错误。
      - 文件不存在或路径错误。
      - 脚本没有可执行权限(需要 `chmod +x`)。

#### 3. 标准解决方案与调试策略


1. 确保应用在前台运行:
    - Web 服务器:使用特定参数使其在前台运行。
      - Nginx: `CMD ["nginx", "-g", "daemon off;"]`
      - Apache: `CMD ["httpd", "-D", "FOREGROUND"]`
    - 应用/脚本:如果你的应用默认在后台运行,找到使其在前台运行的配置。如果必须通过脚本启动,确保脚本最后执行的命令是前台应用,并使用 exec 命令将控制权交给它,例如 `exec node app.js`。


2. 使用 `tail -f /dev/null` 进行调试:
    - 目的:当你不确定问题所在时,用一个永远不会退出的命令强制保持容器运行,以便进入容器内部进行排查。
    - 方法:在 Dockerfile 的末尾添加 CMD ["tail", "-f", "/dev/null"]。


3. 检查容器日志:
    - 命令:`docker logs <container_id>`
    - 目的:**这是定位问题的第一步。日志会显示应用的标准输出和错误信息**,通常能直接揭示脚本错误或应用启动失败的原因。


4. 覆盖 `ENTRYPOINT` 进入容器:
    - 命令:`docker run -it --entrypoint  /bin/sh <image_name>`
    - 目的:当日志信息不足时,此命令可以让你获得一个容器内部的交互式 shell。
    - 操作:进入容器后,你可以:
        - 检查文件系统,确认脚本、配置文件是否存在于正确的位置。
        - 检查文件权限。
        - 手动执行 `ENTRYPOINT` 或 `CMD` 中的命令,观察其输出和行为。