−25%

Windows รายปี ถึง 31 ต.ค. ดูแพ็กเกจ

EQVPS
เริ่มต้นใช้งาน

VPS สำหรับพรีวิวของเอเจนต์เขียนโค้ด: ทุก branch ที่เอเจนต์พุชได้ URL ของตัวเอง

เอเจนต์เขียนโค้ดทำ branch เสร็จแล้วบอกว่าใช้งานได้ ให้เซิร์ฟเวอร์ที่ตรวจสอบเรื่องนี้ได้: ทุก branch ถูก build เป็นคอนเทนเนอร์และเปิดบน subdomain HTTPS ของตัวเองหลังรหัสผ่าน และคีย์ SSH ของเอเจนต์ทำได้แค่เปิดและปิดพรีวิว

เอเจนต์เขียนโค้ดบอกว่าฟีเจอร์ใช้ได้แล้ว อาจจะจริง เทสต์ที่มันเขียนผ่านหมด ซึ่งพิสูจน์ได้น้อยกว่าที่ทุกคนอยากให้เป็น สิ่งที่คุณต้องการจริง ๆ คือเปิดดูในเบราว์เซอร์ก่อนที่มันจะเข้าใกล้ผู้ใช้ โดยไม่ต้องให้เอเจนต์แตะโปรดักชัน

สภาพแวดล้อมพรีวิวแก้ปัญหานี้ และไม่ต้องมีแพลตฟอร์มใด ๆ VPS หนึ่งเครื่องที่มี IPv4 ของตัวเอง Docker Caddy และสคริปต์สั้น ๆ สองตัว ให้ทุก branch มี URL ของตัวเองแบบ https://feature-login-form.preview.example.com อยู่หลังรหัสผ่าน และ build ใหม่ทุกครั้งที่พุช เอเจนต์ได้คีย์ SSH ที่เปิดและปิดพรีวิวได้ และทำอย่างอื่นไม่ได้

เราสร้างระบบนี้บนเซิร์ฟเวอร์ทดสอบและไล่ครบทั้งวงจรก่อนเขียน: สร้าง เปิด พุชอีกครั้ง รื้อทิ้ง และลองใช้คีย์ในทางที่ผิด

สิ่งที่ต้องมี

  • VPS ที่มี IPv4 สาธารณะ AI-Agent-IP รองรับพรีวิวหลายตัวพร้อมกัน
  • DNS record แบบ wildcard: *.preview.example.com → IP ของเซิร์ฟเวอร์
  • repository ของแอปที่มี Dockerfile ซึ่งให้บริการบนพอร์ต 8080 ภาษาใดก็ได้
  • Ubuntu 24.04 หรือ Debian 12/13 พร้อม apt install -y docker.io caddy git

Clone repository หนึ่งครั้งไปที่ /srv/app ด้วย deploy key แบบอ่านอย่างเดียว

สองสคริปต์

/usr/local/bin/preview-up build branch แล้วนำขึ้นออนไลน์:

#!/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

มีบางทางเลือกที่ตั้งใจ คอนเทนเนอร์ publish เฉพาะบน 127.0.0.1 ด้วยพอร์ตสุ่ม จึงไม่มีอะไรเข้าถึงได้นอกจากผ่าน Caddy เรื่องนี้สำคัญเพราะพอร์ตที่ Docker publish จะข้าม ufw ไป พรีวิวแต่ละตัวจำกัดหน่วยความจำ 512 MB เพื่อไม่ให้ branch ที่คุมไม่อยู่ล้มตัวอื่นไปด้วย และการรัน branch เดิมซ้ำจะ build ใหม่ในที่เดิม: พุช รัน 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'

ใส่ hash ลงใน /etc/caddy/Caddyfile:

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

จากนั้น systemctl reload caddy ตอนนี้ทุกพรีวิวอยู่หลังการล็อกอินเดียวกัน branch ที่ทำไม่เสร็จพร้อมเอาต์พุต debug จึงไม่เปิดโล่งบนอินเทอร์เน็ต ใน Caddy รุ่นใหม่ ๆ directive นี้สะกดเป็น 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 build และ publish branch, up ครั้งที่สองหลัง commit ใหม่ให้บริการเวอร์ชันใหม่ และ down ลบทั้งคอนเทนเนอร์และไซต์ การลอง cat /etc/shadow, up x; id, ชื่อ branch อย่าง -x หรือ a/../b หรือการล็อกอินแบบโต้ตอบด้วยคีย์นั้น ได้รับเพียง usage: up|down <branch> ทั้งหมด ชื่อ branch ต้องขึ้นต้นด้วยตัวอักษรหรือตัวเลข และห้ามมี ..

เชื่อมเข้ากับเอเจนต์

กับ Claude Code หรือ Codex แค่บรรทัดเดียวในไฟล์คำสั่งของโปรเจกต์ก็พอ:

หลังพุช branch ให้รัน ssh deploy@preview.example.com up <branch> แล้วใส่ URL ที่พิมพ์ออกมาในสรุปของคุณ รัน down เมื่อ branch ถูก merge หรือถูกทิ้ง

ถ้า CI ของคุณรันทุกครั้งที่พุชอยู่แล้ว ให้เรียกคำสั่งเดียวกันจากตรงนั้นและเก็บคีย์ให้พ้นสภาพแวดล้อมของเอเจนต์ทั้งหมด นั่นคือทางเลือกที่เข้มงวดกว่า และเราจะเลือกแบบนี้สำหรับทุกอย่างที่เกินโปรเจกต์ส่วนตัว

สิ่งที่ต้องคอยดู

  • ดิสก์ ทุกการ build ทิ้ง layer ของอิมเมจไว้ cron รายสัปดาห์ที่รัน docker image prune -f ช่วยคุมไว้ได้ ส่วน down ลบอิมเมจของพรีวิวนั้นอยู่แล้ว
  • branch ที่ไม่มีใครปิด เอเจนต์เปิด branch มากกว่าคน cron ที่แสดงคอนเทนเนอร์ preview-* ที่เก่ากว่าหนึ่งสัปดาห์แล้วรัน preview-down กับมัน จะช่วยไม่ให้คุณเจอพรีวิวที่ถูกลืมยี่สิบตัว
  • ความลับ พรีวิวควรรันด้วยข้อมูลรับรองสำหรับทดสอบ รหัสผ่าน basic auth กันคนแปลกหน้าได้ แต่ไม่ได้ทำให้ชี้พรีวิวไปที่ฐานข้อมูลโปรดักชันได้
  • เวลา build อิมเมจถูก build บนเซิร์ฟเวอร์ การ build หนัก ๆ จึงแย่ง CPU กับพรีวิวที่รันอยู่ ถ้า build นานหลายนาที ให้ build ใน CI แล้วดึงอิมเมจมาแทน

ตัวเอเจนต์เองไม่จำเป็นต้องอยู่บนเซิร์ฟเวอร์นี้ ถ้าอยู่ ดู VPS สำหรับ Claude Code และถ้าต้องการให้มันสั่งซื้อและดูแลเซิร์ฟเวอร์นั้นเอง เชื่อมต่อกับเซิร์ฟเวอร์ MCP ของ EQVPS เครื่องเดียวกันยังโฮสต์ MCP gateway ของทีมได้ด้วย สำหรับรันโค้ดที่ไม่น่าเชื่อถือโดยไม่ต้องมีเซิร์ฟเวอร์มี sandbox และแพ็กเกจที่มี IPv4 ของตัวเอง เริ่มต้นที่ $8 ต่อเดือน

FAQ

ทำไมไม่ให้เอเจนต์ดีพลอยขึ้นโปรดักชันโดยตรง

เพราะ “เทสต์ผ่าน” กับ “ใช้งานได้” ไม่ใช่เรื่องเดียวกัน และเอเจนต์ควรเป็นคนสุดท้ายที่ตัดสินเรื่องนี้ พรีวิวให้คนมีอะไรให้คลิกดูก่อนที่สิ่งใดจะถึงผู้ใช้ แลกกับคำสั่งเพิ่มอีกหนึ่งคำสั่ง

แพ็กเกจไหนเหมาะ

ขึ้นกับว่ามีพรีวิวรันพร้อมกันกี่ตัว AI-Agent-IP ($15/เดือน, 4 vCPU, 4 GB RAM, ดิสก์ 40 GB) รองรับเว็บแอปเล็ก ๆ หลายตัวได้สบาย โดยจำกัดตัวละ 512 MB ส่วนที่หนักที่สุดคือการ build ของ Docker ถ้าอิมเมจใหญ่ให้คอยดูดิสก์และหน่วยความจำ

ต้องใช้ใบรับรอง wildcard ไหม

ไม่ต้อง Caddy ขอใบรับรองแยกให้แต่ละ subdomain ของพรีวิวเอง มีแค่ DNS record ที่เป็น wildcard Let's Encrypt ให้ใบรับรองใหม่ได้ราว 50 ใบต่อโดเมนที่จดทะเบียนต่อสัปดาห์ เหลือเฟือ เว้นแต่เอเจนต์จะสร้าง branch นับร้อย

เอเจนต์จะทำเซิร์ฟเวอร์พังด้วยคีย์ SSH ได้ไหม

ไม่ได้ด้วยคีย์นั้น คีย์ถูกจำกัดให้ใช้คำสั่งเดียวที่รับเฉพาะ “up <branch>” หรือ “down <branch>” พร้อมชื่อ branch ที่ปลอดภัย การเข้า shell อาร์กิวเมนต์เพิ่มเติม และการ forward พอร์ตจะถูกปฏิเสธ

ใช้กับแพ็กเกจ NAT ได้ไหม

ไม่ได้ พรีวิวต้องเข้าถึงได้ทางพอร์ต 80 และ 443 บน subdomain ของตัวเอง ซึ่งต้องใช้ IPv4 สาธารณะ แพ็กเกจ NAT ส่งต่อเฉพาะพอร์ต SSH

ความคิดเห็น

ยังไม่มีความคิดเห็น เป็นคนแรกสิ

แสดงความคิดเห็น

ความคิดเห็นจะถูกตรวจสอบก่อนแสดง