程式代理說功能已經能用了。也許確實如此。它寫的測試都通過了,但這證明的東西比大家希望的少。你真正想要的,是在它接近使用者之前先在瀏覽器裡打開看看,而且不必讓代理碰正式環境。
預覽環境正好解決這個問題,而且不需要什麼平台。一台有獨立 IPv4 的 VPS、Docker、Caddy 和兩個短腳本,就能給每個分支一個自己的 URL,例如 https://feature-login-form.preview.example.com,有密碼保護,每次推送都會重新建置。代理拿到的 SSH 金鑰只能啟動和停止預覽,其他什麼都做不了。
我們在測試伺服器上架好這一套,寫之前完整跑過一遍:建立、開啟、再次推送、拆除、嘗試濫用金鑰。
需要準備什麼
- 一台有公開 IPv4 的 VPS。AI-Agent-IP 可以同時容納多個預覽。
- 一筆萬用字元 DNS 記錄:
*.preview.example.com→ 伺服器 IP。 - 你的應用程式儲存庫,內含在 8080 連接埠提供服務的
Dockerfile。任何語言都行。 - 裝好
apt install -y docker.io caddy git的 Ubuntu 24.04 或 Debian 12/13。
用唯讀的部署金鑰把儲存庫複製到 /srv/app,只需一次。
兩個腳本
/usr/local/bin/preview-up 建置一個分支並上線:
#!/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 把它移除:
#!/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
裡面有幾處是刻意的。容器只在 127.0.0.1 上以隨機連接埠發布,所以除了經過 Caddy,沒有東西能連到它們。這點很重要,因為 Docker 發布的連接埠會繞過 ufw。每個預覽有 512 MB 記憶體上限,一個失控的分支不會拖垮其他分支。再次執行同一個分支會就地重新建置:推送、執行 up、重新整理。
Caddy:每個預覽一個檔案,所有預覽共用一組密碼
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'
把雜湊填進 /etc/caddy/Caddyfile:
(preview_common) {
basicauth {
reviewer PASTE-THE-HASH-HERE
}
}
import /etc/caddy/previews/*.caddy
接著執行 systemctl reload caddy。現在所有預覽都在同一個登入後面,帶除錯輸出的半成品分支不會暴露在網際網路上。較新的 Caddy 版本裡這個指令也寫作 basic_auth;basicauth 在兩種版本都還能用。
代理的金鑰:只有 up 和 down
代理需要的是觸發預覽,而不是擁有伺服器。建立一個 deploy 使用者和一個只接受兩道指令的閘道腳本:
#!/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
在 /home/deploy/.ssh/authorized_keys 裡,把代理的公開金鑰綁定到這個腳本:
restrict,command="/usr/local/bin/preview-gate" ssh-ed25519 AAAA... coding-agent
在代理工作的機器上:
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
在我們的測試中,up 建置並發布了分支,新提交後的第二次 up 提供了新版本,down 移除了容器和網站。用這把金鑰嘗試 cat /etc/shadow、up x; id、-x 或 a/../b 這類分支名稱,或是互動式登入,得到的都只有 usage: up|down <branch>。分支名稱必須以字母或數字開頭,而且不能包含 ..。
接上代理
使用 Claude Code 或 Codex 時,在專案的指示檔裡加一行就夠了:
推送分支後,執行
ssh deploy@preview.example.com up <branch>,並把印出的 URL 寫進你的摘要。分支合併或放棄後執行down。
如果你的 CI 已經在每次推送時執行,就從 CI 呼叫同一道指令,讓金鑰完全不出現在代理的環境裡。這是更嚴格的做法,凡是超出個人專案的情況,我們都會選它。
需要留意的地方
- 磁碟。 每次建置都會留下映像檔層。每週用 cron 跑一次
docker image prune -f就能控制;down已經會刪除預覽自己的映像檔。 - 沒人關閉的分支。 代理開的分支比人多。用一個 cron 列出超過一週的
preview-*容器並對它們執行preview-down,就不會冒出二十個被遺忘的預覽。 - 機密。 預覽應該使用測試憑證。basic auth 密碼擋得住陌生人,但不代表可以把預覽接到正式資料庫。
- 建置時間。 映像檔在伺服器上建置,所以重型建置會和正在執行的預覽搶 CPU。如果建置要好幾分鐘,就在 CI 建置,再拉取映像檔。
代理本身不一定要住在這台伺服器上。如果住在上面,請看用於 Claude Code 的 VPS;要讓它自己下單並照看這台伺服器,請把它接上 EQVPS MCP 伺服器。同一台機器也能代管團隊的 MCP 閘道。要在完全沒有伺服器的情況下執行不可信的程式碼,可以用沙箱,而有獨立 IPv4 的方案每月 $8 起。
評論
目前還沒有評論。歡迎成為第一位。