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

Docker Compose exec 报 the input device is not a TTY

Docker Compose exec 在管道、文件重定向或定时任务中报 the input device is not a TTY。区分伪终端与标准输入,按任务需要使用 -T,并处理通过 stdin 读取 Compose 配置时的交互冲突。

把容器里的命令放进 cron、CI 脚本,或在前面接上管道后,执行时可能出现:

the input device is not a TTY

例如,服务 app 的容器已经在运行,镜像内也有 cat,下面这条命令却在读取内容之前失败:

printf 'probe\n' | docker compose exec app cat

这里的 app 是 Compose 文件中的服务名。管道把标准输入接到了 printf 的输出,输入端不再是终端;如果这次 exec 同时要求连接标准输入和分配伪终端,就可能被拒绝。它还没有证明容器里的程序、数据库账号或文件权限有问题。

本文针对 Linux、macOS 上的 shell、cron 和 CI 中这类输入方式冲突,示例使用 docker compose。旧版 Python Compose 使用 docker-compose;不能仅凭命令里有没有连字符判断实际版本,先查看对应命令的版本输出:

docker compose version

现代 Compose 会根据终端状态调整部分默认行为,所以同一条命令在不同版本、不同重定向方式下未必都报错。非交互任务需要稳定的行为时,应明确写出是否需要 TTY,不依赖自动判断。

管道或文件输入保留 stdin,关闭 TTY

如果目的只是把文本送给容器里的程序,使用大写 -T:

printf 'probe\n' | docker compose exec -T app cat

预期输出是 probe,随后命令结束。这个例子只回显输入,要求已有运行中的 app 服务且镜像内提供 cat;不需要为了检查 TTY 新建容器或改动应用数据。

-T 关闭的是伪终端分配,标准输入仍然可以传进去。不要为了“非交互执行”同时加上 --interactive=false,否则需要从 stdin 读取内容的程序将收不到预期输入。

文件重定向也属于这个分支。先用一份不含敏感信息的小文本验证输入通道:

docker compose exec -T app cat < ./input.txt

input.txt 由宿主机 shell 打开,不要求它已经挂载进容器。输出应与文件内容相同。如果错误变成找不到文件、服务未运行或容器内没有 cat,就按新的错误处理;继续修改 TTY 参数不会补齐这些条件。

需要读取标准输入的批处理程序也可使用这一形式,程序参数仍要单独确认。特别是数据库导入,关闭 TTY 只解决输入通道,不能保证 SQL 正确、目标库正确或导入可回滚。应在隔离环境验证内容与权限,保留备份,再执行正式写入。

定时命令不读 stdin 时,一并关闭输入连接

如果命令只运行一次并返回结果,不接收管道、不读取输入文件,也不需要密码提示,可以同时禁用输入连接:

docker compose -f /srv/example/compose.yaml exec -T --interactive=false app id

/srv/example/compose.yaml 是示例绝对路径,app 要替换为真实服务名;镜像中须有 id。该命令打印容器内本次执行所用的用户身份,不修改服务数据。

放进 cron 或 CI 时,沿用启动项目时的配置文件组合、项目名和 Docker 上下文。若启动时使用了 -p 或多个 -f,执行时也要保持一致。只换成一个绝对配置路径,不能保证命中原来的项目。

--interactive=false 适合明确不读 stdin 的任务。若应用需要输入确认、密码或菜单选择,应先找它提供的批处理参数和凭据读取方式;关闭输入连接不会替你完成提示,也不应把密码直接拼进命令行来绕过提示。

选项的位置会影响解析:-T 和 --interactive=false 放在服务名之前。服务名之后的内容是容器内要执行的命令及参数,写错位置可能把选项交给应用程序。

需要交互 shell 时,让 Compose 从文件读取配置

另一种情况是想进入 shell,却把 Compose 配置本身通过标准输入交给了命令:

printf '%s\n' "$COMPOSE_YAML" | docker compose -f - exec app sh

这里假设 COMPOSE_YAML 已经包含一份有效配置。-f - 表示从 stdin 读取 Compose 配置,左侧管道也因此占用了本来用于交互输入的通道。

给它加 -T 并不等于恢复了交互终端。错误可能消失,但 shell 仍拿不到你期望的键盘输入,甚至在输入结束后直接退出。报错消失后,还要确认 shell 能否接收键盘输入。

需要手工进入容器时,把配置保存到项目目录里的文件,随后在有真实终端的窗口中执行:

docker compose -f /srv/example/compose.yaml exec app sh

使用与启动时相同的配置和项目名,并确认镜像内有 sh。从 stdin 配置改为磁盘文件时,还要核对相对路径:stdin 配置中的路径以当前工作目录为基准,文件配置通常以第一个配置文件所在目录为基准。不要随手把配置移到另一个目录,再用它启动或更新服务。

如果只是要执行一条已知命令,就直接采用前面的批处理形式,不必先启动一个等待输入的 shell。旧的 COMPOSE_INTERACTIVE_NO_CLI 环境变量不受 Compose V2 支持,不应作为现代 Compose 的修复办法。

原生 docker exec 的参数不同

同样是执行容器命令,原生 docker exec 不默认启用标准输入连接和 TTY。向已有容器传入文本时,使用 -i,不要附带 -t:

printf 'probe\n' | docker exec -i example-app-1 cat

example-app-1 是示例容器名,不是 Compose 服务名。docker exec 没有与 Compose 相同的大写 -T 用法,因此不能把两个命令的选项原样互换。只有从终端进入交互 shell 时,才通常使用 docker exec -it。

修改后检查实际执行结果

先用回显文本或查询身份这类无写入操作,确认命令能结束、输出符合预期,再恢复实际业务命令。批处理应检查退出状态和业务结果;交互 shell 则要确认能够输入命令并看到响应。

验证时保留原来的调用方式:终端里直接执行成功,不能证明带管道的调用、cron 或 CI 也成功。若原任务会读取文件,就继续带文件输入验证;若它要求完全无人值守,就确认不会停在密码或确认提示上。

涉及输出文件时,不要把测试结果直接重定向到唯一一份备份。宿主机 shell 可能在 Docker 命令开始之前就创建或截断目标文件,即使命令随后报错,旧备份也可能已经被覆盖。使用新的临时文件名,确认退出状态和内容有效后再接入正式备份流程。

这些修改只涉及本次 exec 的调用选项,不需要重建容器、放宽权限或删除数据卷。保留原脚本副本即可撤回参数修改;如果实际命令已经写入数据,撤回选项不会撤销那次写入,仍需按应用或数据库的恢复流程处理。