DEBUG.WIKI
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

Docker Compose 写了两个 command,初始化命令没有执行

Docker Compose 同一服务重复写 command 不能按顺序执行初始化和主程序。说明单个 command 配合 sh -c、初始化失败阻断、ENTRYPOINT 参数冲突,以及重建后的验证和数据回退边界。

同一个服务需要先做初始化,再启动应用,于是在 Compose 文件里写了两个 command。启动后只看到应用日志,没有初始化输出;换一个 Compose 版本,还可能直接遇到重复键校验错误。

command 是服务的一项配置,不是可以重复追加的任务列表。同一层 YAML 映射的键必须唯一。即使某个读取器接受重复键,也不能依靠它执行两次命令。要表达“第一条成功后才执行第二条”,应保留一个 command,把顺序交给明确启动的 shell 或启动脚本。

以下适用于当前 docker compose 管理的 Linux 容器,镜像内有 /bin/sh。Windows 容器、没有 shell 的精简镜像,以及固定把参数交给某个应用的入口程序,需要按各自镜像调整,不能直接套用。

先分清 command 的三个写法

下面这种写法不能表示两个启动步骤。web、镜像名和两个程序路径均为示例:

services:
  web:
    image: example-web:release-tag
    command: /app/bin/prepare
    command: /app/bin/serve

把它改成列表也不会自动变成任务队列:

command: ["/app/bin/prepare", "/app/bin/serve"]

这通常表示一个程序和它的参数:后面的路径会作为参数传给前面的程序。若镜像定义了 ENTRYPOINT,整组内容还可能成为该入口的参数。

同样,直接写成 command: /app/bin/prepare && /app/bin/serve 也不能保证按 shell 语法运行。Compose 的 command 不会因为是字符串,就自动使用镜像 Dockerfile 的 SHELL。没有真正启动 shell 时,&& 可能被应用当作普通参数。

先在项目目录查看将要生效的配置:

docker compose config --quiet
docker compose config

若部署使用多份 -f 文件、项目名或 --env-file,这里应沿用相同参数。第一条用于校验;第二条查看合并后的 web.command 和 web.entrypoint。配置输出可能含凭据,只在受控终端查看。校验通过说明配置能解析,不能证明初始化成功。

用一个 shell 串起初始化和主进程

修改前保留 Compose 文件、覆盖文件和当前镜像版本。下面给出完整的服务结构;使用时保留自己已有的环境变量、挂载、端口和依赖配置,不要用这个最小示例覆盖整个项目。

services:
  web:
    image: example-web:release-tag
    command:
      - /bin/sh
      - -c
      - |
        /app/bin/prepare &&
        exec /app/bin/serve

example-web:release-tag 应替换成已经构建好的应用镜像。/app/bin/prepare 代表会执行完并返回退出码的初始化程序,/app/bin/serve 代表持续在前台运行的主程序;两者都必须在镜像内存在,且容器用户有执行权限。

这段配置有三层含义:列表明确传递三个参数;-c 后的整个多行字符串交给 /bin/sh 解析;YAML 的 | 保留换行,本身不表示管道。

第一行末尾的 && 要保留。它让 shell 只在初始化返回 0 时启动应用;初始化失败时,后面的主进程不会启动,shell 返回这条失败命令的状态。只把两条命令分成两行,或用分号连接,并不具有这个失败阻断条件。

exec 用主进程替换这一层 shell,避免额外留着一层等待它退出的 shell。不要无条件宣称主进程一定是 PID 1:镜像原有入口或启用的 init 进程也会影响进程层次。

第二条命令应是最后的前台主进程。若把一个长期运行的服务器放在第一条,shell 会一直等待它结束,后续命令自然不会执行。需要同时运行两个独立服务时,应分别建服务,不能靠顺序连接让它们并行。

出现 Unknown command sh 时,核对镜像入口

如果修改后应用提示不认识 sh,或者把 /bin/sh 当成了自己的参数,先查看本地目标镜像的元数据:

docker image inspect example-web:release-tag

重点看 Config.Entrypoint 和 Config.Cmd,再结合前面的 Compose 生效配置判断。command 覆盖镜像默认命令,不会自动清掉镜像入口。若入口固定执行某个程序,它可能根本不把传入参数当作新命令执行。

这时应查看入口脚本如何传递参数,选择镜像支持的启动方式。对于自己维护的镜像,可以把顺序逻辑放进已有入口,完成原有初始化后再执行主程序。不要为了让示例运行,直接给第三方数据库或应用镜像加上 entrypoint: /bin/sh:原入口可能负责切换用户、目录权限、配置生成或数据初始化,覆盖它会跳过这些步骤。

如果报错是找不到 /bin/sh,则先确认镜像是否包含 shell。不要把路径改成 /bin/bash 碰运气;只有镜像确实提供 Bash 且脚本需要 Bash 特性时才使用它。无 shell 镜像应把启动准备放进应用或受控的一次性任务,而不是临时修改生产容器安装工具。

更新容器后,分别验证两步

先在隔离测试项目中确认初始化能安全重复执行。若它包含数据库迁移或数据写入,应先备份相关数据,并核对是否允许多个副本同时执行;每次重新启动都会运行的初始化,不适合直接承载无法重复执行的操作。

确认依赖服务已就绪后,更新目标服务并查看状态和日志:

docker compose up -d --no-deps web
docker compose ps --all web
docker compose logs --tail=100 web

--no-deps 不会替你启动依赖。up 会在服务配置改变时重建相应容器,业务会经历停止和启动,应安排合适的维护窗口。无需删除数据卷。

验证应同时看到初始化完成的业务证据和主程序正常提供服务。仅仅容器处于运行状态,或 up -d 返回成功,不能证明两个步骤都正确。再在测试环境制造一个可恢复的初始化失败条件,确认主程序不会随后启动,并检查退出状态和日志;不要在生产数据上故意制造迁移失败。

如果初始化需要数据库,串联命令只保证本容器里的先后顺序,不保证数据库已经可用。短写法的 depends_on 也不等待健康检查通过,应另行配置适合应用的就绪条件。post_start 在容器启动后才执行,准确触发时机没有保证,不适合代替“应用启动前必须完成”的步骤。

撤回时,恢复已知可运行的命令配置和镜像版本,再用相同项目参数更新服务。配置回退不会撤销初始化已经写入的数据;涉及迁移时必须使用该应用认可的数据恢复或迁移回退流程。