问题背景:为什么放弃 Kubernetes
在开发沙箱场景中,Kubernetes 带来了过度复杂的问题。对于中小团队或个人开发者而言,K8s 的学习成本、运维负担和资源开销往往得不偿失。一个典型的开发沙箱需求其实很简单:代码推送后自动构建容器,并生成可访问的预览链接。
无 K8s 方案的核心优势在于简化架构:无需维护复杂的控制平面,不需要 etcd、kubelet 等组件,单台服务器即可承载多个隔离的开发环境。这种轻量级方案特别适合以下场景:
- Coding Agents:AI 编码助手需要快速创建临时环境验证代码
- PR 预览:每个 Pull Request 自动生成独立预览实例
- 内部演示:快速搭建产品演示环境供团队评审
核心架构:Docker API + Go 服务
无 K8s 沙箱的架构可以拆解为三个核心组件:Git 接收层、镜像构建层、容器调度层。
Git 接收层
使用 Go 编写的 HTTP 服务接收 Git webhook(GitHub/GitLab/Gitea 均支持)。关键设计点是异步处理:收到 push 事件后立即返回 200 状态码,将构建任务丢入队列,避免 Webhook 超时。
// 简化的webhook处理器结构
type WebhookHandler struct {
buildQueue chan BuildTask
dockerCli *client.Client
}
func (h *WebhookHandler) HandlePush(w http.ResponseWriter, r *http.Request) {
task := extractBuildTask(r)
h.buildQueue <- task // 非阻塞入队
w.WriteHeader(http.StatusOK)
}
镜像构建层
直接调用 Docker Build API,而非依赖 docker build 命令。这样做的好处是可以精细控制构建参数,并实时获取构建日志流式输出到前端。
构建上下文的管理策略:为每个项目创建独立目录,clone 代码后作为 build context。为避免磁盘爆炸,需要设置自动清理策略 —— 保留最近 5 个版本的构建缓存,旧版本立即删除。
容器调度层
这是替代 K8s 的核心。直接使用 Docker Engine API 管理容器生命周期:
- 端口分配:维护一个可用端口池(如 10000-20000),每个沙箱分配一个独占端口
- 资源限制:通过 Docker API 设置 CPU 和内存上限,防止单个沙箱耗尽主机资源
- 网络隔离:每个沙箱使用独立 Docker 网络,避免端口冲突和未授权访问
预览 URL 路由方案
预览 URL 的实现有两种主流方案,各有适用场景。
方案一:路径前缀路由(推荐)
使用反向代理(Caddy 或 Traefik)将请求按路径前缀路由到对应容器。
https://preview.example.com/app-abc123/ -> localhost:10001
https://preview.example.com/app-def456/ -> localhost:10002
Go 服务需要维护一张路由表,记录分支名 / Commit SHA 与端口的映射关系。Caddy 配置可以动态生成,通过 API 热重载,无需重启代理服务。
优势:单域名即可支持无限沙箱,SSL 证书管理简单。
方案二:子域名路由
为每个沙箱分配独立子域名:
abc123.preview.example.com -> localhost:10001
def456.preview.example.com -> localhost:10002
需要通配符 SSL 证书(*.preview.example.com)和 DNS 通配符解析。Go 服务监听 Docker 事件,容器启动时自动注册 DNS 记录,停止时清理。
优势:每个沙箱拥有独立 Origin,避免 Cookie 冲突和 CORS 问题。
Git 触发与构建流程详解
完整的自动化流程包含以下步骤:
- Webhook 接收:验证请求签名(GitHub 使用 X-Hub-Signature-256),防止伪造
- 任务入队:将构建任务序列化后存入 Redis/RabbitMQ,支持多 Worker 并行
- 代码拉取:使用 go-git 库或执行 git clone,检出指定分支
- 镜像构建:调用 Docker Build API,传入构建上下文和标签参数
- 容器启动:基于新镜像创建容器,绑定到预分配的端口
- 路由注册:更新反向代理配置,使预览 URL 生效
- 状态通知:将构建结果和预览链接回写到 Git commit 状态或 Slack / 钉钉
关键容错点:
- 构建失败时保留上次成功的容器运行,避免服务中断
- 设置构建超时(建议 10 分钟),防止死锁占用 Worker
- 容器健康检查失败自动重启,最多重试 3 次
可落地参数与配置清单
资源配额建议
| 组件 | CPU 限制 | 内存限制 | 适用场景 |
|---|---|---|---|
| 前端沙箱 | 0.5 核 | 512MB | React/Vue 静态站点 |
| API 服务 | 1 核 | 1GB | Node.js/Python 后端 |
| 全栈应用 | 2 核 | 2GB | Next.js/Nuxt 等 |
| 数据库 | 1 核 | 2GB | PostgreSQL/MySQL |
端口分配策略
- 基础端口范围:10000-20000
- 每个沙箱占用 1 个主端口(应用)+ 可选调试端口
- 端口分配算法:从池中取最小可用值,释放后回收
存储清理策略
- 镜像保留:每个项目保留最近 5 个版本的镜像
- 容器日志:保留 7 天,自动轮转
- 构建缓存:每日凌晨清理超过 3 天的缓存
安全配置要点
- 禁用容器的特权模式(--privileged=false)
- 挂载只读文件系统(--read-only),临时数据写入 tmpfs
- 禁止容器访问 Docker Socket,防止逃逸
- 网络层面:沙箱容器只能访问外部网络,禁止访问宿主机其他端口
风险与限制
单点故障:无 K8s 意味着缺少自动故障转移。建议方案:使用 systemd 管理 Go 服务,配置自动重启;数据库使用 SQLite 或单节点 PostgreSQL,定期备份。
扩展瓶颈:单台 Docker 主机有容器数量上限(约 1000 个)。如需扩展,可采用多主机 + Docker Swarm 模式,仍比 K8s 轻量。
资源争抢:缺乏 K8s 的精细调度,可能出现资源热点。建议设置硬性的 CPU / 内存配额,并监控宿主机负载,超过 80% 时暂停新沙箱创建。
总结
无 K8s 的自托管开发沙箱通过 Docker API 与 Go 服务的组合,在保持轻量化的同时实现了 Git 推送自动构建和预览 URL 生成的核心能力。这种架构适合资源有限的团队快速落地,单台 4 核 8G 服务器即可支撑 20-30 个并发沙箱。
关键成功因素在于路由设计的简洁性和资源配额的严格控制。路径前缀路由方案在运维复杂度上优于子域名方案,推荐作为起步选择。随着规模增长,可以逐步引入 Docker Swarm 或迁移到 K8s,但初期保持简单是更务实的策略。
资料来源:
- tastyeffectco/sandboxes - Self-hosted dev sandboxes with preview URLs. One command. No Kubernetes.
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。