Docker Compose exec 进入运行中容器的交互式 Shell
使用 docker compose exec 进入正在运行的服务容器;容器停止或不想干扰现有服务时,改用 docker compose run --rm,并了解 bash 不存在时的处理方法。
使用 docker compose exec 获取交互式 shell
docker compose exec web bash 会在已作为 web 服务运行的容器中打开交互式 shell。exec 后面的名称是 compose.yaml 中的服务名称,不是容器名称。如果镜像中没有 bash,请改用 sh。
docker compose ps
docker compose exec web bash先运行 docker compose ps。输出中应列出 web,其状态为 running。然后,第二条命令会将您带到容器内的提示符,输入 exit 或按 Ctrl-D 可返回主机。退出后服务仍会继续运行,因为 exec 在主进程旁边启动了第二个进程。关闭 shell 不会影响 PID 1(进程 ID 1),也就是容器构建时设定要运行的进程。
这是进入容器的两种方式之一。exec 会加入已经存在的容器。docker compose run 会根据相同的服务定义创建新容器。本指南中的其他内容几乎都源于这一差异。
为什么在 Compose 中 -it 可选,而使用普通 docker 时必须指定
两个标志控制会话的交互部分。-i 保持标准输入打开,因此您输入的内容会传递给进程。-t 分配伪终端(称为 TTY),这样 shell 才会显示提示符并处理方向键。普通的 docker exec 默认同时关闭这两个选项,因此您看到的每个示例都会写入 docker exec -it。docker compose exec 会自动同时启用这两个选项,因此 docker compose exec -it web bash 和 docker compose exec web bash 的作用相同。Compose 仍然接受 -it,这样可以继续使用原有习惯。
几秒钟内就能发现缺少 TTY。shell 可以运行,但不会显示提示符,Ctrl-C 也不会传递给进程。相反,如果您需要要求 Compose 不要 分配 TTY,则有专用标志和后文的专门章节。
没有 bash 时的处理方法
对基于 Alpine 的镜像请求 bash 时,exec 会失败,如下所示:
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown这不是 exec 本身的问题。该消息表示你请求的二进制文件不在镜像中。Alpine 提供 BusyBox,其中将 ash 作为 /bin/sh 提供,完全不包含 bash,因此应请求 sh:
docker compose exec web sh基于 Debian 和 Ubuntu 的镜像(包括 -slim 标签)确实包含 bash。bash 支持命令历史记录和更完善的补全功能。因此应先尝试 bash,失败后再回退到 sh。几乎所有通用镜像都包含 sh。
有些镜像完全不包含 shell。Distroless 镜像以及构建 FROM scratch 的镜像只包含应用程序二进制文件及其库,其他内容均不包含。这是有意为之,因为不存在的 shell 无法被攻击者利用。在这类镜像中,sh 会因同样的原因失败,也没有其他命令可尝试。有两种方法可行。Google 的 distroless 镜像发布了添加 BusyBox shell 的 :debug 标签,因此可以临时切换标签进入容器。或者,在目标容器的命名空间中启动一个独立容器:
CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot现在,netshoot 中的工具已连接到应用程序的网络,因此 curl localhost:8080 和 ss -lntp 的行为就像在应用程序容器内部执行一样。你看到的文件系统属于 netshoot,而不是应用程序。由于共享了进程命名空间,以 root 身份执行 ls /proc/1/root/ 时,可以访问目标容器自己的文件。
服务未运行时,请使用 docker compose run --rm
exec 需要容器正在运行。对已停止的服务执行该命令时会拒绝执行:
service "web" is not running它不会替您启动任何内容。docker compose run 会:
docker compose run --rm web bashrun 会根据 web 服务定义创建一个新容器。新容器使用相同的镜像、环境变量、卷和网络,并将服务的命令替换为您输入的命令。--rm 会在您退出时删除该容器。不加 --rm 时,残留容器会以类似 myproject-web-run-4f1c2b 的名称累积。docker compose ps -a 会显示这些容器,但没有其他机制会清理它们。
run 有两种行为容易让人意外。除非添加 --service-ports,否则它不会发布服务的端口。这是有意设计的:如果第一个容器仍占用主机端口 8080,第二个容器再绑定该端口就会因 bind: address already in use 失败。它还会先启动服务在 depends_on 中列出的所有依赖项,然后才显示您的 shell。因此,简单查看容器内部可能会启动数据库和缓存。--no-deps 会跳过这些依赖项。
run 会经过镜像的 ENTRYPOINT,而 exec 不会。exec 直接在现有容器中启动您的命令,因此入口点脚本不会接收到该命令。在 run 下,您的 bash 会作为参数传递给该脚本。许多官方镜像会以 exec "$@" 结束入口点,因此命令会直接传递下去,您就能获得 shell。如果脚本会自行解析参数,则会以其他方式处理这些参数。此时,可以仅为这次运行替换入口点:
docker compose run --rm --entrypoint sh web这是某个命令在 exec 下正常、在 run 下行为不同的最常见原因。命令与入口点的区别说明了每次操作分别替换镜像配置中的哪一部分。
exec 与 run:如何选择
- exec 需要已有运行中的容器。run 不需要,并且可能会启动依赖项。
- exec 可以看到实时进程列表和当前文件状态,包括应用启动后写入的内容。run 使用镜像的干净副本,因此看不到这些内容。
- exec 会跳过 entrypoint。run 会执行 entrypoint。
- 除非传入
--rm,否则 run 会留下一个容器。
使用 exec 查看实际运行情况。对于一次性迁移命令,或需要使用相同环境的临时副本,请使用 run --rm。如果实际服务无法保持运行足够长的时间,也应使用 run --rm,因为此时无法通过 exec 进入容器。
实用的 exec 选项:用户、工作目录和副本
大多数镜像会切换到非 root 用户,因此在 exec shell 中安装诊断工具会在这里失败:
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)-u root会在同一容器中为您提供 root shell:
docker compose exec -u root web sh-w /srv/app仅为该命令设置工作目录。-e KEY=value会向您的会话添加环境变量,但不会添加到服务中。当服务运行多个副本时,--index 2决定您进入哪个容器。如果您要排查挂载目录中的文件所有权问题,请参阅容器镜像中的 PUID 和 PGID,了解决定谁可以写入该目录的是数字 ID,而不是用户名。
在数据库容器内获取 psql 或 mysql shell
客户端已经位于数据库镜像中,因此主机上不需要安装客户端,也不需要发布数据库端口:
docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -pPostgres 镜像包含 psql,MySQL 镜像包含 mysql,MariaDB 镜像包含 mariadb。连接从容器内部建立,因此即使 compose 文件完全不发布数据库端口,也可以正常工作。这种安排更安全:未发布的端口不会被互联网上的任何主机访问。
有一个陷阱会浪费很长时间。Docker 看到命令之前,shell 会先在主机上展开变量,因此当变量只存在于容器内时,-U "$POSTGRES_USER" 会传入空字符串。使用单引号,并在容器内启动 shell,即可在正确的位置展开变量:
docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'这里不要在没有命令的情况下使用 docker compose run --rm db。这会针对同一个数据卷启动第二个 Postgres 服务器,并导致启动失败:
FATAL: lock file "postmaster.pid" already exists锁文件正在发挥作用,因为两个服务器同时写入同一个数据目录会导致数据损坏。数据库运行期间,应使用 exec 进入正在运行的容器。数据库是否应放在 Compose 中是另一个独立的决策,在 Docker 中还是主机上运行数据库介绍了其中的权衡。
启动时需要控制台的服务:stdin_open 和 tty
exec 和 run 适用于您手动打开的 shell。主进程本身具有交互特性的服务,需要在 compose 文件中设置两个键:
services:
console:
image: python:3.12-slim
command: python
stdin_open: true
tty: truestdin_open: true 是 docker run -i,tty: true 是 docker run -t。如果未设置这两个键,容器会启动后立即以代码 0 退出,而 docker compose ps -a 会显示 Exited (0)。这不是崩溃。python 在 stdin 没有终端时会立即读到文件结束符,然后正常退出。对于没有人输入内容的程序,这是正确行为。
设置这两个键后,连接到正在运行的进程:
docker attach $(docker compose ps -q console)按 Ctrl-P,然后按 Ctrl-Q 可分离连接,同时保持进程运行。只有容器同时启用了 TTY 和 stdin 后,这个按键序列才有效。Ctrl-C 则会向 PID 1 发送中断信号并停止服务。
普通服务应保持这两个键关闭。Web 服务器从不读取 stdin,而 tty: true 会使许多程序切换为彩色输出并使用行缓冲,因为它们认为有人正在查看输出,导致 docker compose logs 中充满转义码。
脚本化 exec 在 cron 和 CI 中失败的原因:-T 标志
在终端中可以正常运行的 exec 命令,在 cron 任务或持续集成(CI)运行器中却会失败:
the input device is not a TTYCompose 默认请求伪终端,而 cron 不会为任务提供终端,因此该请求会在命令实际运行前失败。-T 可关闭此请求:
0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump-T 还涉及第二个原因。TTY 会在字节流输出时对其进行改写,因此经过 TTY 的压缩转储到达时可能已经损坏。任何重定向或通过管道传输的输出都需要使用 -T。
cron 还有两个需要注意的细节。使用绝对路径传递 -f,因为 cron 会从主目录运行任务,而该目录中没有 compose 文件,Compose 随后会因 no configuration file provided: not found 停止。exec 会返回所运行命令的退出码,因此失败的 pg_dump 会在 set -e 下使脚本失败,而不是生成空备份并报告成功。其他日常命令已整理在Compose 命令速查表中,建议将其与这些脚本放在一起。
为什么在容器内进行的更改会消失
您使用 exec 安装工具、编辑配置文件并修复问题,但一周后修复内容却消失了。这是容器的可写层按设计工作的结果。docker compose up -d在镜像标签或服务定义发生任何更改后,都会销毁旧容器,并根据镜像创建新容器;所有手动编辑也会随旧容器一起消失。
docker compose restart则不同。它会停止并启动同一个容器,因此手动编辑的内容会保留。这就是手动修复可能持续数周,却在一次无关的更新期间消失的原因。命名卷和绑定挂载在这两种操作后都会保留,因为其中的数据位于容器外部;绑定挂载和命名卷介绍了应如何为需要保留的数据选择二者之一。
因此,应将 exec shell 视为用于读取和测试的位置。确定修复方案后,应将其写入可以持久保留的位置:将软件包写入 Dockerfile,将设置写入 compose 文件。然后执行 docker compose up -d 应用更改,再通过另一次 exec 确认新容器确实包含这些更改。
FAQ
docker compose exec 和 docker compose run 有什么区别?
exec 在已经运行的容器中、主进程之外执行命令,并跳过镜像的 entrypoint。run 根据相同的服务定义,使用相同的镜像、环境变量、卷和网络创建新容器,将您的命令传递给 entrypoint,然后先启动所有 depends_on 服务。除非添加 --service-ports,否则 run 也不会发布该服务的端口。使用 exec 检查正在运行的服务。服务已停止,或您不希望干扰服务时,使用 run --rm。
为什么 docker compose exec 报告服务未运行?
exec 连接到现有容器,无法创建容器。因此,服务已停止或崩溃时会返回 service "web" is not running。检查 docker compose ps -a,它会列出已退出的容器,并显示类似 Exited (1) 的状态;读取 docker compose logs web,了解容器停止的原因。若仍要获取 shell,请运行 docker compose run --rm --entrypoint sh web。该命令会根据相同的服务定义创建新容器,并且不会执行有问题的启动命令。
镜像没有 bash 时,如何打开 shell?
docker compose exec web bash 返回 exec: "bash": executable file not found in $PATH,表示镜像中没有 bash。对于基于 Alpine 构建的镜像,这是正常情况。请使用 docker compose exec web sh,因为 BusyBox 提供 /bin/sh。Distroless 和 scratch 镜像完全不包含 shell,因此任何 exec 命令都无法运行。若发布者提供镜像的 :debug 标签,请切换到该标签;或者使用 docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot 在目标容器的命名空间中启动调试容器,其中 $CID 来自 docker compose ps -q web。
为什么在 cron 中执行 exec 命令时,会失败并显示“输入设备不是 TTY”?
docker compose exec 默认请求伪终端,而 cron 不提供伪终端。因此,该请求会在命令运行前失败。添加 -T 将其关闭:docker compose exec -T db pg_dump -U postgres app。对于任何重定向或管道输出,也使用 -T,因为 TTY 会改变字节流并损坏二进制转储。在 cron 中,还应通过 -f 指定 compose 文件的绝对路径,否则 Compose 会退出并返回 no configuration file provided: not found。
使用 exec 在容器内进行的更改,重启后是否保留?
这些更改会在 docker compose restart 后保留,因为该操作会复用同一个容器。镜像或配置发生更改后,执行 docker compose up -d 会丢失这些更改,因为该操作会根据镜像重新创建容器,并丢弃其可写层。写入命名卷或绑定挂载的数据在两种情况下都会保留,因为这些数据位于容器外部。使用 exec 进行诊断性更改,然后将永久配置写入 Dockerfile 或 compose 文件。