资讯动态

用Docker沙箱给Claude Code上锁:无人值守任务的安全运行指南

发布时间:2026/9/9 8:01:12 来源:尧图企业网站定制
把 Claude Code 丢在后台跑任务这事儿我干过一次之后就再也不敢不设防了。那次它接到的任务是“修复测试失败”结果呢测试修好了它顺手把 CI 配置也改了往全局 node_modules 里塞了一堆依赖还给宿主机的用户目录写了一堆缓存文件。等我发现问题的时候整个开发环境已经没法信任了最后只能重装系统。这种“能力很强但管不住”的体验用过 Claude Code 跑无人值守任务的朋友应该都不陌生。后来我换了个思路既然管不住它那就把它关进笼子里跑。这个笼子就是 Docker Sandboxes。把 Claude Code 放进一次性的容器里文件系统只读、网络按最小化放行、CPU 内存磁盘全部限量任务跑完容器直接扔风险被限制在一个可以随时销毁的范围内。这篇文章就围绕这个思路讲讲从零搭一个 Claude Code 沙箱的全过程以及无监督运行时最容易踩的坑和对应的防护策略。不管你是想把它接进 CI/CD还是只想让它在后台替你干点活这套方案都值得参考。1. 无监督运行的真正风险不是“AI 失控”而是权限过大很多人一听说“给 AI Agent 做沙箱”第一反应是担心 AI 某天突然“觉醒”然后搞破坏。说实话这个担心有点科幻了。实际跑过 Claude Code 的人都清楚它压根不需要“觉醒”它只需要权限。1.1 它的本能就是“能改就改能装就装”Claude Code 的设计目标是在你的项目里当一个能干的工程师所以它会读文件、改代码、执行 shell 命令、调用各种工具。这听起来没问题问题在于当它运行在本机时它手里的权限和当前登录用户完全一样。这意味着它能做的所有事情本质上和一个拥有你账号完整权限的工程师坐在电脑前操作是一样的。改配置可以。装依赖可以。把代码推送到远端仓库也可以。它确实不会“故意”搞破坏但它会为了完成手头的任务做出很多你想不到的附带操作。我第一次遇到这种情况是跑一个“优化测试速度”的任务。我预期的操作是它去改几个测试文件结果它发现项目里缺少某个工具直接执行了全局安装命令把一堆包装到了系统目录。然后它又“贴心”地帮我改了环境变量让这个工具能被正确调用。整个流程逻辑上没问题但对我本就复杂的开发环境来说这简直就是一场灾难。版本冲突、路径污染、配置文件被改得面目全非光清理就花了我大半天。1.2 两个典型事故场景比“AI 失控”现实得多说两个我后来在社群里经常分享的案例都是真实发生过的事。场景一任务“修好失败的单元测试”结果流水线被打挂了。Claude Code 分析测试失败原因时发现 CI 配置里测试命令的参数不对。它没有像人类工程师那样犹豫一下“这是不是该改”而是直接修改了 CI 文件并提交。最终单测确实过了但整个流水线的行为被悄悄改变了后面几天的构建全在用一套没人审查过的新配置在跑。场景二为了装一个所谓的“必需依赖”把用户目录权限搞乱了。有一次它判断某个工具需要特定版本于是尝试用 sudo 执行安装命令。虽然系统提示密码导致操作失败但它在失败前已经往用户目录写入了大量缓存文件还把 PATH 的配置文件改了。等我发现时很多命令行工具的行为都不正常了。你要明白Claude Code 不是坏它是“眼里只有任务”。它不会像人一样思考“这个改动会不会影响别的东西”它的核心逻辑是“沿着最短路径完成任务”。在没有外部约束的环境里这种执行力就是破坏力。1.3 为什么偏偏是 Docker普通用户和虚拟机都不太合适我见过有人试图用“创建一个普通用户然后用这个用户跑 Claude Code”来解决问题。这个思路对了一半但远远不够。理由有两点第一普通用户依然能访问你的个人文件、SSH 密钥、代码仓库只是无法写系统目录而已对“代码泄露”这种风险几乎没有任何抵抗能力。第二进程之间的隔离依然很弱Claude Code 可以启动任意子进程这些子进程可以访问网络、读文件、做很多超范围的事情。虚拟机倒是隔离得很彻底但启动一个虚拟机动辄几分钟占掉几个 G 内存跑一个几秒钟的代码任务完全是大炮打蚊子。如果我要批量跑几十个任务虚拟机方案在资源和效率上都撑不住。Docker 容器的特点正好卡在这个需求点上秒级启动和销毁资源开销可以忽略不计文件系统、网络、进程都有独立的命名空间能做到“看起来在一个环境里实际上和宿主机隔开”资源限制参数非常成熟可以精确控制内存、CPU、磁盘写入容器可以做成只读的任务跑完丢弃成本极低。需要强调一句Docker 不是万能保险柜它共享宿主机内核存在理论上的逃逸风险。但对付“正常使用场景下不小心越界”的 Claude Code 任务它已经把风险从“灾难级”降到了“可控级”。对于个人和绝大多数团队来说这个性价比已经非常高了。2. 沙箱的边界设计文件、进程、网络、资源四个维度都要管搭建沙箱不是“装个 Docker 然后跑一下”那么简单。我把边界设计拆成四个维度每个维度都要有明确的策略否则就会出现“关住了文件系统但网络还是裸奔”这种漏洞。2.1 文件系统代码只读工作目录单独给一块这是整个沙箱里最重要的一条。Claude Code 需要读取你的项目代码但绝大多数场景下它不需要修改原代码。所以我通常把代码库挂载成只读-v /path/to/repo:/repo:ro:ro表示只读。容器里的 Claude Code 可以打开、分析、搜索这些文件但一旦它尝试修改或删除就会直接报权限错误。这个错误会打断它的操作链让任务在中途停下来而不是继续“发挥”。同时要给容器一个可写的工作目录比如/work挂载一个宿主机上的独立目录-v /path/to/work:/workClaude Code 在任务的中间过程会产生大量临时文件、缓存、修改结果这些都应该被限制在/work里。代码原稿保持只读不产生任何污染。这种做法还有一个好处任务结束后我只要看/work里发生了什么改动就行了不需要去检查整个项目有没有被动过手脚。2.2 进程与权限坚决不用 root把不必要的系统权限全部摘掉很多人在容器里默认用 root 跑觉得省事。但放进沙箱里的本来就是一个“不能完全信任”的 Agent给它 root 权限就相当于把整个容器命名空间的门都打开了。正确的做法是在容器里创建一个普通用户用这个用户运行 Claude Code用--cap-dropALL把 Linux capabilities 全部丢弃让容器内的进程失去 mount、raw socket、ptrace 等敏感能力加上--security-opt no-new-privileges禁止任何 setuid 提权行为用--pids-limit限制进程数量防止它 fork 出一堆子进程把机器拖垮。这些参数单个看好像没什么组合起来效果非常明显。Claude Code 在容器里能做的事情被压缩到“只能操作挂载进去的文件、只能运行普通用户能运行的程序”。2.3 网络按需开放没有明确需求就不要给网络我可以很坦白地说Claude Code 在无人值守模式下最大的不可控因素就是网络。它有可能出于“完成任务的直觉”向外部服务发送请求、拉取依赖、上传代码片段。所以网络策略必须在一开始就定死。最安全的策略是如果模型 API 可以通过内网网关访问那就让容器完全没有公网路由如果必须访问某些地址就把出口白名单严格控制住。具体怎么操作我在第 5 章展开这里先记住一个原则网络是默认关闭的打开必须经过明确的授权。2.4 资源CPU、内存、磁盘配额一个都不能少AI Agent 写出来的代码很可能有死循环或者生成大量中间文件这不是骂它是确确实实会发生的事。尤其是当它认为“多试几次”能让任务成功时可能瞬间飙出几十个进程。所以在docker run里我基本形成了肌肉记忆一样会写上的参数是资源维度参数示例作用内存--memory2g --memory-swap2g超过上限直接 OOM不让它无限占用CPU--cpus1.0限制 CPU 核数跑出死循环也烧不掉整台机器进程数--pids-limit512防止 fork 炸弹式操作磁盘写--storage-opt size20g限制容器可写层大小防止写爆磁盘这些参数看上去是限制实际上是在给意外事故兜底。我就遇到过 Claude Code 为了“分析所有可能性”生成了一批模拟数据文件几分钟就写了几 G。如果不是磁盘配额我那台机器早就被填满了。3. 从零搭建Claude Code 专用 Docker 沙箱的完整实操理论讲完了下面是最干的部分从零把一个可以无人值守跑 Claude Code 的 Docker 沙箱搭起来。这部分参考了我在多台机器上反复测试后的最终配置你可以直接抄。3.1 基础镜像怎么选尽量精简不要用全量系统镜像Claude Code 是 Node.js 工具运行环境首选 Node 官方镜像。我强烈建议选node:20-slim这类精简版不要用ubuntu:latest理由有两条一是体积。slim 镜像压缩后可能只有几十兆ubuntu 全量镜像动不动两百多兆起步构建和拉取都慢。二是攻击面。镜像里装的东西越少能被 Agent 拿来当“武器”或者被漏洞利用的面就越小。这里有个坑要提前说很多人喜欢用 Alpine 镜像觉得更小。但 Alpine 用的是 musl libc不是主流的 glibcClaude Code 的某些原生依赖在 Alpine 上会出现兼容性问题。我自己测试时就遇到过装好了但启动直接报错的情况所以省心起见统一用 slim 系列。另外Claude Code 的安装方式是通过 npm 全局安装。如果你用的是 Node 18 以下的版本很可能装不上或者运行报错建议至少 Node 18具体以你安装版本对应的官方要求为准。3.2 写一个完整的 Dockerfile下面这个 Dockerfile 我目前一直在用属于比较稳妥的版本你可以根据自己项目情况调整。FROM node:20-slim # 安装 Claude Code 依赖的常用系统工具 # git 用于版本操作ca-certificates 用于 HTTPS 调用 RUN apt-get update \ apt-get install -y --no-install-recommends git ca-certificates \ rm -rf /var/lib/apt/lists/* # 安装 Claude Code建议锁版本方便复现 RUN npm install -g anthropic-ai/claude-code # 创建非 root 用户 # 这个 UID 后面要跟宿主机的用户 UID 对齐才能正常读写挂载目录 RUN useradd -m -u 1000 sandbox \ mkdir -p /work /out \ chown -R sandbox:sandbox /work /out USER sandbox WORKDIR /work # 默认执行 claude ENTRYPOINT [claude]几点说明useradd -m -u 1000 sandbox创建了一个 UID 为 1000 的普通用户。如果宿主机当前用户的 UID 就是 1000那挂载目录的读写权限就正好符合预期ENTRYPOINT设置成claude这样docker run时后面直接跟参数就能被 Claude Code 接收省去每次手动敲完整命令/out目录我预留为结果输出目录后面挂载到宿主机时方便留存产物。构建镜像docker build -t cc-sandbox .3.3 验证镜像里的 Claude Code 能不能跑先做一次最基本的启动测试docker run --rm cc-sandbox --version如果能看到 Claude Code 的版本号说明镜像构建成功。我再补一个建议在看不清状况之前不要直接跑正式任务。先进去敲一下--help确认你手上这个版本的参数和你预期一致。不同版本的 Claude Code权限模式和非交互运行的参数名可能会有差异这些全部以claude --help输出的实际内容为准不要在没确认的情况下凭感觉传参。3.4 认证与密钥注入API Key 绝不能写进镜像Claude Code 运行必须要有模型服务的认证信息。最容易犯的错误就是把 API Key 直接写进 Dockerfile 里。一旦镜像被推到仓库或者分享给同事Key 就泄露了。我用的方案是运行时注入。两种方式任选第一种通过环境变量文件# 写一个 .env.cc 文件里面放认证信息 # ANTHROPIC_API_KEYsk-xxxx # ANTHROPIC_BASE_URLhttp://localhost:8080 docker run --env-file .env.cc ...第二种通过挂载只读的密钥文件。有些场景密钥信息不想暴露在进程环境变量里可以把它挂载进容器再通过 Claude Code 支持的配置文件路径指向它。如果你模型服务走的是私有化网关或者兼容 Anthropic API 格式的服务通过ANTHROPIC_BASE_URL这类环境变量把请求指向内网网关就行认证信息同样只能在运行时注入。3.5 挂载策略代码只读工作目录可写结果单独存放我把一次任务的挂载分成三块各管各的# 代码原稿只读 -v /home/me/projects/myapp:/repo:ro # 工作目录Claude Code 的所有改动都落在这里 -v /home/me/cc-work:/work # 结果输出目录任务结束后保留产物 -v /home/me/cc-out:/out需要留意的是如果你宿主机上的用户 UID 不是 1000可能会出现“容器里能写 /work 但宿主机上文件属主很乱”或者“容器里根本没有权限写挂载目录”的情况。最简单的解法是运行时用--user参数直接指到当前用户docker run --user $(id -u):$(id -g) ...这样容器进程的 UID 就和宿主机当前用户对齐权限问题基本不会出现。4. 无监督启动命令、超时、资源限制的兜底组合环境搭好之后真正决定安全级别的其实是“怎么启动”这件事。我总结了一套组合拳目的只有一个即便 Claude Code 跑偏了也能在一个可控范围内被兜住。4.1 一个可以直接抄的启动命令下面这条命令是我在真实跑任务时用的模板你可以照搬后改改路径和任务描述docker run --rm \ --name cc-task-001 \ --user $(id -u):$(id -g) \ --network sandbox-net \ --cap-dropALL \ --security-opt no-new-privileges \ --pids-limit512 \ --memory2g \ --memory-swap2g \ --cpus1.0 \ --stop-timeout10 \ --init \ --env-file .env.cc \ -v $PWD/repo:/repo:ro \ -v $PWD/work:/work \ -v $PWD/out:/out \ cc-sandbox \ -p 分析 /repo 下的代码结构把模块清单写到 /out/modules.txt不要修改任何代码文件逐项拆解一下--rm容器退出后自动删除不留残余容器--name cc-task-001给容器命名便于查看日志和停止--user $(id -u):$(id -g)对齐 UID/GID解决文件权限问题--network sandbox-net使用一个受限的自定义网络后面第 5 章细说--cap-dropALL丢全部 Linux capabilities--security-opt no-new-privileges禁止提权--pids-limit512、--memory2g、--cpus1.0资源兜底--init用 Docker 内置的 init 进程tini接管子进程和信号转发避免僵尸进程-pClaude Code 的非交互模式参数任务直接在后台执行不需要终端。4.2 非交互模式的正确用法无人值守环境下Claude Code 必须跑在非交互模式里。你肯定不希望任务执行到一半它在终端里弹出一个“是否执行此命令”的询问然后整个程序卡在那里等着一个永远不回来的人敲回车。Claude Code 提供了类似print模式的选项也就是命令里的-p。它会把任务作为单次命令执行结束后自动退出。我强烈建议在跑正式任务前先跑一次claude --help确认你安装的版本里非交互模式的参数到底是什么。不同版本之间确实有差异别盲信网上任何一篇旧教程。另外Claude Code 有一个“跳过所有权限确认”级别的高风险选项。我的原则很明确在宿主机上绝对不碰它但在沙箱里它反而可以变成必需项。因为无人值守模式下没人敲 y权限确认反而会导致任务挂起。沙箱已经把可操作的边界控制住了“跳过确认”的破坏力也就被限制在了容器内部。你可以在claude --help里找 permission 相关的选项按需使用。4.3 超时兜底任务卡住不退出怎么办AI Agent 执行任务时最让人头疼的其实不是报错而是“既不报错也不结束”的悬挂状态。有可能是它在一个问题上来回尝试有可能是生成的子进程没退出也有可能是网络请求在等待响应。无论哪种都必须有一个机制把它按时间杀掉。我的做法是双重兜底第一层外部 timeout。在宿主机上把整个 docker run 包进timeout命令timeout 600 docker run --rm ... cc-sandbox -p 任务描述600 秒跑不完就直接终止。这个时间要按任务复杂度来定别设太短导致任务频繁被杀也别设太长导致失控时间太久。第二层容器内部的--init和--stop-timeout10。--init保证容器的第一个进程是 tini能正确回收孤儿进程--stop-timeout控制在收到停止信号后最多多少秒优雅退出超时就直接杀。4.4 资源限制在真实任务里的效果有人觉得 2G 内存不够用我把默认值设成 2G 是因为大部分代码分析任务跑不到这个阈值。如果确实需要更多可以调但最好是在同一个沙箱镜像里先做过测试再调。比如任务要加载大型代码库或跑数据预处理内存需求高那可以设成 4G但不要无脑给。CPU 限制到 1.0 的效果比较明显。它不会显著拖慢大多数任务但如果 Claude Code 生成了一段死循环代码这个限制能让整个机器不至于被拖垮。我实际遇到过一次无限递归如果没有--cpus限制那台 8 核机器很快就满了其他服务全都跟着遭殃。5. 网络出口控制只放行它该去的地方别的全堵住如果说前几章是把 Claude Code 关进了房间那网络出口就是房间的门锁。门锁不给力前面所有的工作都白费。这一章我详细讲讲我是怎么给沙箱做出口控制的。5.1 为什么非要管住网络不可Claude Code 为了完成任务会自己做很多“合理的判断”。比如发现缺依赖它可能执行npm install这会向公网的 npm registry 发出请求又比如它觉得应该在远端仓库创建一个分支就可能调用 git push。这些行为在它看来很正常但对企业内部项目来说代码文件一旦被推到未授权地址就是安全事故。更要命的是如果 Agent 的主进程能访问宿主机网络它就有机会扫描内网、访问本不该访问的内部服务。所以网络控制不是技术洁癖是必须。5.2 方案A全内网模式容器之间通信不碰公网如果你使用的是私有化部署的模型服务或者已经把模型网关容器化我最推荐用 Docker 的--internal网络。这个模式下创建的容器网段完全不提供公网路由容器无法访问任何外部地址最多只能和同一网络里的其他容器互通。操作步骤# 创建内部网络 docker network create --internal sandbox-net # 模型网关容器也接这个网络 docker run -d --network sandbox-net --name model-gateway ... my-gateway # Claude Code 容器再接这个网络 docker run --rm --network sandbox-net ... cc-sandbox -p 任务这样Claude Code 唯一能访问的网络服务就是同一个sandbox-net里的模型网关公网上的一切都不可达。代码数据完全留存在内网不经过任何公共出口。这个方案在安全角度上是最干净的。5.3 方案B宿主机防火墙出口白名单有些场景下模型网关没有容器化或者内网还有别的服务需要容器访问不能完全去掉路由。这时候用“固定子网 防火墙白名单”的方式也能做到类似效果。先创建一个固定子网的 bridge 网络docker network create --subnet172.20.0.0/16 sandbox-net然后在宿主机上配置防火墙只允许这个子网访问白名单内的目标地址和端口其余流量全部丢弃。比如只允许访问内网网关172.16.1.10的8080端口iptables -I FORWARD -s 172.20.0.0/16 -d 172.16.1.10 -p tcp --dport 8080 -j ACCEPT iptables -I FORWARD -s 172.20.0.0/16 -j DROP规则顺序很重要先放行白名单再丢弃全部。如果你是用 firewalld 或者 ufw也可以用对应的规则语法逻辑一致。另外可以在容器启动时加--add-host api.internal:172.16.1.10把模型网关的域名写死在容器 hosts 文件里避免走外部 DNS 解析减少一层依赖。5.4 审计日志把“它干了什么”留下来前面所有控制都是在防“它乱来”但这还不够。万一真的发生意外你得知道它到底干了什么才能定位和补救。我的建议是至少留三份记录容器标准输出和标准错误通过重定向落到宿主机日志文件Claude Code 的会话日志让它把每次工具调用、文件读写都写进挂载的/out目录/work目录里所有最终产物任务结束后整体打包归档。宁可多留一份日志也不要事后靠猜。对于无人值守的 AI Agent可观测性就是最后一道防线。日志不是用来事后追责的是用来还原现场和持续改进提示词策略的。6. 沙箱实战排查四个最常见的坑和完整定位思路搭建过程里我踩过的坑不少有些问题网上的资料说得模棱两可我实际排障时绕了不少弯。这里整理四个出现频率最高的问题每个都附上我的完整排查思路而不是只给一个“改一下就行”的答案。6.1 坑一容器里没有权限写挂载目录现象Claude Code 能正常读取代码但一旦尝试往/work里写文件就报 Permission denied任务中途大量失败。排查链路先确认容器进程的用户身份再对照挂载目录的属主。用docker run --rm -v $PWD/work:/work --user $(id -u):$(id -g) cc-sandbox ls -ln /work看结果。如果进程用户的 UID 和挂载目录属主 UID 对不上就会出现“看得到目录但写不进去”的情况。解决统一 UID。跑容器时固定加--user $(id -u):$(id -g)或者构建镜像时把 sandbox 用户的 UID 改成宿主用户一致的数值。不要图省事直接用 root否则容器里生成的缓存文件落到宿主机上全是 root 所有后面清理和归档都会很麻烦。6.2 坑二无 TTY 环境下认证一直卡在登录流程现象docker run 不带-it运行后日志一直停在“请打开浏览器完成登录”之类的交互式认证界面。排查链路这说明 Claude Code 走的是交互式 OAuth 登录流程而不是 API Key 认证。先检查传入容器的环境变量里有没有正确设置认证信息再进容器手动执行env确认变量真的传进去了。解决无人值守环境统一使用 API Key 方式认证通过--env-file注入。一次性排查命令可以这样写docker run --rm --env-file .env.cc cc-sandbox env看到认证相关变量存在后再跑正式任务。如果这一步没有做很多人会在“明明本地能跑容器里却不行”的问题上卡很久。6.3 坑三任务完了容器却不退出现象Claude Code 已经输出结果但 docker run 一直挂着不结束。docker ps能看到容器还在 Running。排查链路这通常不是 Claude Code 主进程没退出而是它启动的子进程有残留。比如它生成了后台运行的程序或者某些 Node.js 子进程没有正常收尾。先docker stop看能否正常停止再用--init启动容器看是否复现。解决给 docker run 加上--init确保容器内信号和孤儿进程由 tini 接管。同时在宿主机侧用timeout兜底设置一个合理的最大执行时间防止万一真的卡死了整个流程。这个组合我现在每次都会写上。6.4 坑四容器连不上模型服务现象网络或认证已经配好但

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价