−25%

cho Windows trả theo năm, đến 31/10. Xem các gói

EQVPS
Bắt đầu

VPS cho bản xem trước của tác tử lập trình: mỗi nhánh tác tử đẩy lên có URL riêng

Một tác tử lập trình làm xong một nhánh và nói rằng nó chạy được. Hãy cho nó một máy chủ để kiểm chứng: mỗi nhánh được build thành container và mở ở subdomain HTTPS riêng sau mật khẩu, còn khóa SSH của tác tử chỉ có thể bật và tắt bản xem trước.

Một tác tử lập trình nói tính năng đã chạy. Có thể đúng vậy. Các test nó viết đều qua, và điều đó chứng minh ít hơn mọi người mong. Điều bạn thực sự muốn là mở nó trên trình duyệt trước khi nó tới gần người dùng, mà không phải nhờ tác tử chạm vào production.

Môi trường xem trước giải quyết việc đó, và bạn không cần một nền tảng nào. Một VPS có IPv4 riêng, Docker, Caddy và hai script ngắn cho mỗi nhánh một URL riêng như https://feature-login-form.preview.example.com, sau mật khẩu, build lại sau mỗi lần push. Tác tử nhận một khóa SSH có thể bật và tắt bản xem trước, ngoài ra không làm được gì.

Chúng tôi dựng việc này trên một máy chủ thử nghiệm và chạy hết vòng trước khi viết: tạo, mở, push lại, gỡ bỏ, thử lạm dụng khóa.

Bạn cần gì

  • Một VPS có IPv4 công khai. AI-Agent-IP đủ chỗ cho nhiều bản xem trước cùng lúc.
  • Một bản ghi DNS wildcard: *.preview.example.com → IP của máy chủ.
  • Repository ứng dụng có Dockerfile phục vụ ở cổng 8080. Ngôn ngữ nào cũng được.
  • Ubuntu 24.04 hoặc Debian 12/13 với apt install -y docker.io caddy git.

Clone repository một lần vào /srv/app bằng deploy key chỉ có quyền đọc.

Hai script

/usr/local/bin/preview-up build một nhánh và đưa lên mạng:

#!/usr/bin/env bash
# preview-up <branch> — build the branch and open it at https://<name>.preview.example.com
set -euo pipefail
BRANCH="$1"
NAME=$(printf '%s' "$BRANCH" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed 's/--*/-/g; s/^-//; s/-$//' | cut -c1-40)
DIR=/srv/previews/$NAME
git -C /srv/app fetch --quiet origin "$BRANCH"
rm -rf "$DIR"; git -C /srv/app worktree prune
git -C /srv/app worktree add --quiet --force --detach "$DIR" FETCH_HEAD
docker build --quiet -t "preview-$NAME" "$DIR" >/dev/null
docker rm -f "preview-$NAME" >/dev/null 2>&1 || true
docker run -d --name "preview-$NAME" --restart unless-stopped --memory 512m -p 127.0.0.1::8080 "preview-$NAME" >/dev/null
PORT=$(docker port "preview-$NAME" 8080/tcp | head -1 | awk -F: '{print $NF}')
cat > "/etc/caddy/previews/$NAME.caddy" <<CADDY
$NAME.preview.example.com {
	import preview_common
	reverse_proxy 127.0.0.1:$PORT
}
CADDY
systemctl reload caddy
echo "https://$NAME.preview.example.com"

/usr/local/bin/preview-down gỡ nó đi:

#!/usr/bin/env bash
# preview-down <branch> — remove the branch preview
set -euo pipefail
NAME=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed 's/--*/-/g; s/^-//; s/-$//' | cut -c1-40)
docker rm -f "preview-$NAME" >/dev/null 2>&1 || true
docker image rm "preview-$NAME" >/dev/null 2>&1 || true
rm -f "/etc/caddy/previews/$NAME.caddy"; rm -rf "/srv/previews/$NAME"; git -C /srv/app worktree prune
systemctl reload caddy

Một vài lựa chọn ở đây là có chủ ý. Container chỉ publish trên 127.0.0.1 với cổng ngẫu nhiên, nên không gì tới được chúng ngoài qua Caddy. Điều đó quan trọng vì các cổng Docker publish sẽ vượt qua ufw. Mỗi bản xem trước bị giới hạn 512 MB bộ nhớ, để một nhánh mất kiểm soát không kéo sập các nhánh khác. Và chạy lại cùng nhánh sẽ build lại tại chỗ: push, chạy up, tải lại trang.

Caddy: mỗi bản xem trước một tệp, một mật khẩu cho tất cả

mkdir -p /etc/caddy/previews /srv/previews
chmod +x /usr/local/bin/preview-up /usr/local/bin/preview-down
caddy hash-password --plaintext 'choose-a-review-password'

Đặt hash vào /etc/caddy/Caddyfile:

(preview_common) {
	basicauth {
		reviewer PASTE-THE-HASH-HERE
	}
}
import /etc/caddy/previews/*.caddy

Sau đó systemctl reload caddy. Giờ mọi bản xem trước đều nằm sau cùng một đăng nhập, nên một nhánh dở dang có output debug không bị phơi ra internet. Ở các bản Caddy mới, chỉ thị này còn được viết là basic_auth; basicauth vẫn chạy ở cả hai.

Khóa của tác tử: up và down, không gì khác

Tác tử cần kích hoạt bản xem trước, không cần sở hữu máy chủ. Tạo người dùng deploy và một script cổng chỉ nhận đúng hai lệnh:

#!/usr/bin/env bash
# /usr/local/bin/preview-gate — the only thing the agent's key may run
set -euo pipefail
read -r ACTION BRANCH EXTRA <<< "${SSH_ORIGINAL_COMMAND:-}"
[[ -z "${EXTRA:-}" && "${BRANCH:-}" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]{0,99}$ && "$BRANCH" != *..* ]] || { echo "usage: up|down <branch>" >&2; exit 2; }
case "$ACTION" in
  up)   exec sudo /usr/local/bin/preview-up "$BRANCH" ;;
  down) exec sudo /usr/local/bin/preview-down "$BRANCH" ;;
  *)    echo "usage: up|down <branch>" >&2; exit 2 ;;
esac
useradd -m -s /bin/bash deploy
echo 'deploy ALL=(root) NOPASSWD: /usr/local/bin/preview-up, /usr/local/bin/preview-down' > /etc/sudoers.d/preview
chmod 440 /etc/sudoers.d/preview && visudo -cf /etc/sudoers.d/preview
chmod 755 /usr/local/bin/preview-gate

Trong /home/deploy/.ssh/authorized_keys, gắn khóa công khai của tác tử vào script đó:

restrict,command="/usr/local/bin/preview-gate" ssh-ed25519 AAAA... coding-agent

Từ máy nơi tác tử làm việc:

ssh deploy@preview.example.com up feature/login-form
# https://feature-login-form.preview.example.com
ssh deploy@preview.example.com down feature/login-form

Trong thử nghiệm của chúng tôi, up đã build và publish nhánh, up lần hai sau commit mới phục vụ phiên bản mới, và down gỡ container cùng trang. Các thử nghiệm cat /etc/shadow, up x; id, tên nhánh như -x hay a/../b, hoặc đăng nhập tương tác bằng khóa đó đều chỉ nhận usage: up|down <branch>. Tên nhánh phải bắt đầu bằng chữ cái hoặc chữ số và không được chứa ...

Nối vào tác tử

Với Claude Code hoặc Codex, một dòng trong tệp hướng dẫn của dự án là đủ:

Sau khi push một nhánh, chạy ssh deploy@preview.example.com up <branch> và đưa URL được in ra vào phần tóm tắt. Chạy down khi nhánh đã được merge hoặc bỏ đi.

Nếu CI của bạn đã chạy mỗi lần push, hãy gọi cùng lệnh đó từ CI và giữ khóa hoàn toàn khỏi môi trường của tác tử. Đó là lựa chọn chặt chẽ hơn, và chúng tôi sẽ chọn nó cho mọi thứ vượt quá một dự án cá nhân.

Những điều cần để ý

  • Ổ đĩa. Mỗi lần build để lại các layer image. Một cron hằng tuần với docker image prune -f giữ nó trong tầm kiểm soát; down đã xóa image riêng của bản xem trước.
  • Những nhánh không ai đóng. Tác tử mở nhiều nhánh hơn con người. Một cron liệt kê các container preview-* cũ hơn một tuần và chạy preview-down trên chúng sẽ giúp bạn khỏi gặp hai mươi bản xem trước bị bỏ quên.
  • Bí mật. Bản xem trước nên chạy bằng thông tin đăng nhập thử nghiệm. Mật khẩu basic auth chặn người lạ, nhưng không có nghĩa là được trỏ bản xem trước vào cơ sở dữ liệu production.
  • Thời gian build. Image được build trên máy chủ, nên một lần build nặng sẽ tranh CPU với các bản xem trước đang chạy. Nếu build mất vài phút, hãy build trong CI rồi kéo image về.

Bản thân tác tử không nhất thiết sống trên máy chủ này. Nếu có, xem VPS cho Claude Code, và để nó tự đặt và chăm sóc máy chủ đó, kết nối nó với máy chủ MCP của EQVPS. Cùng máy đó cũng có thể chạy MCP gateway của nhóm bạn. Để chạy mã không đáng tin mà không cần máy chủ có sandbox, và các gói có IPv4 riêng bắt đầu từ $8 mỗi tháng.

FAQ

Sao không để tác tử triển khai thẳng lên production?

Vì “test đã qua” và “chạy được” không giống nhau, và tác tử nên là bên cuối cùng quyết định điều đó. Bản xem trước cho con người thứ để bấm vào trước khi bất cứ thứ gì tới người dùng, chỉ tốn thêm một lệnh.

Gói nào phù hợp?

Tùy số bản xem trước chạy cùng lúc. AI-Agent-IP ($15/tháng, 4 vCPU, 4 GB RAM, ổ 40 GB) chứa thoải mái vài ứng dụng web nhỏ, mỗi cái giới hạn 512 MB. Build Docker là phần nặng nhất, nên hãy để ý ổ đĩa và bộ nhớ nếu image của bạn lớn.

Tôi có cần chứng chỉ wildcard không?

Không. Caddy tự lấy chứng chỉ riêng cho từng subdomain xem trước. Chỉ bản ghi DNS là wildcard. Let's Encrypt cho phép khoảng 50 chứng chỉ mới mỗi tên miền đăng ký mỗi tuần, thừa đủ trừ khi tác tử tạo hàng trăm nhánh.

Tác tử có thể phá máy chủ bằng khóa SSH của nó không?

Không với khóa đó. Nó bị giới hạn ở một lệnh duy nhất chỉ nhận “up <nhánh>” hoặc “down <nhánh>” với tên nhánh an toàn. Truy cập shell, tham số thừa và chuyển tiếp cổng đều bị từ chối.

Có chạy trên gói NAT không?

Không. Bản xem trước phải truy cập được qua cổng 80 và 443 ở subdomain riêng, mà việc đó cần IPv4 công khai. Gói NAT chỉ chuyển tiếp cổng SSH.

Bình luận

Chưa có bình luận nào. Hãy là người đầu tiên.

Để lại bình luận

Bình luận được kiểm duyệt trước khi hiển thị.