资讯动态

Onyx Craft Docker 沙箱本地开发指南:基于 docker-compose 与 sandbox-proxy 的调试迭代

发布时间:2026/9/10 10:50:34 来源:尧图企业网站定制
Onyx Craft Docker 沙箱本地开发指南基于 docker-compose 与 sandbox-proxy 的调试迭代【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer本文面向需要在本地对 Onyx Craft 的docker 沙箱后端SANDBOX_BACKENDdocker进行迭代开发的工程师。它完整讲解了两套可落地的开发配方Recipe A 用 docker-compose 一键拉起与自托管用户完全一致的完整 Craft 栈做端到端冒烟验证Recipe B 则把 sandbox-proxy 与 api_server 都跑到本地调试器里实现代理侧代码的断点级迭代。文中所有命令、环境变量与排查思路均基于当前仓库真实代码并补充了DockerSandboxManager、firewall-init.sh、compose overlay 的源码级原理读完你可以在不依赖 K8s 集群的前提下独立完成 Craft docker 链路的开发、验证与故障排查。适用场景何时应该走 docker 沙箱路径Onyx Craft 的沙箱Build 模式沙箱有两种后端实现kubernetes标准开发路径沙箱是真实 K8s Pod与 docker自托管 docker-compose 路径沙箱是 Docker 容器。仓库中 local-kubernetes.md 是日常 Craft 开发的首选但当你改动的是docker 专属链路时就必须回到本指南描述的 compose 侧backend/onyx/sandbox_proxy/目录下的代理服务egress proxy、action gate、凭据注入等backend/onyx/server/features/build/sandbox/docker/目录下的DockerSandboxManager及其dev_mode_serve、internal/exec_helpers等实现deployment/docker_compose/docker-compose.craft.yml这个 compose overlay 本身。也就是说当docker 管道本身就是你正在修改的对象时compose 侧迭代虽然比 K8s kind 路径慢却是最合适的工具。纯业务逻辑不依赖 docker 专属行为的部分仍然建议走 K8s 路径或直接用单元测试驱动详见 backend/tests/README.md。前置条件开始之前请确认以下三项就绪Docker Desktop 正在运行且至少分配了 8 CPU / 16 GB 内存。仓库通用开发前置已满足Python 3.13、uv、Node.js 22、项目 venv.venv以及.vscode/.env文件——具体见仓库根目录 CONTRIBUTING.md 的 Development Setup 章节。已构建onyxdotapp/sandbox:dev镜像docker build \ -t onyxdotapp/sandbox:dev \ backend/onyx/server/features/build/sandbox/image这个 sandbox 镜像在 K8s 与 compose 两种后端之间是共享的与make craft-sandbox-image见 Makefile为 kind 构建的是同一个 tag。镜像本身基于python:3.13-slim内嵌 iptables、ca-certificates、node/bun、GitHub CLI、opencode-serve 以及firewall-init.sh等沙箱运行时构建细节见 backend/onyx/server/features/build/sandbox/image/Dockerfile。一次性环境准备Craft compose overlay 引用了两个compose 外部资源即不属于任何 compose project 管理的资源本地开发前需要手动预创建docker network create onyx_craft_sandbox docker volume create sandbox_proxy_ca这两条命令创建的正是自托管用户在install.sh --include-craft时得到的同名资源。本地开发直接使用这些不带 project 前缀的裸名称是因为 DockerSandboxManager 在 compose project 之外以同名挂载它们——如果在 compose overlay 中声明为内部资源Docker 会为其加上project_前缀导致 api_server 以裸名挂载时找不到卷。可以在 docker-compose.craft.yml 的networks:与volumes:段看到这两个资源被声明为external: true并显式指定name:。两种开发配方Recipe A —— 全栈跑在 compose 里不挂本地调试器这是最接近自托管用户从install.sh --include-craft获得的形态适合对端到端行为做冒烟测试cd deployment/docker_compose SANDBOX_CONTAINER_IMAGEonyxdotapp/sandbox:dev \ docker compose \ -f docker-compose.yml \ -f docker-compose.craft.yml \ --env-file env.template \ up -d --waitdocker-compose.craft.yml在基础 docker-compose.yml 之上叠加了三个关键服务api_server、background、sandbox-proxy以及一个特殊服务sandbox-image-prepullapi_server / background通过挂载/var/run/docker.sock获得宿主 Docker Engine 控制权负责沙箱容器的 provision/terminate/execdepends_on中对sandbox-proxy使用condition: service_healthy而非service_started因为代理只在完成初始同步后才绑定监听端口service_started会与启动期的 provision 产生竞争。sandbox-proxyegress 代理服务以python -m onyx.sandbox_proxy.server启动对应 backend/onyx/sandbox_proxy/server.py监听 8080、healthz 端口 8081挂载sandbox_proxy_ca卷于/var/lib/sandbox-proxy/ca并以只读方式挂载 docker socket代理只需做事件监听与初始同步。sandbox-image-prepulldeploy.replicas: 0的镜像引用服务唯一用途是让docker compose pull在部署期就拉取约 1GB 的 sandbox 镜像避免第一个沙箱在 provision 请求内冷拉镜像entrypoint: [/bin/true]是纵深防御防止任何意外启动的容器以镜像默认 ENTRYPOINT无密码的opencode serve0.0.0.0:4096驻留运行。代理姿态proxy posture在SANDBOX_BACKENDdocker下是强制性的api_server 创建的每个沙箱都会执行firewall-init.shiptables 出网锁定 setprivcapability 收窄并将 HTTPS 流量路由到sandbox-proxy。DockerSandboxManager.__init__在 api_server 启动时就会强校验SANDBOX_PROXY_HOST非空否则直接抛RuntimeErrorif not SANDBOX_PROXY_HOST: raise RuntimeError( DockerSandboxManager requires SANDBOX_PROXY_HOST. The sandbox egress proxy is mandatory when craft is enabled; wire it in docker-compose.craft.yml or unset SANDBOX_BACKEND. )见 docker_sandbox_manager.py 的构造函数。想不带代理迭代的话请改用 K8s 配方SANDBOX_BACKENDkubernetes。compose 文件里两个 shell 变量默认值写法的差异也值得留意${SANDBOX_PROXY_HOST-sandbox-proxy}用的是单横杠形式它保留显式空串这一状态从而让上面的 fail-loud 检查能够触发——空串是必须报错的信号${SANDBOX_PROXY_PORT:-8080}用的是:-形式空值在这里只是笔误不是信号因此回退到默认值。该语义差异对应 configs.py 中SANDBOX_PROXY_HOST os.environ.get(SANDBOX_PROXY_HOST, )与SANDBOX_PROXY_PORT int(os.environ.get(SANDBOX_PROXY_PORT, 8080))的读取逻辑。跟踪代理日志docker compose -f docker-compose.yml -f docker-compose.craft.yml logs -f sandbox-proxy代理启动时会依次打印CA bootstrapped at ...、Informer initial sync complete.与Credential resolvers registered: ...等关键日志是判断代理是否就绪的第一手依据。Recipe B —— 调试器挂载在代理上迭代当你要改backend/onyx/sandbox_proxy/下的代码如gate.py、identity_docker.py、各 resolver时Recipe A 的全容器形态无法打断点需要用 Recipe B 把代理与 api_server 都拉到宿主机上跑。第 1 步只拉起基础设施。ods compose dev --infra ods envods compose dev --infra是tools/odsCLItools/ods/cmd/compose.go的子命令等价于以docker-compose.ymldocker-compose.dev.yml组合只启动db, cache, search, model servers等基础设施容器并激活s3-filestoreprofile而不启动 api_server / background 等业务服务。代理依赖 Postgres 与 Redis所以这两者是必需的。ods envtools/ods/cmd/env.go会查询正在运行的容器把解析后的真实主机端口映射写入.vscode/.env对已有文件做 upsert其余条目保持不动。这样任何在宿主机启动的本地进程都能通过.vscode/.env连上 compose 侧的基础设施。第 2 步在调试器下本地运行代理。在仓库根目录执行source .venv/bin/activate PYTHONPATH./backend \ SANDBOX_BACKENDdocker \ SANDBOX_PROXY_LISTEN_PORT8888 \ python -m onyx.sandbox_proxy.serverPYTHONPATH./backend是必需的onyx包位于backend/之下从仓库根目录直接运行且不设置 PYTHONPATH 会抛出ModuleNotFoundError。第 3 步的 api_server 同样适用。也可以在 VSCode 里添加一个指向backend/onyx/sandbox_proxy/server.py的 launch 配置携带相同的环境变量代理会读取.vscode/.env获取 Postgres Redis 主机。SANDBOX_PROXY_LISTEN_PORT8888这个覆盖对 Recipe B 是承重的代理默认监听 8080但第 3 步的 api_server 也要在宿主机绑定 8080因此必须把代理挪走。healthz 保持 8081 默认端口空闲不变。对应配置读取见 configs.py 的SANDBOX_PROXY_LISTEN_PORT与SANDBOX_PROXY_HEALTHZ_PORT。一个容易踩的坑FileCAStore会把 CA 写到/var/lib/sandbox-proxy/ca/。这个路径是硬编码的configs.py中的SANDBOX_PROXY_CA_VOLUME_PATH是常量不随环境变量变化因为 compose 的volumes:挂载目标才是唯一事实来源。因此本地代理需要对该目录有写权限——要么用你的 uid 预先创建sudo mkdir -p /var/lib/sandbox-proxy/ca sudo chown $USER /var/lib/sandbox-proxy/ca要么直接sudo运行代理。第 3 步本地运行 api_server把 docker 后端指向本地代理。PYTHONPATH./backend \ SANDBOX_BACKENDdocker \ SANDBOX_CONTAINER_IMAGEonyxdotapp/sandbox:dev \ SANDBOX_PROXY_HOSThost.docker.internal \ SANDBOX_PROXY_PORT8888 \ uvicorn onyx.main:app --host 0.0.0.0 --port 8080这里的链路是api_server 通过宿主机 docker socket provision 沙箱容器每个沙箱的firewall-init.sh把host.docker.internal解析成宿主机 IP 并钉进 iptables 白名单流量随后落到本地运行的代理上。Caveathost.docker.internal只在 Docker DesktopmacOS / Windows上开箱即用。Linux 上需要在沙箱容器上加--add-hosthost.docker.internal:host-gateway而这条路径当前尚未在仓库中打通——因此Linux 上做 Recipe B 更简单的方式是直接用 Recipe A。如果你要验证代理重启后沙箱 iptables 仍钉旧 IP之类的行为务必记住该限制。第 4 步照常通过 API provision 沙箱并触发一个受 gate 保护的动作例如 Slackchat.postMessage然后在gate.py、addons/gate.py、identity_docker.py等文件里设置断点。代理侧的运行骨架值得先读一遍 backend/onyx/sandbox_proxy/server.py它先用 mitmproxy 的DumpMaster构建 regular 模式代理注册OnyxPatResolver、MCPServerResolver、ExternalAppResolver三个凭据解析器first-claim-wins 顺序再挂载GateAddon负责动作审批启动时/healthz在 CA 就绪与身份表初始同步完成前一直返回 503而不是连接拒绝对应 compose 健康检查的start_period: 60s。沙箱内的冒烟检查命令在刚 provision 好的沙箱容器内部执行docker exec -it sandbox-id8 bash容器名sandbox-id8取自 sandbox ID 的前 8 位见_sandbox_container_name。依次验证出网锁定与代理链路# 经代理出网成功叶子证书由代理 CA 签发 curl -v https://example.com 21 | grep -E (Issuer|HTTP/) # 绕过尝试被 iptables 拦截 curl --noproxy * --max-time 5 https://example.com # DNS 已关闭 nslookup example.com # IPv6 被丢弃 curl -6 --max-time 5 https://example.com # 验证 agent 以零 capability 运行 getpcaps $$这些命令背后的安全语义由 firewall-init.sh 保证它作为沙箱的 entrypoint 以entrypoint模式执行以下步骤安装代理 CA把/sandbox-ca/ca.crt来自sandbox_proxy_ca卷的只读挂载装入系统信任库并生成ca-bundle.crtiptables 出网锁定iptables -F OUTPUT; iptables -P OUTPUT DROP仅放行lo、conntrack 已建立连接与发往代理 IP:端口 的流量其余一律REJECTIPv6 同样强制OUTPUT DROP部分锁定即安全回退自校验通过检查链规则而非网络探测网络探测无法区分锁定生效与没有网——fail-open 是危险的确认默认策略为 DROP、代理放行规则存在setpriv --bounding-set-all --reuid1000 --regid1000降权执行真正的/workspace/entrypoint.sh。注意第 2 步中host.docker.internal的解析使用getent ahostsv4而非hosts只取 AF_INET 答案——iptables 规则是 IPv4 专属的双栈解析若先返回 AAAA 会让iptables -d ipv6直接报 host/network not found 而中断初始化。对应地docker_sandbox_manager.py 的build_container_create_kwargs在代理姿态下会覆写镜像 ENTRYPOINT 为/workspace/firewall-init.shcommand改为/workspace/entrypoint.sh否则 Docker 会把镜像自带 entrypoint 前置到 command 前静默绕过初始化以user0:0启动以便 iptables 生效同时cap_add[NET_ADMIN,SETPCAP,SETUID,SETGID,CHOWN]——这五个 capability 在 agent execve 前全部退出 bounding set最终运行中的容器零 capabilityrootNET_ADMIN 窗口被set -euo pipefail约束在 init 的秒级运行期内任何一步失败都会非零退出把ONYX_PAT替换为占位符replaced_by_egress_proxy见SANDBOX_PROXY_INJECTED_PLACEHOLDER真实值由代理在链路上从 Postgres 读取注入沙箱永远看不到裸凭据注入HTTPS_PROXY/HTTP_PROXY以及NODE_EXTRA_CA_CERTS、REQUESTS_CA_BUNDLE、SSL_CERT_FILE、CURL_CA_BUNDLE、GIT_SSL_CAINFO等 SDK 级 CA 变量覆盖绕过/etc/ssl/certs的库只加入专用桥接网络onyx_craft_sandbox绝不加入 compose 默认网络因此 postgres、redis、minio 等默认网络服务名在沙箱内不可达唯一受支持的 API 端点是桥上的onyx-craft-api别名。清理Teardowncd deployment/docker_compose docker compose -f docker-compose.yml -f docker-compose.craft.yml down # 可选清除代理 CA下次启动时强制重新生成 docker volume rm sandbox_proxy_ca # 可选清除沙箱状态 docker volume ls --filter nameonyx-craft-sandbox- -q | xargs -r docker volume rm沙箱卷的命名前缀onyx-craft-sandbox-对应SANDBOX_DOCKER_VOLUME_PREFIX默认值configs.py每个沙箱一个命名卷、挂载到/workspace/sessions一个用户一个容器、多会话共存的模型与 K8s Pod 模型保持一致。需要单独终止某个沙箱时可以走DockerSandboxManager.terminate的路径强删容器 强删命名卷不必等到 idle-cleanup 任务对应background服务中SANDBOX_IDLE_TIMEOUT_SECONDS默认 3600 秒兜底。常见问题与排查firewall-init.sh: FATAL: CA source /sandbox-ca/ca.crt not present代理还没完成 CA 引导。等待sandbox-proxy打印persisted proxy CA cert...Recipe A或本地代理打印同样的日志Recipe B然后重新 provision 沙箱。firewall-init.sh: FATAL: could not resolve proxy host sandbox-proxy沙箱容器解析不到代理名。用docker inspect sandbox-id8确认沙箱在onyx_craft_sandbox网络上且代理也在同一网络并处于运行状态——沙箱内对sandbox-proxy的解析依赖 Docker 内嵌 DNS 与同一桥接网络。所有出网请求返回 403unidentified_sandboxDockerEventsLookup没有看到沙箱容器的标签。用docker inspect sandbox-id8 | grep onyx.app核对标签若缺失说明 manager 运行时没有带SANDBOX_BACKENDdocker。容器标签由build_sandbox_labels生成包含onyx.app/componentcraft-sandbox、onyx.app/sandbox-id、onyx.app/tenant-id、onyx.app/user-id等代理正是靠这些标签把出口流量归属到具体沙箱从而决定是否放行与是否触发审批。docker volume inspect: No such volume: sandbox_proxy_ca跳过了预创建步骤。执行docker volume create sandbox_proxy_ca即可。代理重启后沙箱容器启动报 capability 错误代理在桥接网络重启后拿到了新 IP但沙箱的 iptables 规则仍钉着旧 IP。重新 provision 沙箱即可让firewall-init.sh重新解析并更新规则。总结docker 沙箱链路的完整心智模型把整个 docker 沙箱链路串起来看SANDBOX_BACKENDdocker之下其实是一条控制面与数据面分离的架构api_server与 background worker通过宿主 docker socket 驱动沙箱容器生命周期这是控制面沙箱内 agent 的一切外部 HTTPS 流量强制经过 sandbox-proxy这是数据面同时承担 MITM 解密、凭据注入与动作审批三重职责。firewall-init.sh用 iptables setpriv 把沙箱的网络与权限钉死让代理成为唯一出口compose overlay 则通过external: true的网络与卷、service_healthy依赖和replicas: 0的镜像预热服务把这个模型完整落地到单机自托管场景。本文的两条配方覆盖了这条链路的两类开发需求Recipe A 用最小命令复现自托管用户的完整环境做端到端验证Recipe B 把代理和 api_server 移到调试器下让你能在gate.py、identity_docker.py等关键路径上打断点。相关源码、配置与测试均可按文中给出的仓库相对路径继续深入阅读。【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

免费获取报价