向 Docker 容器内的 Bash 发送命令:完整指南
> 发布日期:2026-03-31 | 作者:Tony @ Jay's Lab
>
> 从手动调试到程序化控制,覆盖 Docker 容器内命令执行的所有方式。包含 CLI、SDK、API、长连接交互等场景,附带代码示例和最佳实践。
1. 概览:为什么需要向容器发命令?
Docker 容器本质上是隔离的进程空间。向容器内发命令的场景包括:
- 调试排错:进入容器检查日志、网络、进程状态
- 自动化运维:批量执行配置更新、健康检查
- CI/CD 管道:在构建容器内运行测试、编译
- AI Agent 控制:LLM 通过工具调用操作用户容器(如小虾 xiaoxia.app 的场景)
- 编排系统:Kubernetes exec、Docker Compose 管理
2. 方法一:docker exec(最常用)
2.1 一次性执行
# 基础用法
docker exec <container> bash -c "ls -la /app"
# 指定工作目录
docker exec -w /app <container> bash -c "npm test"
# 设置环境变量
docker exec -e NODE_ENV=production <container> bash -c "node server.js"
# 以特定用户执行
docker exec -u root <container> bash -c "apt update"
2.2 交互式进入(调试用)
# 进入容器的 bash shell
docker exec -it <container> bash
# 如果容器没有 bash,用 sh
docker exec -it <container> sh
# 进入后可以像正常 shell 一样操作
# exit 或 Ctrl+D 退出
-i 和 -t 的含义:
| 参数 | 作用 | 什么时候需要 |
|---|---|---|
| `-i` (interactive) | 保持 stdin 打开 | 需要输入时(管道、交互) |
| `-t` (tty) | 分配伪终端 | 需要终端 UI 时(vim、top、颜色输出) |
| `-it` 组合 | 完整交互式终端 | 人工调试 |
| 都不加 | 只执行,不交互 | 自动化脚本 |
2.3 多条命令
# 方式 1:bash -c 内用 && 或 ;
docker exec <container> bash -c "cd /app && npm install && npm run build"
# 方式 2:heredoc(更可读)
docker exec -i <container> bash << 'EOF'
cd /app
echo "Starting build..."
npm install
npm run build
echo "Done!"
EOF
# 方式 3:执行本地脚本
docker exec -i <container> bash < ./local-script.sh
2.4 常用参数速查
docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
-i, --interactive 保持 stdin 打开
-t, --tty 分配伪终端
-d, --detach 后台执行(不等待结果)
-e, --env KEY=VAL 设置环境变量
-u, --user USER 指定执行用户
-w, --workdir DIR 指定工作目录
--privileged 特权模式(慎用)
3. 方法二:管道传命令
当需要从外部程序动态发送命令时:
# 基础管道
echo "ls /app" | docker exec -i <container> bash
# 多条命令
echo -e "cd /app\nls -la\npwd" | docker exec -i <container> bash
# 从文件读取命令
cat commands.txt | docker exec -i <container> bash
# 变量替换(注意:变量在宿主机展开)
APP_DIR="/app"
echo "ls $APP_DIR" | docker exec -i <container> bash
⚠️ 注意 -i 必须加,否则 stdin 不会传入容器。
4. 方法三:Docker SDK(程序化控制)
4.1 Go(Docker SDK)
package main
import (
"context"
"fmt"
"io"
"os"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
func execInContainer(containerID, command string) (string, error) {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return "", err
}
defer cli.Close()
// 创建 exec 实例
execConfig := types.ExecConfig{
Cmd: []string{"bash", "-c", command},
AttachStdout: true,
AttachStderr: true,
}
exec, err := cli.ContainerExecCreate(ctx, containerID, execConfig)
if err != nil {
return "", err
}
// 附加并读取输出
resp, err := cli.ContainerExecAttach(ctx, exec.ID, types.ExecStartCheck{})
if err != nil {
return "", err
}
defer resp.Close()
output, _ := io.ReadAll(resp.Reader)
return string(output), nil
}
func main() {
output, err := execInContainer("my-container", "ls -la /app")
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(output)
}
4.2 Python(docker-py)
import docker
client = docker.from_env()
container = client.containers.get("my-container")
# 简单执行
exit_code, output = container.exec_run("bash -c 'ls -la /app'")
print(f"Exit code: {exit_code}")
print(output.decode())
# 流式输出(适合长命令)
exit_code, stream = container.exec_run(
"bash -c 'npm install'",
stream=True,
demux=True # 分离 stdout 和 stderr
)
for stdout_chunk, stderr_chunk in stream:
if stdout_chunk:
print(stdout_chunk.decode(), end='')
if stderr_chunk:
print(f"[ERR] {stderr_chunk.decode()}", end='')
# 带环境变量和工作目录
exit_code, output = container.exec_run(
"node server.js",
environment={"NODE_ENV": "production", "PORT": "3000"},
workdir="/app",
user="node"
)
4.3 Node.js(dockerode)
const Docker = require('dockerode');
const docker = new Docker();
async function execInContainer(containerId, command) {
const container = docker.getContainer(containerId);
const exec = await container.exec({
Cmd: ['bash', '-c', command],
AttachStdout: true,
AttachStderr: true,
});
const stream = await exec.start({ hijack: true, stdin: false });
return new Promise((resolve) => {
let output = '';
stream.on('data', (chunk) => { output += chunk.toString(); });
stream.on('end', () => resolve(output));
});
}
// 使用
const result = await execInContainer('my-container', 'ls -la /app');
console.log(result);
5. 方法四:Docker Engine API(HTTP)
直接调 Docker 守护进程的 REST API,适合不想引入 SDK 的场景。
5.1 创建并执行
# Step 1: 创建 exec 实例
EXEC_ID=$(curl -s --unix-socket /var/run/docker.sock \
-X POST "http://localhost/containers/<container>/exec" \
-H "Content-Type: application/json" \
-d '{"Cmd":["bash","-c","ls /app"],"AttachStdout":true,"AttachStderr":true}' \
| jq -r '.Id')
# Step 2: 启动并获取输出
curl -s --unix-socket /var/run/docker.sock \
-X POST "http://localhost/exec/$EXEC_ID/start" \
-H "Content-Type: application/json" \
-d '{"Detach":false,"Tty":false}'
5.2 远程 Docker API(TCP)
# 如果 Docker 开启了 TCP 端口(默认 2375/2376)
curl -X POST "https://docker-host:2376/containers/my-app/exec" \
--cert client-cert.pem --key client-key.pem \
-H "Content-Type: application/json" \
-d '{"Cmd":["bash","-c","echo hello"],"AttachStdout":true}'
⚠️ 安全提醒:暴露 Docker API 等于给 root 权限,必须用 TLS 客户端证书认证。
6. 方法五:长连接交互式 Session
当需要保持一个持续的 bash session(多次发命令,保持状态):
6.1 命名管道(简单方案)
# 创建管道
mkfifo /tmp/docker-stdin
# 启动后台 bash session
docker exec -i <container> bash < /tmp/docker-stdin &
DOCKER_PID=$!
# 发送命令
echo "cd /app" > /tmp/docker-stdin
echo "ls -la" > /tmp/docker-stdin
echo "pwd" > /tmp/docker-stdin # 会输出 /app(状态保持了!)
# 结束
echo "exit" > /tmp/docker-stdin
rm /tmp/docker-stdin
6.2 socat 双向通信
# 方式 1:通过 Unix socket 中转
socat EXEC:"docker exec -i my-container bash",pty TCP-LISTEN:9999,reuseaddr,fork
# 远程连接
socat - TCP:localhost:9999
6.3 Go 实现(带 PTY 的持久 Session)
// 创建一个带 TTY 的 exec session
execConfig := types.ExecConfig{
Cmd: []string{"bash"},
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
}
exec, _ := cli.ContainerExecCreate(ctx, containerID, execConfig)
// 附加到 session(双向流)
resp, _ := cli.ContainerExecAttach(ctx, exec.ID, types.ExecStartCheck{Tty: true})
// 写入命令
resp.Conn.Write([]byte("ls /app\n"))
// 读取输出
buf := make([]byte, 4096)
n, _ := resp.Reader.Read(buf)
fmt.Println(string(buf[:n]))
// 继续发命令(session 保持状态)
resp.Conn.Write([]byte("cd /app && pwd\n"))
6.4 WebSocket 方案(最适合 Agent 场景)
在容器内运行一个轻量 WebSocket 服务,外部通过 WebSocket 发送命令:
# 容器内:agent.py
import asyncio
import websockets
import subprocess
async def handler(websocket):
async for message in websocket:
# 执行收到的命令
result = subprocess.run(
message, shell=True, capture_output=True, text=True, timeout=30
)
await websocket.send(json.dumps({
"stdout": result.stdout,
"stderr": result.stderr,
"exit_code": result.returncode
}))
asyncio.run(websockets.serve(handler, "0.0.0.0", 8765))
# 宿主机:调用方
import websockets
async with websockets.connect("ws://container-ip:8765") as ws:
await ws.send("ls -la /app")
result = await ws.recv()
print(result)
7. 方法对比
| 方法 | 适用场景 | 状态保持 | 流式输出 | 编程友好 | 复杂度 |
|---|---|---|---|---|---|
| `docker exec` | 日常调试、脚本 | ❌ | ❌ | ⭐ | 低 |
| 管道 | 批量命令 | ❌ | ❌ | ⭐⭐ | 低 |
| Docker SDK | 应用集成 | ❌ | ✅ | ⭐⭐⭐ | 中 |
| Docker API | 无 SDK 环境 | ❌ | ✅ | ⭐⭐ | 中 |
| 命名管道 | 简单长 session | ✅ | ⚠️ | ⭐ | 低 |
| WebSocket Agent | **AI Agent 控制** | ✅ | ✅ | ⭐⭐⭐ | 高 |
8. AI Agent 场景的最佳实践
当 LLM 需要操控 Docker 容器时(如小虾 xiaoxia.app 的场景),推荐架构:
8.1 架构
LLM (云端)
│ tool_call: exec("npm install")
▼
Agent Controller (Go/Python 服务)
│ Docker SDK
▼
Docker Engine
│
▼
User Container (bash)
8.2 关键设计点
1. 超时控制
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 防止 LLM 发出 `yes | rm -rf /` 之类的死循环命令
2. 输出截断
# LLM 的上下文窗口有限,截断过长输出
MAX_OUTPUT = 10000 # 字符
output = result.stdout[:MAX_OUTPUT]
if len(result.stdout) > MAX_OUTPUT:
output += f"\n... (截断,完整输出 {len(result.stdout)} 字符)"
3. 命令黑名单
BLOCKED_COMMANDS = [
r'rm\s+-rf\s+/', # 删除根目录
r':()\{.*\|.*&\}', # fork bomb
r'mkfs\.', # 格式化磁盘
r'dd\s+if=.*of=/dev', # 覆写设备
]
4. 资源限制
# 创建容器时就限制资源
docker run -d \
--memory=512m \
--cpus=1 \
--pids-limit=100 \
--read-only \
--tmpfs /tmp:size=100m \
my-app
5. 非 root 执行
# Dockerfile
RUN useradd -m appuser
USER appuser
# Agent 的命令以 appuser 身份执行,不是 root
8.3 完整 Agent Controller 示例(Go)
package agent
import (
"context"
"fmt"
"io"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
type ExecResult struct {
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
ExitCode int `json:"exit_code"`
TimedOut bool `json:"timed_out"`
}
type AgentController struct {
cli *client.Client
containerID string
timeout time.Duration
maxOutput int
}
func NewAgentController(containerID string) (*AgentController, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
return &AgentController{
cli: cli,
containerID: containerID,
timeout: 30 * time.Second,
maxOutput: 10000,
}, nil
}
func (a *AgentController) Exec(command string) (*ExecResult, error) {
ctx, cancel := context.WithTimeout(context.Background(), a.timeout)
defer cancel()
exec, err := a.cli.ContainerExecCreate(ctx, a.containerID, types.ExecConfig{
Cmd: []string{"bash", "-c", command},
AttachStdout: true,
AttachStderr: true,
})
if err != nil {
return nil, fmt.Errorf("exec create: %w", err)
}
resp, err := a.cli.ContainerExecAttach(ctx, exec.ID, types.ExecStartCheck{})
if err != nil {
return nil, fmt.Errorf("exec attach: %w", err)
}
defer resp.Close()
output, _ := io.ReadAll(resp.Reader)
outStr := string(output)
// 截断
if len(outStr) > a.maxOutput {
outStr = outStr[:a.maxOutput] + fmt.Sprintf(
"\n... (truncated, full output %d chars)", len(output))
}
// 获取退出码
inspect, _ := a.cli.ContainerExecInspect(ctx, exec.ID)
return &ExecResult{
Stdout: outStr,
ExitCode: inspect.ExitCode,
TimedOut: ctx.Err() == context.DeadlineExceeded,
}, nil
}
9. docker exec vs docker attach
一个常见混淆:
| `docker exec` | `docker attach` | |
|---|---|---|
| 作用 | 在容器内**新建**一个进程 | 附加到容器的**主进程** (PID 1) |
| 退出影响 | 不影响容器 | **可能导致容器停止** |
| 多实例 | 可以同时开多个 | 多人 attach 看到同一个输出 |
| 用途 | 调试、执行命令 | 查看主进程日志 |
99% 的情况用 docker exec,不要用 docker attach。
10. 常见问题
Q: 容器里没有 bash 怎么办?
# 用 sh(几乎所有镜像都有)
docker exec -it <container> sh
# Alpine 镜像默认只有 ash/sh
docker exec -it <container> /bin/ash
Q: 怎么知道容器里有哪些 shell?
docker exec <container> cat /etc/shells
# 或者
docker exec <container> which bash sh ash zsh 2>/dev/null
Q: exec 命令卡住不返回?
# 加超时
timeout 30 docker exec <container> bash -c "some-command"
# 或者后台执行
docker exec -d <container> bash -c "some-long-task"
Q: 怎么在 docker-compose 里 exec?
# 直接用服务名
docker compose exec web bash -c "python manage.py migrate"
# 不分配 TTY(CI 环境)
docker compose exec -T web bash -c "pytest"
Q: Kubernetes 里怎么做?
# kubectl exec 语法类似
kubectl exec -it <pod> -- bash -c "ls /app"
# 指定容器(多容器 Pod)
kubectl exec -it <pod> -c <container> -- bash
11. 总结
| 场景 | 推荐方法 |
|---|---|
| 快速调试 | `docker exec -it |
| 自动化脚本 | `docker exec |
| 应用集成 | Docker SDK(Go/Python/Node) |
| AI Agent 控制 | Docker SDK + 超时 + 截断 + 黑名单 |
| 长期交互 session | WebSocket Agent 或命名管道 |
| CI/CD | `docker compose exec -T` |
核心原则:
1. 用 exec 不用 attach
2. 非 root 执行
3. 设超时防死循环
4. 截断输出防爆上下文
5. 生产环境限制资源(memory/cpu/pids)