@alexgiantwhale: https://x.com/alexgiantwhale/status/2079342482593309022

X AI KOLs Timeline 工具

摘要

一篇详细介绍如何从零搭建 Dev Container 的技术文章,包括编写 Dockerfile、devcontainer.json、配置安全选项和生命周期命令,以固化 Go 开发环境并限制容器权限。

https://t.co/5s6uyYd7oX
查看原文
查看缓存全文

缓存时间: 2026/07/21 16:46

从零搭一个 Dev Container:把开发环境和 Coding Agent 一起放进容器

上一篇我用一个最小 Docker 实验验证了:同一个进程,挂载和运行参数不同,能看到、能修改的东西就不同。

原理不复杂,但真让我每天手敲一条带十几个参数的 docker run,我肯定坚持不了。

更实际的做法,是把开发环境写进仓库:新人 clone 下来不用先装一圈工具,CI、编辑器和 Coding Agent 也尽量使用同一套环境。

这就是 Dev Container 要解决的问题。

这篇不只讲怎么在 VS Code 里点 “Reopen in Container”。我会从一个真实的 Go 小项目开始,写出 .devcontainer/,用官方 CLI 启动,再进入容器检查 UID、capabilities、seccomp 和工作区写权限。

先说结论:Dev Container 很适合固化开发环境,也能承载一部分权限配置;但它默认优先考虑开发体验,不是开箱即用的安全沙箱。

Dev Container 到底是什么

Dev Container 是一个开放规范,不是 VS Code 私有格式。

它允许你用仓库里的 devcontainer.json 描述“开发容器应该怎样创建和使用”,支持它的工具再把这些配置翻译成 Docker 参数。

一个常见目录是:

text.devcontainer/ ├── devcontainer.json ├── Dockerfile └── verify.sh

三者的职责不同:

文件负责什么Dockerfile镜像里长期存在的系统、运行时和 OS 依赖devcontainer.json用户、挂载、端口、编辑器设置、生命周期命令和运行参数verify.sh检查最终环境是否真的符合预期

可以把它理解成:Dockerfile 定义“这台开发机里装了什么”,devcontainer.json 定义“这台开发机如何启动、如何接入”。

第一步:准备开发镜像

示例项目是上一篇的 Go 权限探针。我先写一个很短的 Dockerfile:

dockerfileFROM mcr.microsoft.com/devcontainers/go:1-1.22-bookworm

稳定、全项目共用的 OS 依赖放在这里。

凭据不要写进镜像层。

我刻意固定了 Go 和 Debian 版本,而不是使用 latest。

开发镜像会进入每个人的日常环境。浮动标签今天能构建,几周后可能换掉 Go、系统库甚至默认配置。需要升级时,应该主动改版本、重建、跑测试,而不是让升级悄悄发生。

如果只需要官方镜像,也可以直接在 devcontainer.json 里写 image;需要安装 apt 包、系统库或自定义工具时,再使用 Dockerfile。

第二步:写 devcontainer.json

这是我实际跑通的配置:

json{ “name”: “Agent Sandbox Demo”, “build”: { “dockerfile”: “Dockerfile”, “context”: “..” }, “remoteUser”: “vscode”, “containerUser”: “vscode”, “updateRemoteUserUID”: true, “securityOpt”: [ “no-new-privileges”, “seccomp=builtin” ], “runArgs”: [ “–cap-drop=ALL”, “–pids-limit=256”, “–memory=1g”, “–cpus=2” ], “mounts”: [ “source=agent-sandbox-go-cache,target=/go/pkg/mod,type=volume” ], “postCreateCommand”: “go test ./… && .devcontainer/verify.sh”, “customizations”: { “vscode”: { “extensions”: [“golang.go”], “settings”: { “go.toolsManagement.autoUpdate”: false } } } }

逐段看它做了什么。

build:从哪个镜像开始

dockerfile 的路径相对于 devcontainer.json。因为 Dockerfile 在 .devcontainer/ 里,而构建上下文要覆盖项目根目录,所以 context 写 ..。

构建上下文决定 Docker build 能看到哪些文件。上下文不要无脑指向更大的父目录,同时用 .dockerignore 排除 .git、构建产物和本地临时文件。

remoteUser 和 containerUser:不要默认用 root

这两个名字容易混:

  • containerUser 控制容器整体以哪个用户运行;

  • remoteUser 控制编辑器、终端、任务和调试器连接后使用哪个用户。

这里都设成镜像内置的 vscode。updateRemoteUserUID: true 会在 Linux 上尝试把容器用户 UID/GID 对齐宿主机用户,减少 bind mount 的权限问题。

securityOpt 和 runArgs:把边界写进配置

我设置了:

  • no-new-privileges:禁止进程通过 setuid 等方式获得新权限;

  • seccomp=builtin:明确启用 Docker 内置 seccomp;

  • –cap-drop=ALL:先删除 Linux capabilities;

  • PID、内存和 CPU 上限:避免一个失控进程拖垮整个开发环境。

这里没有设置 –network none。原因很现实:日常开发要下载 Go module、查文档、访问 Git 和测试服务。

所以这是一套“开发可用的基线”,不是处理完全不受信任代码的最高隔离级别。网络边界应该通过代理、域名允许列表或独立执行环境继续收紧。

mounts:只缓存依赖,不挂 Home

Go module 缓存放进独立 Docker volume,重建容器不用每次重新下载:

json“mounts“: [ “source=agent-sandbox-go-cache,target=/go/pkg/mod,type=volume” ]

我没有挂载 /.ssh、/.aws、~/.kube,也没有挂 /var/run/docker.sock。

Dev Container 支持把这些目录挂进去,但“支持”不代表“应该”。尤其 Docker Socket,拿到它通常就等于拿到控制宿主 Docker daemon 的能力,容器边界会被直接绕开。

第三步:理解生命周期命令

Dev Container 最容易写乱的是生命周期。

配置什么时候运行适合做什么Dockerfile RUN构建镜像时安装稳定的系统依赖和运行时onCreateCommand容器第一次创建最早期的项目初始化updateContentCommand创建时、代码内容更新时与仓库内容相关的准备工作postCreateCommand容器创建完成后安装项目依赖、跑一次初始化检查postStartCommand每次容器启动启动后台服务postAttachCommand每次工具连接很轻量的连接后操作

我的示例只使用:

json“postCreateCommand“: “go test ./… && .devcontainer/verify.sh”

因为测试和环境验证应该在容器创建后执行一次,但没必要每次打开终端都跑。

不要把长期稳定的 apt 安装塞进 postCreateCommand。那会让容器创建依赖当时的网络和软件源,也让每个人拿到的镜像基础不一致。能固定在 Dockerfile 的,就放回 Dockerfile。

第四步:启动 Dev Container

用 VS Code

安装 Dev Containers 扩展,打开项目后运行:

textDev Containers: Reopen in Container

修改 Dockerfile 或 devcontainer.json 后,运行:

textDev Containers: Rebuild Container

只重开窗口不会自动重建镜像,这是很常见的“我明明改了配置,为什么没生效”。

用官方 CLI

Dev Container 不一定要从编辑器启动。官方 CLI 包名是 @devcontainers/cli:

bashnpx -y @devcontainers/cli up –workspace-folder .

进入已经启动的环境执行命令:

bashnpx -y @devcontainers/cli exec
–workspace-folder .
sh -lc ‘id; go version; pwd’

我在 2026 年 7 月 16 日用 CLI 0.87.0 实际得到:

textuid=1000(vscode) gid=1000(vscode) go version go1.22.12 linux/arm64 /workspaces/ai/labs/agent-sandbox-demo

这三个输出分别确认:命令不是 root、Go 来自 Linux 容器、当前目录是容器内工作区。

第五步:不要只看配置,要验证最终环境

我在 .devcontainer/verify.sh 里放了几个断言:

sh#!/bin/sh set -eu

echo “user=(id -un) uid=(id -u) gid=(id -g)" echo "go=(go version)” echo “no_new_privs=(awk '/^NoNewPrivs:/ { print $2 }' /proc/self/status)" echo "effective_caps=(awk ‘/^CapEff:/ { print $2 }’ /proc/self/status)” echo “seccomp=$(awk ‘/^Seccomp:/ { print $2 }’ /proc/self/status)”

test “(id -u)" -ne 0 test "(awk ‘/^NoNewPrivs:/ { print $2 }’ /proc/self/status)” = “1” test “(awk '/^CapEff:/ { print $2 }' /proc/self/status)" = "0000000000000000" test "(awk ‘/^Seccomp:/ { print $2 }’ /proc/self/status)” = “2”

实际输出是:

textuser=vscode uid=1000 gid=1000 workspace=/workspaces/ai/labs/agent-sandbox-demo go=go version go1.22.12 linux/arm64 no_new_privs=1 effective_caps=0000000000000000 seccomp=2 workspace_write=allowed

这里 Seccomp: 2 代表过滤模式已经启用,CapEff 全 0 代表当前进程没有有效 capability。

工作区可写不是漏洞,而是这套开发环境的功能要求:开发者和 Coding Agent 都需要修改代码。真正该限制的是“只能写这个工作区和明确的缓存/输出目录”,而不是幻想一个完全只读的环境还能完成编程任务。

一个实测踩到的坑:配置会合并

我第一次启动时,明明写了 –cap-drop=ALL 和 no-new-privileges,最终 Docker 命令里却多出:

text–cap-add SYS_PTRACE –security-opt seccomp=unconfined

进入容器查看:

textNoNewPrivs: 1 Seccomp: 0

Seccomp: 0 说明过滤没有启用。

原因不是 Dev Container CLI 随机改配置,而是 Dev Container 规范允许把元数据写进镜像 label。官方 Go 开发镜像为了支持调试器,在镜像元数据里声明了 SYS_PTRACE 和 seccomp=unconfined;工具会把镜像元数据、Feature 和仓库里的 devcontainer.json 合并。

我最后在项目配置里显式加入:

json“securityOpt“: [ “no-new-privileges”, “seccomp=builtin” ]

重建后,最终 Docker 命令同时出现了 seccomp=unconfined 和后面的 seccomp=builtin,Docker 使用后者,容器内 Seccomp 变成 2。

SYS_PTRACE 还有一个更细的取舍:非 root 开发用户的 effective capabilities 是 0,但 capability bounding set 仍保留了 SYS_PTRACE 位。这方便调试器,也说明这不是“绝对最小权限”镜像。

如果你的环境不需要调试器,或者要运行真正不受信任的代码,应该改用自己控制、没有这类 Dev Container 元数据的基础镜像,并检查最终生成的 Docker 参数和 /proc/self/status,不要只审查表面的 JSON。

Coding Agent 怎样使用这套环境

关键不在于“仓库里有 .devcontainer”,而在于 Agent 的命令到底在哪里执行

有三种常见情况:

  • Agent 本身运行在 Dev Container 内:它调用的 Shell、Go、Git 都天然位于容器内;

  • Agent 运行在宿主机,但通过 devcontainer exec 执行项目命令:关键操作进入容器,宿主 Agent 本身仍有宿主权限;

  • Agent 只读取了 .devcontainer,实际命令仍在宿主机执行:这没有形成隔离。

所以至少要让 Agent 或自动化脚本先执行:

bashid uname -a cat /proc/1/cgroup pwd go version

确认用户、内核环境、cgroup、路径和工具链符合预期,再开始修改和测试。

对于宿主机上的 Agent,我更倾向把项目命令统一包装成:

bashnpx -y @devcontainers/cli exec
–workspace-folder .
go test ./…

这至少避免“编辑器在容器里,Agent 却悄悄调用宿主机 Go”这种双环境问题。

Dev Container 解决什么,不解决什么

它擅长解决:

  • 新人或新机器快速拿到一致工具链;

  • 把运行时、依赖和初始化步骤版本化;

  • 让编辑器、CLI、CI 与 Agent 尽量复用同一环境定义;

  • 把用户、挂载、端口和部分 Docker 运行参数放进仓库评审。

它不会自动解决:

  • 默认工作区通常可写,网络通常可用;

  • 仓库中的 initializeCommand 可能直接在宿主机运行;

  • 镜像元数据和 Features 可能追加权限或执行安装脚本;

  • Docker Socket、Home 目录和凭据一旦被挂载,容器就能接触它们;

  • 普通 Docker 容器仍共享宿主机内核,不等于虚拟机安全边界。

尤其是从陌生仓库拿到 .devcontainer 时,不要看到“容器”两个字就直接点确认。先审查 Dockerfile、Compose、Features、mounts、initializeCommand、privileged、capAdd、securityOpt 和 runArgs。

我的最终建议

如果目标是统一团队开发环境,Dev Container 很值得用。

如果目标是让 Coding Agent 少污染宿主机,它也是一个很实用的入口:工具链装在容器里,项目权限可以被显式描述,环境用完可以重建。

但如果目标是安全运行来源未知、可能主动攻击环境的代码,仅有 Dev Container 不够。至少继续考虑 Rootless Docker、严格网络出口、短期 Secret、独立工作副本;风险再高,就用 gVisor、Firecracker 或临时虚拟机。

一句话总结:

Dockerfile 固化“装什么”,devcontainer.json 固化“怎么开发”,验证脚本确认“最终到底给了什么权限”。

只有三层都可审查、可复现、可验证,Dev Container 才不只是“换台机器也能跑”,而是真正适合人和 Coding Agent 共用的开发环境。

完整示例:https://github.com/alexgiantwhale/examples/tree/main/01-agent-sandbox-demo/.devcontainer

你现在的开发环境是直接装在宿主机,还是已经容器化了?

相似文章

@thinkszyg: https://x.com/thinkszyg/status/2066837941477920993

X AI KOLs Timeline

一篇面向开发者(尤其是AI编码工具使用者)的实用指南,介绍如何安全高效地使用Claude Code、Codex等工具进行多Agent并行开发,重点包括任务拆解、文件隔离(worktree)、边界控制、顺序合并等最佳实践,避免文件冲突和混乱。