Docker Compose 中 command 与 entrypoint 的区别
ENTRYPOINT 用于启动程序,command 用于提供参数。本文用四种覆盖组合说明 Compose 行为,并解释设置 entrypoint 后为何会清除镜像 CMD。
Docker Compose 中 command 与 entrypoint 的区别:一条规则
在 Docker Compose 中,entrypoint:设置要运行的程序,command:设置传递给该程序的参数。容器进程由 entrypoint 列表和追加在其末尾的 command 列表组成。本页的其他行为都由这句话推导而来。
这两个键分别对应 Dockerfile 中的两条指令。entrypoint:会替换镜像的 ENTRYPOINT。command:会替换镜像的 CMD。它们并非相互独立,这也是许多人遇到问题的原因:设置 entrypoint:时,也会丢弃镜像的 CMD。Compose 规范对此有明确说明。如果 entrypoint不是 null,Compose 就会忽略镜像中的默认 command。
查看镜像已声明的内容
覆盖任何设置之前,先查看镜像发布的内容。
docker image inspect --format '{{json .Config.Entrypoint}}' postgres:16
docker image inspect --format '{{json .Config.Cmd}}' postgres:16您会看到 ["docker-entrypoint.sh"] 和 ["postgres"],因此容器会运行 docker-entrypoint.sh postgres。该脚本会在首次启动时创建数据目录,读取 POSTGRES_* 变量,将权限降至 postgres 用户,最后执行传入的参数。关键是确定要修改哪一部分。要向数据库传递选项,请替换 command:。如果替换 entrypoint:,整个初始化过程都不会运行。
四种组合,显示在一张小图中
构建一个镜像,其唯一用途是打印启动它时接收的参数列表。
FROM alpine:3.20
ENTRYPOINT ["/bin/echo", "ep"]
CMD ["cmd"]docker build -t argdemo .services:
demo:
image: argdemo每次编辑后运行 docker compose up,并读取它记录的那一行。
- 两个键都未设置。 进程为
/bin/echo ep cmd,日志显示ep cmd。 - 仅设置
command: ["cmd2"]。 进程为/bin/echo ep cmd2。entrypoint 未变,只有参数发生变化。 - 仅设置
entrypoint: ["/bin/echo", "ep2"]。 进程为/bin/echo ep2,日志显示ep2。镜像中的cmd消失,且不会收到任何警告。 - 两个键都已设置。 进程为
/bin/echo ep2 cmd2。只有这种情况下,您才能控制完整的参数列表。
设置 entrypoint 会清除镜像的 CMD
镜像的 CMD 会作为该镜像 ENTRYPOINT 的默认参数列表写入。替换 entrypoint 后,这些参数就属于一个不再运行的程序,因此 Compose 会丢弃它们,而不是构造出镜像作者本来没有设计的命令行。docker run --entrypoint 的行为相同,因此这是 Docker 的行为,不是 Compose 的特殊问题。
后果很具体。nginx:1.27 声明了 ENTRYPOINT ["/docker-entrypoint.sh"] 和 CMD ["nginx", "-g", "daemon off;"]。设置 entrypoint: /custom-init.sh 后,脚本启动时参数列表为空。脚本通常以 exec "$@" 结尾,但此时没有可供 exec 的对象,因此 exec 不执行任何操作,脚本继续运行到最后一行,容器以退出码 0 退出,任何位置都不会显示错误消息。请自行补回这些参数:
services:
web:
image: nginx:1.27
entrypoint: /custom-init.sh
command: ["nginx", "-g", "daemon off;"]需要记住的规则是:每次设置 entrypoint: 时,都要在同一次修改中确定 command: 应该是什么。
exec form 与 shell form,以及 Compose 的差异
Dockerfile 接受两种语法。CMD ["nginx", "-g", "daemon off;"] 是 exec form:二进制文件直接运行,不经过 shell。CMD nginx -g "daemon off;" 是 shell form:Docker 会将其改写为 /bin/sh -c 'nginx -g "daemon off;"',因此先运行 shell,您的程序则成为它的子进程。
Compose 不遵循这条规则,这常常会让人困惑。command: 中的字符串会拆分为参数并直接执行,不会添加 /bin/sh -c 包装。Compose 参考文档对此有明确说明:command 字段不会在镜像中定义的 SHELL 上下文中运行,因此如果需要使用 shell 功能,必须自行调用 shell。
因此,command: echo "hello $$HOSTNAME" 会打印字面文本 hello $HOSTNAME。整个过程中没有 shell 读取该字符串,所以不会进行展开。需要使用 shell 时,请显式调用它:
services:
demo:
image: alpine:3.20
command: /bin/sh -c 'echo "hello $$HOSTNAME"'信号、PID 1 以及干净地执行 docker compose down
docker compose stop 和 docker compose down 会向每个容器中的 PID 1 发送 SIGTERM,等待 stop_grace_period,然后发送 SIGKILL。默认宽限期为 10 秒。
PID 1 在 Linux 中具有特殊行为。内核不会对 PID 1 应用信号的默认动作。因此,未安装 SIGTERM 处理程序的进程作为 PID 1 运行时,会直接忽略 SIGTERM。它会在整个宽限期内继续运行,随后被强制终止,从而中断所有打开的连接或未提交的事务。
在程序前面加一层 shell 会增加这种风险,因为 shell 是 PID 1,而大多数 shell 不会将信号转发给子进程。有些 shell 会在 -c 字符串中用最终命令替换自身,因此程序有时确实会成为 PID 1。这取决于 shell 和具体字符串,不能凭猜测判断。请查看实际结果:
docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echo如果 PID 1 显示为 /bin/sh -c ...,而不是你的程序,有两种修复方法。在镜像中使用 exec 形式,或者保留 shell,并通过 exec 将进程交给程序:
services:
web:
image: myapp:1.4
command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'exec 会用你的程序替换 shell 进程,而不是派生子进程。因此,你的程序会继承 PID 1 并接收信号。
有些程序会创建子进程但从不回收它们,从而留下僵尸进程,因为 PID 1 同时也是子进程回收者。Compose 提供了相应的开关:
services:
web:
image: myapp:1.4
init: true
stop_grace_period: 30sinit: true 会以 PID 1 运行一个小型 init 进程。它会将信号转发给你的进程,并回收子进程。stop_grace_period 会为确实需要较长时间关闭的服务提供更多时间。如果程序需要接收其他信号,stop_signal: SIGQUIT 可以更改 Compose 发送的信号。使用 docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27 查看镜像已经声明的设置。
如果某个堆栈中的 docker compose down 每个服务总是耗时 10 秒,这说明没有进程处理 SIGTERM。在责怪工具之前先修复此问题,并参阅docker compose down 与 stop 的区别,了解每个子命令会删除哪些内容。
同样的 exec 与 shell 区别还会出现在另一个位置。使用 test: ["CMD", "curl", "-f", "http://localhost/"] 编写的健康检查会直接运行二进制文件,而 test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] 会通过 shell 运行,因此 || 才有意义。编写能够如实失败的 Compose 健康检查介绍了该字段的其他内容。
为官方镜像追加标志
这是大多数读者查看本节的原因。您希望为 postgres 添加一个额外标志,同时不能影响初始化脚本。
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
command: postgres -c max_connections=200 -c shared_buffers=256MB
volumes:
pgdata:只有 command: 发生了变化,因此 docker-entrypoint.sh 仍会运行,并继续执行您为它提供的内容。请检查结果,不要直接假设配置已生效:
docker compose up -d db
docker compose exec -T db psql -U postgres -c 'show max_connections;'输出应显示 200。如果仍显示 100,请运行 docker compose config,并确认您期望的 command 已包含在合并后的输出中。Compose 会直接替换 command 来合并覆盖文件,而不是向其中追加内容。因此,另一个同样设置 command: 的文件会静默覆盖前一个文件。
上面的 ${POSTGRES_PASSWORD} 由 Compose 在主机上根据您的 .env 文件展开,此时容器尚未创建。Compose 中的环境文件和密钥介绍了该值可以安全存放的位置。
使用 docker compose run 执行一次性迁移
docker compose run 会根据相同的服务定义创建新容器,并将命令替换为服务名称后面输入的内容。镜像的 entrypoint 仍会运行,因此该容器的准备方式与长期运行的服务容器完全一致。
docker compose run --rm app python manage.py migrate--rm会在命令退出时删除容器。不使用该选项时,每次运行都会留下一个已停止的容器,可在docker compose ps -a中看到。- 不会发布端口。
run容器会忽略服务的ports:,除非添加--service-ports,因此不会与已运行的服务发生端口冲突。 - 会先启动依赖项。
depends_on中的所有内容都会在命令运行前启动,--no-deps可跳过该步骤。 - 容器会获得类似
myproject-app-run-9f2c1a的自动生成名称,因此不会与服务容器冲突。
如果还要替换 entrypoint,可以使用以下选项:
docker compose run --rm --entrypoint /bin/sh app -c 'python manage.py migrate'最终的参数列表为 /bin/sh -c 'python manage.py migrate',因为服务名称后面的内容仍会被视为命令。docker compose exec 是另一个工具,其工作方式不同:它会在已经运行的容器中执行进程,并完全忽略 entrypoint: 和 command:。需要新容器执行任务时使用 run;需要查看运行中容器内部状态时使用 exec。Compose 命令速查表并列介绍了其余子命令。
为什么容器会立即退出?
先查看退出代码,因为它可以快速缩小原因范围。
docker compose ps -a
docker compose logs app退出代码为 0,且没有输出。 命令已执行并结束。最常见的原因是,entrypoint: 覆盖项同时覆盖了镜像的 CMD,导致入口点在空参数列表下运行,没有可继续执行的内容。
错误以 permission denied 结尾。 镜像中的脚本没有可执行位,通常是因为仓库中的文件从未设置该权限。请在构建时使用 COPY --chmod=0755 entrypoint.sh /entrypoint.sh 设置。
错误以 no such file or directory 结尾,但对应文件确实存在于镜像中。 脚本使用了 Windows 换行符。这样一来,脚本首行会被解析为 #!/bin/sh 加回车字节,因此内核会查找名称中包含该字节的解释器,但找不到。运行 dos2unix entrypoint.sh,然后将 * text eol=lf 添加到 .gitattributes,避免问题再次出现。
executable file not found in $PATH。 command: 中指定的二进制文件不在镜像中,或者您写入了 cd 之类的 shell 内置命令,而此处只能使用实际的程序。
进入入口点失败的镜像 Shell
如果入口点在您来得及检查任何内容前就退出,请替换它:
docker compose run --rm --entrypoint /bin/sh app如果返回 executable file not found in $PATH,说明镜像完全没有 Shell。Distroless 和基于 scratch 的镜像通常不会包含 Shell。即使不启动入口点,您仍然可以从外部读取文件系统:
docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probe如果需要让容器保持运行状态,以便反复连接到其中,请让它运行一个永不退出的进程。将以下内容放入一个不提交到版本库的覆盖文件中:
services:
app:
entrypoint: ["tail", "-f", "/dev/null"]
command: []严格来说,不需要 command: [],因为设置 entrypoint: 已经清除了镜像的 CMD;但明确写出它,可以记录配置意图,方便下一个阅读该文件的人理解。启动容器,然后进入其中:
docker compose -f compose.yaml -f compose.debug.yaml up -d app
docker compose exec app /bin/sh现在手动运行真正的入口点,并观察它在哪一步停止。这样,错误消息会直接显示在您的终端中,而不是出现在一个半秒前就已退出的容器里。如果您还在组装第一个堆栈,在 VPS 上创建第一个 Compose 堆栈介绍了上述内容所假定的文件布局。
FAQ
为什么执行 docker compose up 后容器会立即退出?
检查 docker compose ps -a 以获取退出码。退出码为 0 且没有输出,通常表示您在服务上设置了 entrypoint:,这同时清除了镜像的 CMD,因此 entrypoint 在空参数列表下运行后直接结束。使用 command: 添加回这些参数。以 permission denied 结尾的错误表示 entrypoint 脚本没有可执行权限。对于一个确实存在的文件,如果错误以 no such file or directory 结尾,表示脚本使用了 Windows 换行符,因此其 shebang 行指定了一个不存在的解释器。
在 Compose 中设置 entrypoint 会移除镜像的 CMD 吗?
会。如果 entrypoint 非 null,Compose 会忽略镜像声明的默认命令。这是有文档记录的行为,与 docker run --entrypoint 一致。原因是镜像的 CMD 是作为该镜像 ENTRYPOINT 的参数编写的,因此替换 entrypoint 后,原有参数就不再对应任何对象。如果新的 entrypoint 仍需要参数,请在同一服务中设置 command:。
Compose 中的字符串 command 会通过 shell 执行吗?
不会。与 Dockerfile 中的 CMD 不同,Compose 中的字符串 command: 会被拆分为参数并直接执行,不会使用 /bin/sh -c 包装。因此,$VARIABLE 不会由容器内的 shell 展开。需要使用 shell 时,请像 command: /bin/sh -c 'echo "hello $$HOSTNAME"' 那样自行调用。双写的 $$ 会转义美元符号,使 Compose 将其原样传递给容器,而不是在宿主机上展开。
为什么 docker compose down 停止一个容器需要 10 秒?
Compose 会向 PID 1 发送 SIGTERM,等待 stop_grace_period(默认 10 秒),然后发送 SIGKILL。内核不会对 PID 1 应用默认的信号处理动作,因此没有 SIGTERM 处理程序的程序会忽略该信号,并始终等待完整的时间间隔。使用 docker compose exec -T app cat /proc/1/cmdline | tr '\0' ' ' 确认 PID 1 实际运行的是什么。如果它是 shell,请将镜像改为 exec 形式,或在 shell 字符串中写入 exec。如果进程会创建子进程但从不回收它们,请在服务上设置 init: true。