DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Docker Compose 启动 bash 后立即 Exited (0),无法 exec 进入容器

开发镜像以 bash 为主进程,docker compose up -d 后立即 Exited (0),exec 会因容器未运行而失败。核对实际启动命令,按长期开发 shell 或一次性终端选择 stdin_open、tty 和 compose run,并保全重建前的数据。

docker compose up -d 显示容器已启动,紧接着执行 docker exec 却得到:

Error response from daemon: Container ... is not running

查看全部容器时,这个开发容器的启动命令是 bash,状态已经变成 Exited (0)。同一个服务用 docker compose run 却能打开终端,还多出了另一个容器。

这组现象常见于把 SDK 或工具镜像当作长期开发环境使用,但容器默认只启动一个 shell,没有配置保持标准输入和终端的场景。shell 结束后,容器也结束了;exec 需要目标容器的主进程仍在运行,不能通过给 exec 补上 -it 把已退出的主进程恢复过来。

本文针对 Linux 容器内的 bash 开发环境,命令采用当前的 docker compose 写法。不要把它套到数据库启动失败、应用崩溃或正常完成的一次性任务上;Exited (0) 本身不足以证明退出原因。

先确认主进程确实只是一个 shell

在启动服务时使用的 Compose 项目目录执行,示例服务名为 dev,请替换为实际名称:

docker compose config --services
docker compose ps --all --no-trunc dev
docker compose logs --tail 100 dev

如果启动时指定了 -f、-p 或 --env-file,这些命令也应沿用相同参数。ps --all 会包含已停止的容器;不带它时,默认列表可能根本看不到刚刚退出的对象。重点对照服务、容器名称、启动命令和状态,别把另一条 run 创建的容器当成原服务容器。

退出码 0 表示该进程正常结束,不代表 Web 服务或开发环境还活着。如果日志中已经出现应用错误,或启动命令是一段会执行完毕的脚本,应沿着那个程序的日志判断。空日志也不是成功证明:没有执行任何命令的 shell 可以不留下业务日志。

查看合并后的 Compose 配置:

docker compose config

核对 command、entrypoint、stdin_open 和 tty。配置输出可能含环境变量值,不要直接公开。如果配置里没有设置启动命令,还需查看容器继承了镜像中的什么命令。从前面的列表抄取目标容器 ID,替换下面的占位值:

docker inspect --type container --format '{{json .Config}}' CONTAINER_ID

这里查看配置中的 Cmd、Entrypoint、OpenStdin 和 Tty,不要只凭镜像名称推断默认行为;同一工具镜像的不同版本可以有不同入口。整段配置也可能包含环境变量中的秘密,只保留排查所需字段。

如果最终只是启动 bash,既没有待执行脚本,也没有可继续读取的标准输入,它可能很快读到输入结束并正常退出。up -d 的作用是让容器在后台启动,不是给进程补一项永远运行的任务。

要保留开发 shell,就为该服务配置输入和终端

确认这个容器的用途就是保留一个可反复进入的开发环境后,在现有服务中加入:

services:
  dev:
    image: your-dev-image:your-tag
    command: ["/bin/bash"]
    stdin_open: true
    tty: true

your-dev-image:your-tag 是占位镜像名,需要换成已有的、包含 /bin/bash 的开发镜像。这个示例只展示相关字段;实际修改时保留原有构建配置、工作目录、挂载、网络和用户设置,不要覆盖整份文件。

stdin_open: true 对应容器运行时的 -i,保持标准输入;tty: true 对应 -t,分配伪终端。二者让这个以 shell 为主进程的开发容器具备交互环境。它们改变的是服务容器的创建配置,和后来一次 exec 是否分配终端是两件事。

示例中的 command 明确选择 bash。如果原有镜像已经以 bash 为默认命令,可以只添加这两个布尔配置。若镜像有自定义 ENTRYPOINT,command 可能作为它的参数,而不是独立执行 bash;先核对入口,不能随意清空初始化脚本来套用示例。没有 bash 的镜像也不能靠开启 TTY 获得 bash。

修改前保存 Compose 文件副本,并确认需要保留的文件已在适当的挂载中,或另行备份。随后在本地开发环境检查并应用该服务的配置:

docker compose config --quiet
docker compose up -d --no-deps dev
docker compose ps --all dev
docker compose exec dev /bin/bash

up 会根据配置变化停止并重建服务容器,挂载的卷会保留,但原容器可写层中的临时修改不应被当作持久数据。--no-deps 避免为了这次 shell 配置启动关联服务;如果开发任务本身需要数据库等依赖,仍需按项目约定准备它们。

预期结果是目标服务持续显示运行状态,exec 能打开新的 shell。进入后退出这个由 exec 新建的 shell,再检查服务状态;正常情况下,原先的主 shell 仍然存在。如果退出的是主 shell 本身,容器仍会停止,这些配置不会禁止进程退出。

只需要临时终端时,使用一次性容器

如果只是偶尔运行编译命令、查看工具版本或打开终端,并不需要一个长期挂着的开发容器,可以直接使用:

docker compose run --rm --no-deps dev /bin/bash

应在交互终端中执行,并确认服务镜像包含 bash、入口允许执行这一命令。run 会根据服务配置创建新容器,传入的命令替换服务的默认命令;它不是进入原来的已退出容器。因而 run 成功而 exec 失败,并不矛盾。

列表里的第二个容器就是这次新建的对象。--rm 在这次进程结束后移除一次性容器,避免留下无用对象;不要把仅存于其可写层的文件当作长期成果。

新容器仍会使用服务定义的挂载等配置。通过挂载目录改动的源文件可以保留下来,误改也同样会保留;--rm 不会替你撤销对宿主机文件或共享数据的写入。尤其不要把这条命令当成隔离的数据库修复环境,未经核对就让新旧进程同时访问同一份数据库目录。

run 默认不发布服务配置中的端口。因此,进入一次性终端成功,只能证明这个新容器中的命令能运行,不能据此判断原服务的端口和后台进程已经恢复。

应用服务应启动自己的前台进程

如果目标是运行 Web 应用或后台工作进程,就应该让容器入口执行对应的长期前台程序。只保留一个交互 shell,虽然能让容器显示运行,也不会自动启动应用。

检查 Dockerfile 和 Compose 的命令是否只剩 bash,或脚本是否启动后台任务后就退出。按应用镜像的启动方式恢复前台入口,再验证应用响应与日志。这里不能用一组通用 TTY 配置替代应用本身的启动要求,也不应靠反复重启一个立即结束的 shell 证明服务恢复。

如果新增配置不符合原来用途,恢复本次修改前的 Compose 文件,重新检查并用相同项目参数执行 up 应用。重建前继续保全需要的文件;不必为回退输入和终端配置删除数据卷。