SSD Nodes Learn 🎉 VPS từ $4.99/tháng
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-07

Cách tự host sandboxd trên VPS với Docker

Tự chạy sandboxd trên VPS với Docker: cài đúng version, thêm model key, cấu hình HTTPS preview, kiểm tra RAM và disk, rồi dọn sandbox cũ.

sandboxd là gì và bạn nhận được gì khi tự chạy nó

Để tự host sandboxd, bạn cần một máy chủ Linux có Docker và một domain. Bạn gửi một prompt, coding agent sẽ xây dựng một ứng dụng thực bên trong một container cô lập, rồi ứng dụng đó chạy tại URL preview riêng. Các công cụ xây dựng ứng dụng từ prompt là nhóm dịch vụ hosted nổi bật nhất trong 2026. sandboxd là công cụ chạy trên VPS của bạn, theo giấy phép MIT, với mã nguồn được tạo lưu ngay trên disk của bạn.

Thiết kế được cố ý giữ nhỏ gọn. Control plane viết bằng Go điều khiển Docker, Traefik v3 định tuyến mọi hostname preview, SQLite lưu state, và mỗi ứng dụng chạy trong một container. Không có Kubernetes và cũng không cần database server riêng. Vì vậy, một máy có 2 vCPU vẫn có thể chạy được hệ thống này.

Toàn bộ mô hình dựa trên 4 object. app là project lâu dài, chứa tên, metadata của git và secret. sandbox là Docker container chạy app đó, và mỗi app tại một thời điểm chỉ trỏ đến một sandbox. workspace là các file của app. Các file này nằm trên host và vẫn tồn tại sau khi container bị dừng hoặc xóa. task là một prompt được giao cho agent bên trong sandbox. Dừng sandbox sẽ giải phóng memory nhưng vẫn giữ các file. Xóa sandbox sẽ loại bỏ container, sau đó app có thể khởi động một container mới.

sandboxd khác Dify và OpenHands như thế nào?

Ba công cụ này thường bị nhầm lẫn vì đều chạy LLM (large language model) trên server của bạn, nhưng sản phẩm đầu ra khác nhau. Dify xây dựng các ứng dụng LLM: giao diện chat, pipeline truy xuất dữ liệu và workflow gọi model mỗi khi có người sử dụng. Model là một phần của sản phẩm hoàn chỉnh. OpenHands làm việc trên repository bạn đã có: bạn trỏ công cụ vào code của mình, rồi nó đọc file, chạy command và đề xuất thay đổi. sandboxd bắt đầu từ con số 0. Nó dựng khung project từ một preset, build project trong container mới và cung cấp cho bạn một URL để xem. Sản phẩm đầu ra là một ứng dụng React hoặc FastAPI thông thường, không cần model để chạy.

Vì vậy, hãy chọn dựa trên thứ bạn muốn có ở cuối quy trình. sandboxd phù hợp khi bạn muốn bắt đầu từ một câu mô tả rồi giữ lại code sau đó. Hai công cụ còn lại phù hợp khi repository hoặc sản phẩm dùng model đã tồn tại.

Điểm khác biệt còn lại là tuổi đời. Đây là yếu tố cần cân nhắc trước khi xây dựng bất cứ thứ gì thực tế nào dựa trên công cụ này.

ChartGitHub stars and forks, read from the GitHub API on 4 August 2026
The data behind this chart
[
  {
    "tool": "sandboxd",
    "github_stars": "875",
    "forks": "50"
  },
  {
    "tool": "OpenHands",
    "github_stars": "83,091",
    "forks": "10,711"
  },
  {
    "tool": "Dify",
    "github_stars": "151,320",
    "forks": "23,886"
  }
]

sandboxd có 875 star, so với 83,091 của OpenHands và 151,320 của Dify. Repository được tạo vào ngày 3 tháng 6 năm 2026, nên tính đến tháng 8 năm 2026, nó mới được 2 tháng, trong khi OpenHands ra mắt từ tháng 3 năm 2024 và Dify từ tháng 4 năm 2023. Bản release v0.1.0 được phát hành vào ngày 6 tháng 6 năm 2026 và v0.3.6 vào ngày 1 tháng 8 năm 2026. Project tự gọi là beta và cho biết các bản release 0.x có thể làm hỏng tính tương thích. Hãy xem những con số này là rủi ro về dependency, không phải kết luận về chất lượng: một project mới 2 tháng chỉ mới có 2 tháng để người khác phát hiện bug.

Máy chủ cần gì và điều gì sẽ xảy ra khi thiếu tài nguyên

Dự án cho biết 2 vCPU và 4 GB RAM là đủ để bắt đầu. Mức này phù hợp với control plane và một sandbox nhỏ, nhưng không đủ cho hai người build cùng lúc. Hãy chia ngân sách bộ nhớ theo từng phần. Traefik và control plane viết bằng Go dùng ít tài nguyên. Mỗi sandbox đang chạy chứa đầy đủ toolchain Node hoặc Python, và mức sử dụng cao nhất thường xuất hiện trong một npm install rồi đến production build. Hãy dự trù 8 GB cho máy chủ chạy ổn định vài app, đồng thời chỉ xem swap là lớp dự phòng chứ không phải dung lượng thực tế, vì một build phải dùng swap sẽ mất vài phút thay vì vài giây.

Khi hết bộ nhớ, bạn sẽ gặp hai loại lỗi khác nhau và biểu hiện của chúng hoàn toàn không giống nhau. Bên trong sandbox, container chạm ngưỡng --memory mà sandboxd đặt ra, rồi kernel kill process lớn nhất. Vì vậy build dừng mà agent không đưa ra thông báo hữu ích. docker ps -a hiển thị exit code 137 cho container đó, còn docker inspect trên container này báo "OOMKilled": true. Một Node build bị dừng theo cách này thường in JavaScript heap out of memory trước tiên.

Lỗi thứ hai xảy ra trên host. sandboxd chạy một pressure reaper để dừng các sandbox khi bộ nhớ trên host gần cạn. Vì vậy, trên máy nhỏ, một sandbox có thể biến mất trong lúc bạn đang xem preview của nó. Các file vẫn an toàn và request tiếp theo đến URL preview sẽ đánh thức sandbox, nhưng task đang chạy lúc container dừng sẽ không tiếp tục.

Disk là vấn đề ít rõ ràng hơn. Mỗi app có workspace riêng trên host, còn một project JavaScript có thể chứa cây node_modules lên đến hàng trăm megabyte. Mười app sẽ dùng vài gigabyte cho dependencies, chưa tính các image. Hãy bắt đầu với 40 GB và theo dõi dung lượng:

docker system df
sudo du -sh /var/lib/sandboxed/workspaces

Data directory mặc định là /var/lib/sandboxed, có thêm e. Nếu gõ /var/lib/sandboxd, bạn sẽ nhận một directory trống và mất năm phút để tìm nguyên nhân.

Cài đặt bản phát hành sandboxd được ghim phiên bản

Docker Engine kèm plugin Compose và git phải được cài sẵn trên máy. Cài đặt Docker trên VPS hướng dẫn phần này.

docker compose version
git --version

Cả hai lệnh phải in ra phiên bản. docker: 'compose' is not a docker command cho biết bạn đang có binary docker-compose độc lập cũ, trong khi trình cài đặt yêu cầu plugin v2.

Trình cài đặt là một shell script được tải qua mạng, vì vậy hãy đọc script trước khi chạy và ghim phiên bản.

curl -fsSL https://raw.githubusercontent.com/tastyeffectco/sandboxd/v0.3.6/install.sh -o install-sandboxd.sh
less install-sandboxd.sh
SANDBOXD_REF=v0.3.6 bash install-sandboxd.sh

SANDBOXD_REF là git ref mà trình cài đặt checkout vào $HOME/.sandboxd/src, mặc định là main. Nếu để biến này unset, bản cài đặt sẽ dùng bất kỳ thay đổi nào được merge vào sáng hôm đó. Điều này đáng lưu ý với một project đã release sáu bản chỉ riêng trong tháng 07 năm 2026. Hãy ghim phiên bản, rồi chỉ upgrade sau khi đọc changelog.

Script clone source, build các image, khởi động stack bằng docker compose up -d, rồi in URL của console và API token ở cuối. Hãy lưu token đó ở nơi an toàn. Đây là credential dùng cho một API có thể điều khiển Docker với quyền root.

curl http://127.0.0.1:9090/healthz

Lệnh này in ra ok khi control plane đã hoạt động. Nếu không in gì, stack chưa khởi động: chạy docker compose ps từ ~/.sandboxd/src để xem service nào đang down, sau đó chạy docker compose logs sandboxd để xem nguyên nhân.

Truy cập console trên máy chủ từ xa

Console được cung cấp thông qua Traefik tại HTTP_PORT, mặc định là cổng 80, với hostname http://console.localhost. Traefik định tuyến theo hostname, nên nếu nhập địa chỉ IP của máy chủ vào trình duyệt thì không khớp với rule nào và trả về lỗi 404. Cho đến khi cấu hình domain thật, hãy forward cổng và giữ nguyên hostname:

ssh -L 8080:127.0.0.1:80 you@your-vps

Sau đó mở http://console.localhost:8080 trên laptop. Trên Linux và macOS, mọi hostname kết thúc bằng .localhost đều phân giải về 127.0.0.1, nên request đi qua tunnel với header Host chính xác. Đặt mật khẩu console trong lần truy cập đầu tiên.

Cấp model cho agent

Trong base image có sẵn 2 coding agent: OpenCode và Claude Code. SANDBOXD_DEFAULT_AGENT quyết định agent nào chạy task không chỉ định agent cụ thể, và mặc định là opencode. Nếu chưa kết nối key nào, task sẽ chạy bằng các model miễn phí không cần key của OpenCode Zen. Vì vậy, lần build đầu tiên không tốn chi phí và bạn có thể kiểm thử toàn bộ quy trình trước khi phát sinh chi phí.

Kết nối key của bạn khi cần model mạnh hơn. Key được gửi đến control plane, không bao giờ được đưa vào sandbox. Key được lưu dưới dạng mã hóa trong data directory và được credential proxy inject trên đường truyền. Vì vậy, cả agent lẫn code do agent viết đều không thể đọc key.

export API=http://127.0.0.1:9090
export SANDBOXD_TOKEN=sk_...                       # printed by the installer
export AUTH="Authorization: Bearer $SANDBOXD_TOKEN"

curl -s -XPOST $API/v1/agents/claude-code/api-key -H "$AUTH" \
  -H 'content-type: application/json' \
  -d '{"api_key":"sk-ant-..."}'

Console cũng thực hiện việc này trong Settings, AI Agents. Tại đây có cả OAuth flow có hướng dẫn nếu bạn muốn dùng subscription của Claude thay vì API key. Model mặc định của từng agent được đặt trong cùng panel này. Bạn cũng có thể ghi đè model cho từng task.

Tạo một app nhỏ từ đầu đến cuối

Tạo app, khởi động sandbox rồi gửi một prompt. Các id được trả về dưới dạng JSON, và quickstart dùng sed để lấy chúng ra nên bạn không cần cài jq.

APP=$(curl -s -XPOST $API/v1/apps -H "$AUTH" \
  -H 'content-type: application/json' \
  -d '{"name":"todo","runtime_preset":"react-vite"}' \
  | sed -E 's/.*"id":"([^"]+)".*/\1/')

SB=$(curl -s -XPOST $API/v1/apps/$APP/sandbox -H "$AUTH" \
  -H 'content-type: application/json' -d '{"ports":[3000]}' \
  | sed -E 's/.*"id":"([^"]+)".*/\1/')

echo "app=$APP sandbox=$SB"

Cả hai biến đều phải chứa một id. $SB rỗng nghĩa là sandbox chưa bao giờ khởi động. Nguyên nhân thường gặp là base image vẫn đang được build hoặc host đã hết memory. Nếu nhận được 401 thay cho id thì bearer token không đúng.

curl -s -XPOST $API/v1/sandboxes/$SB/tasks -H "$AUTH" \
  -H 'content-type: application/json' \
  -d '{"prompt":"Add a todo list with a text input, an add button, and a delete button on each row. Keep the list in localStorage.","agent":"opencode"}'

Response chứa một task id. GET /v1/sandboxes/$SB/tasks/<task id> trả về kết quả của task, còn path /events trên cùng task là một luồng SSE (server sent events) trực tiếp hiển thị những gì agent đang thực hiện. Console cũng hiển thị cùng luồng này dưới dạng chat.

Sau đó app sẽ có tại http://s-<sandbox id>-3000.preview.localhost, trong đó 3000 là port bạn đã yêu cầu. Nếu sandbox đang ở trạng thái ngủ, request đầu tiên sẽ vào catch-all của Traefik. sandboxd khởi động container, chờ port phản hồi rồi hiển thị một trang chờ ngắn. Trang này sẽ tự refresh để chuyển sang app của bạn. Nếu preview không bao giờ rời khỏi trang đó, nghĩa là process bên trong không listen trên port được khai báo trong sandbox.yaml của app.

Đưa các bản preview lên domain thật với HTTPS

Mỗi sandbox có một hostname riêng, vì vậy một bản ghi DNS wildcard có thể bao phủ tất cả sandbox. Trỏ *.preview.yourdomain.com đến địa chỉ IP của server bằng bản ghi A. Sau đó đặt các biến preview trong .env~/.sandboxd/src:

PREVIEW_DOMAIN=yourdomain.com
PREVIEW_ENTRYPOINT=websecure
PREVIEW_TLS=true
SANDBOXD_API_AUTH_DISABLED=false

Traefik cần cấu hình tương ứng: bật entrypoint websecure trong traefik/traefik.yml và thêm certificate resolver. Dùng thử thách DNS-01, vì một chứng chỉ wildcard có thể bao phủ mọi hostname preview. Với HTTP-01, mỗi sandbox mới sẽ cần được cấp chứng chỉ riêng. Một buổi chiều build nhiều sandbox có thể nhanh chóng chạm giới hạn rate limit của Let's Encrypt. Chứng chỉ wildcard qua thử thách DNS-01 trình bày phần cấu hình DNS.

cd ~/.sandboxd/src
docker compose up -d

URL preview sẽ có dạng https://s-<id>-3000.preview.yourdomain.com. Mở cổng 80 và 443 trên firewall, đồng thời không mở cổng 9090 ra Internet: xem các rule firewall ufw cơ bản. Lưu ý rằng bất kỳ ai đoán được hostname preview đều có thể tải ứng dụng, vì vậy hãy xem preview là public.

Mã được tạo nằm ở đâu và có thể export không?

Trên host, trong data directory. Mỗi workspace là một directory thông thường tại /var/lib/sandboxed/workspaces/<id>/, được bind mount vào container, còn các file của app nằm tại /home/sandbox/workspace/app bên trong sandbox. Trạng thái control plane nằm trong một file SQLite duy nhất tại state/sandboxd.db, còn credentials của agent đã mã hóa nằm tại agent-auth/. Không có dữ liệu nào bị ẩn trong container layer, nên backup chỉ cần copy directory đó cùng với file database. backup restic trên VPS xử lý được cả hai.

sudo ls /var/lib/sandboxed/workspaces
sudo du -sh /var/lib/sandboxed/workspaces/*

Git export được tích hợp sẵn thay vì phải gắn thêm. API cung cấp status và diff để đọc, sau đó là commit và push:

curl -s $API/v1/apps/$APP/git/status -H "$AUTH"

curl -s -XPOST $API/v1/apps/$APP/git/commit -H "$AUTH" \
  -H 'content-type: application/json' \
  -d '{"message":"todo list, first pass"}'

curl -s -XPOST $API/v1/apps/$APP/git/push -H "$AUTH" \
  -H 'content-type: application/json' -d '{"branch":"main"}'

Remote riêng tư cần personal access token. Bạn chỉ cần đặt token một lần trong console tại Settings, Git credentials. Token được lưu ở dạng mã hóa và nằm ngoài sandbox, nên agent không thể đọc hoặc tự ý dùng token để push. Hãy push sớm và thường xuyên. Cho đến khi bạn làm vậy, workspace directory là bản sao duy nhất của code, và DELETE /v1/apps/<id> sẽ xóa nó mà không có cách khôi phục lần thứ hai.

Chi phí build tính theo token của model là bao nhiêu?

sandboxd không đo mức chi tiêu của bạn, vì vậy con số cần theo dõi nằm trong console của provider. Các model OpenCode Zen miễn phí không tốn chi phí, nhưng chậm và yếu hơn model trả phí. Điều này thể hiện qua số vòng sửa lỗi nhiều hơn đối với mọi thứ phức tạp hơn một app thử nghiệm đơn giản.

Hóa đơn phụ thuộc vào cách agent loop hoạt động. Mỗi lượt gửi lại context cần thiết, vì vậy chi phí phụ thuộc vào số lượt, không phải số app. Một prompt cho kết quả ngay sẽ ít tốn kém. Nhưng 15 vòng yêu cầu “bây giờ sửa khoảng cách” trên một project có 50 file thì không, vì nội dung các file được gửi kèm trong mỗi lượt. Token input và output có cách tính giá khác nhau, và chi phí của coding agent cho mỗi session đưa ra khoảng chi phí thực tế. Hãy đặt giới hạn chi tiêu cứng tại provider trước khi giao quyền cho một loop chạy unattended.

Dọn dẹp sandbox không còn được sử dụng

Idle reaper sẽ dừng mọi sandbox đã idle quá SANDBOXD_IDLE_THRESHOLD_SECONDS. Giá trị mặc định là 2100 giây, tương đương 35 phút. Thao tác này giải phóng RAM nhưng giữ lại các file. Request tiếp theo đến preview URL sẽ đánh thức container. Hãy giảm giá trị này trên máy có ít tài nguyên, vì các container idle trong 35 phút sẽ chiếm dụng lượng memory mà bạn không thể dùng cho việc khác trong suốt thời gian đó.

Dừng không đồng nghĩa với xóa, và đây là nguyên nhân khiến disk âm thầm đầy lên. Sandbox đã dừng vẫn giữ workspace và container của nó. Xóa sandbox nhưng giữ lại app là DELETE trên sandbox. Thao tác này đồng thời xóa container và workspace. Xóa app sẽ xóa mọi thứ vĩnh viễn.

curl -s -XPOST $API/v1/sandboxes/$SB/stop -H "$AUTH"     # frees RAM, keeps files
curl -s -XDELETE $API/v1/sandboxes/$SB -H "$AUTH"        # container and workspace gone
curl -s -XDELETE $API/v1/apps/$APP -H "$AUTH"            # app and everything under it

Sau vài tuần thử nghiệm, docker system df sẽ hiển thị nhiều image space có thể reclaim hơn bạn dự kiến, vì mỗi app tự pull toolchain của mình đều để lại các layer. docker image prune sẽ xóa những layer dangling. Trước tiên hãy kiểm tra GET /v1/apps, vì image vẫn được sandbox đang sleep tham chiếu thì chưa phải garbage.

Ranh giới container cung cấp và không cung cấp

Mỗi sandbox chạy dưới một user không có đặc quyền, sử dụng root filesystem chỉ đọc, đã loại bỏ toàn bộ Linux capabilities, đặt no-new-privileges, giới hạn bộ nhớ và giới hạn số process. Dự án nêu rõ giới hạn này: container Linux dùng chung kernel là một ranh giới cô lập mạnh nhưng là ranh giới bảo mật yếu. Một lỗi trong kernel có thể khiến host bị breached.

Có 2 điểm cần xử lý. Network egress từ sandbox đang mở trong bản self-hosted build, nên code được tạo có thể truy cập internet, local network và các endpoint cloud metadata. Một subsystem egress của nftables có trong source nhưng bị tắt khi biên dịch bản Docker Compose portable. Vì vậy, các giới hạn này phải do firewall trên host áp đặt. Control plane API về thực tế có quyền root trên host vì nó điều khiển Docker socket. API bind vào 127.0.0.1:9090 theo mặc định, SANDBOXD_API_AUTH_DISABLED phải luôn là false, và tuyệt đối không được publish ra internet.

Nếu bạn định cho người khác gửi prompt đến máy của mình, mô hình này tự nó chưa đủ an toàn. Dự án đề xuất gVisor với SANDBOXD_RUNTIME=runsc. Cơ chế này đặt một kernel chạy trong userspace giữa sandbox và host, nhưng khiến các workload nặng về syscall chậm hơn khoảng 1.7 đến 4 lần. Giải pháp mạnh hơn là dùng một máy cho mỗi tenant. Lý do cũng giống như chạy coding agent trong VM dùng một lần.

Bạn có nên xây dựng trên một dự án đã hai tháng tuổi không?

Đối với một máy build cá nhân, có, nhưng cần áp dụng các biện pháp thận trọng cơ bản: pin SANDBOXD_REF, sao lưu /var/lib/sandboxed và đẩy mọi app bạn cần giữ vào một git remote. Với bất kỳ thứ gì khách hàng sử dụng, hãy chờ đến phiên bản 1.0 hoặc dự trù chi phí cho việc xử lý lỗi, vì maintainer nói rõ rằng nhánh 0.x có thể thay đổi mà không báo trước. Tính đến tháng 8 năm 2026, maintainer cũng cung cấp gói cài đặt được quản lý với giá 79 dollar mỗi tháng. Đây là thông tin đáng biết khi đánh giá liệu dự án có lý do để tiếp tục tồn tại hay không.

Rủi ro này có thể chấp nhận được vì đầu ra của dự án. sandboxd tạo ra một ứng dụng thông thường trong một git repository thông thường. Vì vậy, nếu dự án ngừng phát triển, bạn vẫn giữ được code và chỉ mất wrapper. Đây là vị thế tốt hơn nhiều so với một hosted builder giữ quyền sở hữu dự án của bạn. Để có góc nhìn rộng hơn về những gì đáng chạy trên server trong năm nay, hãy xem những gì đáng tự host trong năm 2026.

FAQ

Yêu cầu tối thiểu của server để chạy sandboxd là gì?

Dự án nêu rằng 2 vCPU và 4 GB RAM là đủ để bắt đầu. Mức này bao phủ control plane, Traefik và một sandbox nhỏ. Nếu muốn chạy đồng thời nhiều app, hãy dùng 8 GB RAM và 40 GB disk, vì mỗi sandbox đang chạy chứa đầy đủ toolchain Node hoặc Python, còn mỗi workspace giữ riêng cây dependency trên disk. Khi host sắp hết tài nguyên, pressure reaper của sandboxd sẽ dừng các sandbox để giải phóng memory. Build vượt quá memory ceiling của container sẽ bị kernel kill: docker ps -a hiển thị exit code 137 cho tiến trình đó.

sandboxd khác Dify hoặc OpenHands như thế nào?

Chúng tạo ra các loại artifact khác nhau. Dify build các ứng dụng gọi model khi runtime, chẳng hạn giao diện chat và pipeline truy xuất. OpenHands chỉnh sửa một repository có sẵn, chạy command và đề xuất thay đổi cho code hiện tại. sandboxd dựng một project hoàn toàn mới từ prompt, build project đó bên trong container riêng rồi phục vụ project tại preview URL. Kết quả là một web application thông thường, không cần model để chạy.

Code do agent viết thực sự nằm ở đâu?

Code nằm trên filesystem của host, không nằm bên trong container image. Mỗi app có một directory tại /var/lib/sandboxed/workspaces/<id>/ được bind mount vào sandbox, và các file xuất hiện tại /home/sandbox/workspace/app bên trong sandbox. State của control plane là một file SQLite duy nhất tại state/ trong cùng data directory. Bạn có thể commit và push lên git remote từ Git tab của console hoặc thông qua các endpoint /v1/apps/<id>/git/commit/git/push. Token dùng cho remote private được control plane lưu dưới dạng encrypted, thay vì chuyển cho sandbox.

Expose sandboxd ra Internet có an toàn không?

Chỉ expose preview URL và console, không expose control plane API. API đó điều khiển Docker trên host, nên có quyền tương đương root. Vì lý do này, API bind vào 127.0.0.1:9090 theo mặc định. Trong bản self-hosted, sandbox cũng có network egress mở. Điều đó có nghĩa là code do agent viết có thể truy cập local network và các cloud metadata endpoint của bạn. Vì vậy, hãy thêm rule cho host firewall nếu trên máy có các hệ thống khác cần bảo vệ. Với prompt từ những người bạn không tin tưởng, hãy chạy một host cho mỗi tenant thay vì chỉ dựa vào ranh giới của container.

#sandboxd#ai-agents#self-hosted#app-builder#Docker