为什么dockerfile中你不应该写entrypoint

部署运行你感兴趣的模型镜像

Docker Dockerfile中ENTRYPOINT的正确使用与常见陷阱

在Docker容器化应用的世界中,Dockerfile中的ENTRYPOINTCMD指令是定义容器启动行为的核心。然而,它们之间的区别以及各自的最佳使用场景常常被混淆或误用,尤其是在处理像基于NVIDIA CUDA的基础镜像这类基础环境时。本文将简单探讨ENTRYPOINT的真正作用,它与CMD如何协同工作,容器的生命周期是如何被决定的,以及在使用GPU算法容器时为什么通常不应覆盖ENTRYPOINT

ENTRYPOINT vs CMD:理解关键区别与协作

ENTRYPOINTCMD 都用于指定容器启动时运行的程序,但它们的设计目标和行为有所不同:

  • ENTRYPOINT:配置容器启动时运行的主要可执行文件或脚本。它定义了容器的“主进程”或一个固定的启动器。

    • 不易覆盖ENTRYPOINT 不易在容器运行时通过 docker run <image> [COMMAND] 的方式直接覆盖,而是需要使用 --entrypoint 参数显式修改。
    • 可靠性要求ENTRYPOINT 指定的指令应该是高度可靠和经过充分测试的,因为它直接关系到容器能否成功启动和初始化。如果 ENTRYPOINT 本身存在错误,容器将无法按预期启动。
    • 用途:适合定义容器的核心、不变的启动逻辑,或者包装一个需要特定初始化步骤的应用程序。
  • CMD:为 ENTRYPOINT 提供默认参数,或者在没有 ENTRYPOINT 的情况下,指定容器启动时要执行的默认命令

    • 易于覆盖:如果 docker run <image> 命令后面附加了其他命令或参数,CMD 的内容会被这些新命令或参数覆盖。这为用户在运行时调整容器行为或执行调试任务提供了便利(例如 docker run <image> /bin/bash)。
    • 用途:提供默认的应用参数,或者指定在没有 ENTRYPOINT 时容器应运行的默认应用。

两者如何协同工作?

  1. 只有 CMD:如果 Dockerfile 中只有 CMD,那么 CMD 指定的命令将在容器启动时执行。docker run <image> [new_command] 时附加的 [new_command] 会完全覆盖 CMD
  2. 只有 ENTRYPOINT:如果 Dockerfile 中只有 ENTRYPOINT,那么容器启动时将执行 ENTRYPOINT 指定的命令。docker run <image> [arg1] [arg2] 时附加的参数 [arg1] [arg2] 会全部作为参数传递给 ENTRYPOINT
  3. ENTRYPOINTCMD 同时存在 (推荐方式)
    • ENTRYPOINTCMD 都使用 JSON 数组的 exec 格式(例如 ENTRYPOINT ["/app/runner.sh"]CMD ["--param1", "value1"])时,CMD 的所有内容会作为参数附加到 ENTRYPOINT 命令之后。最终执行的命令将是 ENTRYPOINT 加上 CMD 的参数。例如,ENTRYPOINT ["executable"]CMD ["param1", "param2"] 会执行 executable param1 param2
    • ENTRYPOINT 的主进程特性:如果 ENTRYPOINT 是一个可执行文件或脚本,它通常会成为容器内的 PID 1 进程(或者通过 exec 将 PID 1 转交给它指定的命令)。容器的生命周期与这个 PID 1 进程紧密相关。如果这个主进程成功启动并且持续运行,容器就会保持运行状态。如果它启动失败、崩溃或正常退出,容器也会随之停止。

CMD是一个Shell脚本时:PID与生命周期管理 (以FastAPI为例)

现在我们来探讨一个常见的情况:如果您的 CMD 不是一个单一的应用程序,而是一个 .sh 脚本(例如 CMD ["/app/run.sh"]),这个脚本内部可能包含多条指令,包括启动一个像FastAPI这样的Web服务。这时,容器的PID 1以及生命周期是如何被控制的呢?

  1. Shell解释器成为初始PID 1
    当容器启动并执行 CMD ["/app/run.sh"] 时,实际情况是 /app/run.sh 脚本由其shebang指定的解释器(如 #!/bin/bash#!/bin/sh)或者默认的 /bin/sh 来执行。这个shell解释器进程最初会成为容器的PID 1

  2. 脚本内部指令的执行与生命周期
    PID 1(即shell解释器)开始逐行执行 /app/run.sh 脚本中的命令。

    • FastAPI作为子进程:假设您的 run.sh 脚本中包含启动FastAPI服务的命令,例如:
      #!/bin/bash
      echo "进行一些初始化设置..."
      # source /opt/venv/bin/activate # 如果使用虚拟环境
      echo "启动FastAPI应用..."
      uvicorn main:app --host 0.0.0.0 --port 8000
      # 脚本的其他命令(如果uvicorn命令不是最后一条,或者uvicorn在后台运行)
      echo "Uvicorn已启动(或者已退出)"
      
      在这个场景下,uvicorn 进程是作为shell脚本(PID 1)的一个子进程启动的。
    • 生命周期控制
      • 如果 uvicorn 在前台运行(默认行为):shell脚本(PID 1)会等待 uvicorn 命令执行完成。只要 uvicorn 服务器持续运行,shell脚本就会停留在这一行,PID 1 就不会退出,容器也就保持运行。如果 uvicorn 服务器因为某种原因(例如接收到 SIGTERM 信号并优雅关闭,或者遇到无法恢复的致命错误而崩溃)退出,shell脚本会继续执行 uvicorn 命令之后的指令。当整个 run.sh 脚本执行完毕后,PID 1(shell解释器)退出,容器随之停止。
      • **推荐使用 exec **:如果在 run.sh 脚本中启动FastAPI服务时使用了 exec 命令,例如:
        #!/bin/bash
        echo "进行一些初始化设置..."
        echo "使用exec启动FastAPI应用..."
        exec uvicorn main:app --host 0.0.0.0 --port 8000
        # exec之后的命令将不会被执行
        
        exec uvicorn... 会导致 uvicorn 进程替换当前的shell进程,并接管PID 1。此时,容器的生命周期就直接与 uvicorn 进程绑定。这是更推荐的做法,因为:
        1. 减少了不必要的shell进程层级。
        2. 信号(如 SIGINT, SIGTERM)能被直接传递给 uvicorn 应用,便于实现优雅关闭。
  3. 为什么我的fastapi后台服务有错误,容器仍然在运行

    • FastAPI/Uvicorn的健壮性:如前所述,像Uvicorn这样的Web服务器通常设计为能够处理应用级的错误(例如,某个API请求处理中发生异常)而不使整个服务器进程崩溃。它会记录错误,向客户端返回适当的HTTP错误码,然后继续服务其他请求。
    • stderr/stdout的继承与 docker logs:当FastAPI/Uvicorn(作为shell的子进程或直接作为PID 1)打印日志或错误信息到其标准输出(stdout)或标准错误(stderr)时:
      • 如果FastAPI是shell的子进程:子进程的stdout/stderr默认会传递给其父进程(即shell脚本)。
      • shell脚本(作为PID 1,或其输出被PID 1继承)的stdout/stderr会被Docker守护进程捕获。
      • 因此,你可以通过 docker logs <container_id>查看到FastAPI服务输出的所有日志和错误信息

docker start 时会执行哪些指令?

如果你容器里的entrypoint坏了,那你是无法进入的容器的,因为你没法执行start,当你使用 docker start <container_id> 命令启动一个已经停止的容器时,Docker 守护进程会执行以下操作:

  1. 加载容器配置:Docker 会读取该容器在创建时(通过 docker rundocker create)保存的配置信息。这包括了最初定义的 ENTRYPOINTCMD(以及其他配置如环境变量、卷挂载、端口映射等)。
  2. 准备运行环境:根据配置重新准备容器的运行环境,例如网络接口。
  3. 重新执行 ENTRYPOINT:Docker 会重新执行容器配置中定义的 ENTRYPOINT
    • 如果 CMD 也存在于配置中,并且没有在 docker run 时被覆盖,那么 CMD 的内容会像容器首次启动时一样,作为参数传递给 ENTRYPOINT
  4. 启动主进程ENTRYPOINT 指定的命令(及其参数,即CMD)作为容器的主进程(PID 1,或由ENTRYPOINT最终exec的进程)开始运行。

重要的是,docker start 不会重新执行 Dockerfile 中的构建步骤。它只是尝试重新启动容器已有的主进程逻辑。 因此,如果容器停止的原因是其 ENTRYPOINT 指令本身存在错误、配置问题或者依赖的条件不满足(例如,ENTRYPOINT脚本尝试连接一个在容器启动时不可用的外部服务且没有错误处理),那么 docker start 尝试再次执行这个不可靠的 ENTRYPOINT 时,很可能因为同样的错误而再次失败,导致容器无法成功启动或再次迅速停止。这就是为什么 ENTRYPOINT 的设计必须非常健壮和可靠。

为什么应该谨慎定义或覆盖ENTRYPOINT

虽然 ENTRYPOINT 功能强大,但在自定义 Dockerfile 时,尤其是在基于已有复杂基础镜像进行构建时,需要谨慎对待:

  1. 灵活性受限:一旦设置了 ENTRYPOINT,用户在 docker run 时想要运行一个不同的命令(例如,一个调试工具或一个临时的shell)会变得不那么直接。虽然可以使用 docker run --entrypoint 来覆盖,但这不如直接覆盖 CMD 来得方便。
  2. 可调试性:如果 ENTRYPOINT 配置错误导致容器无法启动,排查问题会更加困难。一个没有 ENTRYPOINT 或拥有一个简单 ENTRYPOINT(如 sh)的容器更容易通过覆盖 CMD 来启动一个交互式shell进行调试。
  3. 基础镜像的预设:许多官方或第三方基础镜像(尤其是像 nvidia/cuda 这样的专用镜像)已经精心配置了 ENTRYPOINT。这些预设的 ENTRYPOINT 通常执行关键的初始化任务,覆盖它们可能会导致严重问题。

GPU算法容器的陷阱:NVIDIA镜像的特殊性

我们做基于深度学习开发的时候,同事会使用基于NVIDIA CUDA的官方镜像(如 nvidia/cuda:12.4.1-devel-ubuntu22.04),这些镜像通常已经设置了一个专用的 ENTRYPOINT 脚本。我们可以通过以下命令查看特定NVIDIA CUDA镜像的ENTRYPOINT设置:

docker inspect -f '{{.Config.Entrypoint}}' nvidia/cuda:12.4.1-devel-ubuntu22.04

在这里插入图片描述

对于 nvidia/cuda:12.4.1-devel-ubuntu22.04,输出通常是:

[/opt/nvidia/nvidia_entrypoint.sh]

我们进入这个镜像对应的容器,看看它长啥样:

NVIDIA CUDA镜像entrypoint.sh示例内容(节选与分析):

#!/bin/bash
# Copyright (c) 2016-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# ... (版权和许可信息省略) ...

# Gather parts in alpha order
shopt -s nullglob extglob
_SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
# 查找 entrypoint.d 目录下的 .txt 和 .sh 文件
declare -a _PARTS=( "${_SCRIPT_DIR}/entrypoint.d"/*@(.txt|.sh) )
shopt -u nullglob extglob

# ... (打印 banner 的函数省略) ...

# Execute the entrypoint parts
# 遍历并执行 entrypoint.d 中的文件
for _file in "${_PARTS[@]}"; do
  case "${_file}" in
    *.txt) cat "${_file}";;      # 显示 .txt 文件内容
    *.sh)  source "${_file}";;   # source 执行 .sh 脚本,在当前shell环境生效
  esac
done

echo

# This script can either be a wrapper around arbitrary command lines,
# or it will simply exec bash if no arguments were given
if [[ $# -eq 0 ]]; then # 如果没有接收到任何参数 (即 CMD 为空或未提供)
  exec "/bin/bash"     # 则执行 /bin/bash,提供一个交互式shell
else                    # 如果有参数 (来自 CMD 或 docker run <image> cmd)
  exec "$@"             # 则用接收到的命令和参数替换当前脚本进程,使其成为PID 1
fi

分析这个脚本,我们可以看到:

  1. 它首先执行 entrypoint.d 目录中的所有初始化脚本。这可能包括环境检查,

它通常负责:
环境检查:验证NVIDIA驱动程序是否正确安装和版本是否兼容。
设置环境变量:配置如 PATH, LD_LIBRARY_PATH, NVIDIA_VISIBLE_DEVICES, NVIDIA_DRIVER_CAPABILITIES 等关键环境变量,确保CUDA工具链、cuDNN库等能被正确找到和使用。
设备初始化:可能执行一些与GPU设备发现和初始化相关的操作。
兼容性处理:确保容器内的CUDA环境与宿主机的驱动程序正确协作。
模块化扩展:如上面展示的脚本内容,它会执行 entrypoint.d 目录下的其他 .txt (显示信息) 和 .sh (执行配置) 文件,允许灵活地扩展初始化过程。

  1. 然后,它检查是否接收到了参数(通常来自 CMD)。
  2. 如果没有参数,它 exec /bin/bash,这意味着容器会启动一个bash shell。
  3. 如果有参数,它 exec "$@",这意味着 CMD 中指定的命令将替换 nvidia_entrypoint.sh 进程,成为容器的实际主进程 (PID 1)。

覆盖NVIDIA ENTRYPOINT的风险:
在自己的Dockerfile中覆盖这些预设的ENTRYPOINT是一个常见的致命错误:

# 错误示例
FROM nvidia/cuda:12.4.1-devel-ubuntu22.04
ENTRYPOINT ["/app/my_custom_entrypoint.sh"] # 这会跳过所有NVIDIA的GPU初始化

这会导致容器无法正确初始化GPU环境,引发如容器反复重启、无法检测GPU、CUDA库工作异常等问题。

正确做法:
保留NVIDIA基础镜像的 ENTRYPOINT,并通过 CMD 来指定你自己的应用程序或启动脚本:

FROM nvidia/cuda:12.4.1-devel-ubuntu22.04
COPY ./my_app_launcher.sh /app/my_app_launcher.sh
RUN chmod +x /app/my_app_launcher.sh
# NVIDIA的ENTRYPOINT会先执行,然后通过 exec "$@" 执行这里的CMD
CMD ["/app/my_app_launcher.sh", "--arg1", "value1"]

容器因ENTRYPOINT错误卡死循环重启的救援方案

建议还是删掉容器,修改dockerfile,重新构建一个新的可靠的容器。

修改Dockerfile,恢复正确的ENTRYPOINT并使用CMD(根本解决方案)
根据调试结果,修改你的Dockerfile。如果问题是覆盖了基础镜像的ENTRYPOINT,那么应该移除你的ENTRYPOINT定义。将你的应用启动逻辑移到CMD中。

核心知识点回顾

为了帮助您更好地掌握本文的内容,我们再次强调几个核心知识点:

  1. ENTRYPOINT vs CMD 的区别

    • ENTRYPOINT 定义容器的主要执行逻辑或固定启动器,不易被覆盖,应保证其可靠性。
    • CMD 提供默认参数给ENTRYPOINT,或在无ENTRYPOINT时作为默认执行命令,易被覆盖,方便灵活性和调试。
  2. CMDENTRYPOINT 的参数

    • 当两者都存在时(推荐使用JSON格式),CMD的内容会作为参数传递给ENTRYPOINT指定的命令。ENTRYPOINT脚本(如NVIDIA的)通常使用 exec "$@" 来接收并执行这些参数。
  3. Shell脚本 (.sh) 运行FastAPI服务时的PID与日志

    • 如果 CMD 是一个 .sh 脚本,该脚本的解释器(如bash)最初成为PID 1。
    • 脚本中启动的FastAPI服务(如uvicorn)是这个bash解释器的子进程。
    • 子进程(FastAPI/Uvicorn)的stdout/stderr默认会传递给父进程(bash解释器, PID 1)。
    • Docker捕获PID 1的stdout/stderr,因此 docker logs 可以看到FastAPI的输出和错误。
    • 如果在脚本中用 exec uvicorn... 启动FastAPI,则uvicorn会取代bash成为PID 1,其日志同样会被 docker logs 捕获。容器生命周期直接与uvicorn绑定。
  4. NVIDIA CUDA镜像:优先使用CMD,不覆盖ENTRYPOINT

    • NVIDIA官方CUDA镜像内置了关键的ENTRYPOINT脚本(如 /opt/nvidia/nvidia_entrypoint.sh)来初始化GPU环境。
    • 在基于这些镜像构建应用时,不应该覆盖这个ENTRYPOINT。应将您的应用启动命令放在CMD中,让NVIDIA的ENTRYPOINT先完成初始化,然后通过exec "$@"执行您的CMD
  5. 查看与理解基础镜像的ENTRYPOINT

    • 使用 docker inspect -f '{{.Config.Entrypoint}}' <image_name> 可以查看镜像预设的ENTRYPOINT。按需扩展了解 ‘{{.Config.Entrypoint}}’ 是 Go模板 (Go template) 的语法,用来提取和格式化输出json数据中的特定字段。

    • 对于NVIDIA镜像,其ENTRYPOINT脚本已经写好了,通过entrypoint.d实现模块化初始化,并最终通过exec "$@"exec /bin/bash(无CMD时)来执行最终命令。

最佳实践总结 (精简)

  1. CMD优先:大多数应用,将启动逻辑放CMD
  2. ENTRYPOINT用于固定逻辑:需要不变的初始化或包装脚本时使用。
  3. ENTRYPOINT脚本使用 exec "$@":确保信号传递和CMD参数正确执行。
  4. JSON格式ENTRYPOINTCMD优先用JSON数组格式。
  5. 尊重基础镜像ENTRYPOINT:尤其对NVIDIA等专用镜像,不要轻易覆盖。

结语

深刻理解ENTRYPOINTCMD的特性、它们如何协同工作,以及它们如何影响容器的生命周期和PID管理,是构建健壮、灵活且易于维护的Docker容器的关键。特别是在处理像NVIDIA GPU容器这样具有初始化需求的场景时,遵循最佳实践,尊重并利用好基础镜像提供的ENTRYPOINT,将能有效避免许多常见的陷阱,显著提高开发和运维效率。

您可能感兴趣的与本文相关的镜像

Wan2.2-I2V-A14B

Wan2.2-I2V-A14B

图生视频
Wan2.2

Wan2.2是由通义万相开源高效文本到视频生成模型,是有​50亿参数的轻量级视频生成模型,专为快速内容创作优化。支持480P视频生成,具备优秀的时序连贯性和运动推理能力

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值