Skip to content
Zegging's Tech Blog
Go back

在 Ubuntu 裸机上部署一个 self-host 的 E2B 服务

起因

我想在一台 Ubuntu 机器上跑起 E2B 的整套虚拟化服务。主要其实是服务于内部研发的场景:coding agent server、sandbox 执行、数据分析容器(类似于一个远程 Jupyter Kernel)。未来当然是有可能开放到业务服务中的,比如说在 Agent 中执行用户的脚本之类的。

为啥要自己做呢,因为运维和基础中间件不给做。问我要业务场景,我有个锤子的业务场景需要?现在还有方案试图通过 K8S 给每个用户起一个 Pod 来处理 Agent 需要用户隔离的场景呢,要不然就是通过目录(也是一绝)。这种东西用途太广泛了,并且已经是业界的成熟场景解决方案,不太明白为什么还需要一个“业务需求”才能推动。牢骚满腹不如自己开干!

和同事在他的台式机上最终跑通了这个 self-host 链路:

Windows 本地 -> 10.10.21.21:3000 -> E2B API -> orchestrator -> Firecracker microVM

最后我在自己的 Windows 开发机里成功创建了 sandbox:

{
  "alias": "base",
  "clientID": "6532622b",
  "envdVersion": "0.6.6",
  "sandboxID": "ixwcnaf4mnrytcuys54lf",
  "templateID": "gg1xl4lhgm0cht8rjcn9"
}

这篇记录按真实排查时间线整理,重点放在部署过程中的误判、和比较容易发生的坑。

先确认环境

E2B 的核心不是普通 Docker 服务,而是基于 Firecracker/KVM 的 microVM sandbox。因此第一步不是安装软件,而是确认宿主机是否真的具备虚拟化能力。Ubuntu 上先跑了这些检查:

uname -a
lsb_release -a || cat /etc/os-release

egrep -c '(vmx|svm)' /proc/cpuinfo
lscpu | egrep 'Model name|Virtualization|Hypervisor|CPU\(s\)'

ls -l /dev/kvm /dev/net/tun
kvm-ok 2>/dev/null || true
mount | grep cgroup
stat -fc %T /sys/fs/cgroup

free -h
df -h
lsmod | egrep 'kvm|vhost|tun'

docker --version
docker buildx version

这台机器的条件比较理想:

Ubuntu 26.04 LTS
Intel i7-10700
VT-x enabled
/dev/kvm exists
/dev/net/tun exists
cgroup2
30Gi memory
817G free disk
Docker 29.5.2

这一步很重要。 /dev/kvm 不存在的话宿主机本身就不具备跑 Firecracker 后端的条件。

Docker Hub 的镜像坑

Docker 本身装好了,但 hello-world 拉不下来:

failed to fetch anonymous token:
Get "https://auth.docker.io/token?...": i/o timeout

我分别测了几个地址:

curl -I --connect-timeout 10 https://registry-1.docker.io/v2/
curl -I --connect-timeout 10 https://auth.docker.io/token
curl -I --connect-timeout 10 https://github.com

现象是:

  • registry-1.docker.io 可访问
  • github.com 可访问
  • auth.docker.io 超时

也就是说,Docker Hub 的 registry 本身可达,但拿 token 的服务不可达。Docker 拉镜像必须先走 token 授权,所以匿名拉取全部失败。

我试了网上一些 Docker mirror,很多已经返回 410 Gone 或者不是标准 registry 响应。最后实测 dockerproxy.net 可用,配置 Docker mirror:

sudo mkdir -p /etc/docker

sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
  "registry-mirrors": [
    "https://dockerproxy.net"
  ]
}
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

然后重新验证:

docker pull hello-world
docker run --rm hello-world

同时我把 IPv4 forwarding 打开,后面 sandbox 网络会用到:

sudo sysctl -w net.ipv4.ip_forward=1

sudo tee /etc/sysctl.d/99-e2b.conf >/dev/null <<'EOF'
net.ipv4.ip_forward=1
EOF

安装基础工具链

E2B infra 文档要求的基础工具包括:

  • Terraform 1.7.5
  • Packer
  • Go
  • Docker Buildx
  • Node/NPM
  • gsutil

Terraform 和 Packer 按指定版本安装:

mkdir -p ~/bin ~/src
cd /tmp

curl -fsSLO https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_linux_amd64.zip
unzip -o terraform_1.7.5_linux_amd64.zip
install -m 0755 terraform ~/bin/terraform

curl -fsSLO https://releases.hashicorp.com/packer/1.11.2/packer_1.11.2_linux_amd64.zip
unzip -o packer_1.11.2_linux_amd64.zip
install -m 0755 packer ~/bin/packer

grep -qxF 'export PATH="$HOME/bin:$PATH"' ~/.zshrc || echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc
export PATH="$HOME/bin:$PATH"

terraform version
packer version

基础依赖:

sudo apt update
sudo apt install -y \
  golang-go nodejs npm git jq make build-essential pkg-config \
  qemu-utils bridge-utils iptables nftables \
  postgresql-client redis-tools unzip curl wget

E2B 官方 self-host 和单机 Ubuntu 的差异

拉取仓库:

cd ~/src
git clone https://github.com/e2b-dev/infra.git e2b-infra
cd e2b-infra

主要的部署依赖、脚本、产物都在 Makefile 和 IaC 目录里,仓库主要提供的是:

iac/provider-aws
iac/provider-gcp

没有现成的 provider-linux。这意味着:完整 self-host 生产路径主要是 AWS/GCP。单台 Ubuntu 不是官方的一键生产部署路径。不过仓库里有基本的部署依赖和信息,可以自己手动搞:

DEV-LOCAL.md
packages/local-dev
  • Docker Compose 跑 Postgres、Redis、ClickHouse、Grafana、Loki 等
  • 本机 root 跑 orchestrator
  • Firecracker 直接使用 /dev/kvm
  • API 和 client proxy 本机启动

这不是官方生产部署的替代品,但很适合做自托管实验、agent sandbox 原型和内部 PoC。

Go toolchain 自动下载也会卡

进入 e2b-infra 后执行 go version,Go 会自动下载仓库要求的 Go 1.26.3:

go: downloading go1.26.3
Get "https://proxy.golang.org/...": i/o timeout

我一开始在仓库目录里执行:

go env -w GOPROXY=https://goproxy.cn,direct

结果也会卡住。排查了一下,发现原因是 Go 先读取当前仓库的 go.work/go.mod,发现需要新 toolchain,于是还没来得及写代理配置,就先去下载 toolchain 了。

解决方式是跳出仓库,并临时禁用自动 toolchain:

cd ~

GOTOOLCHAIN=local go env -w GOPROXY=https://goproxy.cn,direct
GOTOOLCHAIN=local go env -w GOSUMDB=sum.golang.google.cn
GOTOOLCHAIN=local go env -w GOTOOLCHAIN=auto

GOTOOLCHAIN=local go env GOPROXY GOSUMDB GOTOOLCHAIN GOVERSION

再回仓库:

cd ~/src/e2b-infra
go version

准备 Firecracker 本机环境

根据 DEV-LOCAL.md,本机需要 NBD 和 hugepages:

cd ~/src/e2b-infra

sudo modprobe nbd nbds_max=64
lsmod | grep nbd

sudo sysctl -w vm.nr_hugepages=2048
grep HugePages /proc/meminfo

echo 'nbd nbds_max=64' | sudo tee /etc/modules-load.d/nbd.conf

sudo tee /etc/sysctl.d/99-e2b-local.conf >/dev/null <<'EOF'
vm.nr_hugepages=2048
net.ipv4.ip_forward=1
EOF

sudo sysctl --system | egrep 'hugepages|ip_forward'

后面我也补了 userfaultfd:

echo 1 | sudo tee /proc/sys/vm/unprivileged_userfaultfd

sudo tee /etc/sysctl.d/98-e2b-userfaultfd.conf >/dev/null <<'EOF'
vm.unprivileged_userfaultfd=1
EOF

下载 E2B 预构建内核和 Firecracker

安装 google-cloud-cli / gsutil 后,下载 E2B 的公开构建产物:

cd ~/src/e2b-infra

make download-public-kernels
make download-public-firecrackers

ls -lah packages/fc-kernels/ | head
find packages/fc-versions/builds -maxdepth 4 -name firecracker -type f -exec ls -lah {} \; | head

能看到类似:

packages/fc-kernels/vmlinux-6.1.158
packages/fc-versions/builds/v1.14.1.../amd64/firecracker

这一步如果缺失,后面 orchestrator 就没有可用的 kernel/firecracker 二进制。

启动 local infra 时继续被镜像拉取绊住

启动 local infra:

cd ~/src/e2b-infra
make local-infra

第一次失败在镜像拉取:

postgres:17.4
python:3.12-alpine
error from registry
dockerproxy.net ... 500 Internal Server Error

我先提取 Docker Compose 需要的镜像:

docker compose -f packages/local-dev/docker-compose.yaml config --images | sort -u

再逐个从可用 mirror 拉取并 tag 回原名。最终 local infra 成功启动:

docker compose -f packages/local-dev/docker-compose.yaml ps

主要服务包括:

postgres:17.4
redis:7.4.2
clickhouse:25.4.5.24
grafana/grafana:12.0.0
grafana/loki:3.4.1
grafana/tempo:2.8.2
otel/opentelemetry-collector-contrib

随后执行迁移和 seed:

make -C packages/db migrate-local
make -C packages/clickhouse migrate-local
make -C packages/envd build
make -C packages/local-dev seed-database

busybox 缺失会被包装成 internal error

构建 base template 时,最初只看到一个很笼统的错误:

BuildError: An internal error occurred.

真正原因在 orchestrator 日志里:

error reading busybox file
packages/orchestrator/.busybox/1.36.1/amd64/busybox:
no such file or directory

这里需要补一下 busybox 这个依赖:

cd ~/src/e2b-infra
make -C packages/orchestrator fetch-busybox BUILD_ARCH=amd64

如果 target 不可用,也可以手动下载:

mkdir -p packages/orchestrator/.busybox/1.36.1/amd64

curl -fL --connect-timeout 20 \
  -o packages/orchestrator/.busybox/1.36.1/amd64/busybox \
  https://github.com/e2b-dev/fc-busybox/releases/download/v1.36.1/busybox_v1.36.1_amd64

chmod +x packages/orchestrator/.busybox/1.36.1/amd64/busybox

SDK/脚本侧的错误太抽象和笼统了,必须看 orchestrator 的日志。

启动三大前台服务

local infra 启动后,还需要分别启动三个前台服务。

终端 A:orchestrator

cd ~/src/e2b-infra
sudo env "PATH=$PATH" make -C packages/orchestrator run-local 2>&1 | tee /tmp/e2b-orchestrator.log

健康检查:

curl -s http://localhost:5008/health && echo

终端 B:API

cd ~/src/e2b-infra
make -C packages/api run-local 2>&1 | tee /tmp/e2b-api.log

健康检查:

curl -s http://localhost:3000/health && echo

终端 C:client proxy

cd ~/src/e2b-infra
make -C packages/client-proxy run-local 2>&1 | tee /tmp/e2b-client-proxy.log

健康检查:

curl -s http://localhost:3003/health && echo

旧 orchestrator 进程占端口

调试时多次重启 orchestrator,容易遇到端口占用:

listen tcp :5007: bind: address already in use
listen tcp :5012: bind: address already in use
listen tcp 0.0.0.0:5016: bind: address already in use

先查占用:

sudo ss -lntp | egrep ':(5007|5008|5012|5016|5017|5018)\b'

找到占用这些端口的 orchestrator 进程后,清理旧进程:

sudo kill -9 <orchestrator_pid>

也可以自动提取并清理:

PIDS=$(sudo ss -H -lntp \
  | grep -E ':(5007|5008|5012|5016|5017|5018)[[:space:]]' \
  | sed -n 's/.*pid=\([0-9]\+\).*/\1/p' \
  | sort -u)

for p in $PIDS; do
  sudo kill -9 "$p"
done

最后确认端口清空:

sudo ss -lntp | egrep ':(5007|5008|5012|5016|5017|5018)\b' || true

base template 第一次构建非常慢

执行:

cd ~/src/e2b-infra
make -C packages/shared/scripts local-build-base-template

过程中会看到:

FROM e2bdev/base
Base Docker image size: 468 MB
Setting up system files
Creating file system and pulling Docker image
Uncompressing layer ...
mke2fs 1.47.2
Creating filesystem with 6400000 4k blocks

这里创建的是一个大约 24 GiB 的 ext4 rootfs:

6400000 * 4 KiB ~= 24.4 GiB

这一步非常容易误判为卡死。我这次第一次构建最终耗时:

Build finished, took 40m26s

判断它是不是还活着,可以看进程的 CPU 使用等信息。这一步成功之后,后面重启服务不需要反复构建 base template。

创建 sandbox 成功,但默认只活 15 秒

第一次创建:

curl -s -X POST http://localhost:3000/sandboxes \
  -H "X-API-Key: <LOCAL_DEV_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"templateID": "base"}' | jq

返回成功:

{
  "alias": "base",
  "clientID": "6532622b",
  "envdVersion": "0.6.6",
  "sandboxID": "iuumrzrekf7n0vg9afhll",
  "templateID": "gg1xl4lhgm0cht8rjcn9"
}

但随后查询发现 sandbox 已经不存在。我一开始以为是 Firecracker 启动失败,后来查日志发现不是。

orchestrator 显示 sandbox 已经成功加入 map:

adding sandbox to map
sandbox.ip: 10.11.0.67
sandbox.kernel.version: vmlinux-6.1.158
sandbox.firecracker.version: v1.14.1...

实际上在 API 日志里就有具体原因,sandbox 的有效生命周期就 15s,到点后正常 kill 掉了。

start_time: 2026-06-23T17:12:56
end_time:   2026-06-23T17:13:11

orchestrator 进程的日志有这些关键信息:

Removing sandbox
state_action: kill

解决方式是创建时传 timeout

curl -s -X POST http://localhost:3000/sandboxes \
  -H "X-API-Key: <LOCAL_DEV_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"templateID":"base","timeout":300}' | jq

Python SDK 调用方式

本地或服务器上都可以这样用:

from e2b import Sandbox

sbx = Sandbox(
    api_key="<LOCAL_DEV_API_KEY>",
    api_url="http://10.10.21.21:3000",
    sandbox_url="http://10.10.21.21:3002",
    template="base",
    timeout=3600,
)

print(sbx.id)

注意:

  • api_url3000
  • sandbox_url3002
  • 3003 是 client-proxy health
  • 创建时要显式传 timeout

重启后的启动顺序

机器重启后,不需要重新构建 base template。启动顺序是:

1. Docker local infra

cd ~/src/e2b-infra
docker compose -f packages/local-dev/docker-compose.yaml up -d
docker compose -f packages/local-dev/docker-compose.yaml ps

2. orchestrator

cd ~/src/e2b-infra
sudo env "PATH=$PATH" make -C packages/orchestrator run-local 2>&1 | tee /tmp/e2b-orchestrator.log

3. API

cd ~/src/e2b-infra
make -C packages/api run-local 2>&1 | tee /tmp/e2b-api.log

4. client proxy

cd ~/src/e2b-infra
make -C packages/client-proxy run-local 2>&1 | tee /tmp/e2b-client-proxy.log

可以通过一个脚本一键拉起。

Linux 单机 provider(开发中

E2B 官方的 infra 仓库给出的 provider 主要是面向云服务厂商的,并没有真的 self-host Linux 集群 provider。我把上述的操作整合成了一个 E2B Linux 单机 provider repo,有兴趣的朋友可以试试看,有 bug 欢迎提 issue 我来修,希望最后可以升级成一个 K8S provider。

总结

迷迷茫茫整了一通,感谢我 codex 爹最终部署好了。下次让 Pi ssh 上去自己搞得了……


Share this post on:

Previous Post
新的潮流“创建 Loop 来运行 Agent”,Loop 反而是最不重要的那一环
Next Post
作为研发直接负责具体业务的感受:SEO 业务与产品的价值