向 Docker 容器内的 Bash 发送命令:完整指南

> 发布日期:2026-03-31 | 作者:Tony @ Jay's Lab

>

> 从手动调试到程序化控制,覆盖 Docker 容器内命令执行的所有方式。包含 CLI、SDK、API、长连接交互等场景,附带代码示例和最佳实践。

1. 概览:为什么需要向容器发命令?

Docker 容器本质上是隔离的进程空间。向容器内发命令的场景包括:

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 bash`
自动化脚本`docker exec bash -c "..."`
应用集成Docker SDK(Go/Python/Node)
AI Agent 控制Docker SDK + 超时 + 截断 + 黑名单
长期交互 sessionWebSocket Agent 或命名管道
CI/CD`docker compose exec -T`

核心原则

1. 用 exec 不用 attach

2. 非 root 执行

3. 设超时防死循环

4. 截断输出防爆上下文

5. 生产环境限制资源(memory/cpu/pids)

参考链接