−25%

на річну оплату Windows, до 31.10. До тарифів

EQVPS
Почати

VPS для превʼю coding-агентів: кожна запушена гілка отримує власну адресу

Coding-агент закінчив гілку й каже, що все працює. Дайте йому сервер, де це можна перевірити: кожна гілка збирається в контейнер і відкривається на власному HTTPS-піддомені за паролем, а SSH-ключ агента вміє лише запускати й зупиняти превʼю.

Coding-агент каже, що фіча працює. Можливо. Тести, які він написав, проходять, і це доводить менше, ніж усім хотілося б. Насправді вам потрібно відкрити результат у браузері, перш ніж він наблизиться до користувачів, і зробити це, не просячи агента торкатися продакшну.

Це вирішують превʼю-оточення, і платформа для них не потрібна. Один VPS із власним IPv4, Docker, Caddy і два короткі скрипти дають кожній гілці власну адресу на кшталт https://feature-login-form.preview.example.com — за паролем і з перезбиранням після кожного push. Агент отримує SSH-ключ, який уміє запускати й зупиняти превʼю, і нічого більше.

Ми зібрали це на тестовому сервері й пройшли весь цикл, перш ніж описувати: створити, відкрити, запушити знову, прибрати, спробувати зловживати ключем.

Що потрібно

  • VPS із публічним IPv4. На AI-Agent-IP вміщується кілька превʼю одночасно.
  • Wildcard-запис DNS: *.preview.example.com → IP сервера.
  • Репозиторій застосунку з Dockerfile, який віддає на порту 8080. Мова будь-яка.
  • Ubuntu 24.04 або Debian 12/13 з apt install -y docker.io caddy git.

Один раз клонуйте репозиторій у /srv/app з deploy-ключем лише на читання.

Два скрипти

/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 МБ памʼяті, щоб одна гілка, що пішла врознос, не поклала решту. А повторний запуск тієї самої гілки перезбирає її на місці: push, 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 вистачить одного рядка у файлі інструкцій проєкту:

Після push гілки виконай ssh deploy@preview.example.com up <branch> і додай виведену адресу до свого підсумку. Виконай down, коли гілку змерджено або покинуто.

Якщо ваш CI вже запускається на кожен push, викликайте ту саму команду звідти й тримайте ключ повністю поза оточенням агента. Це суворіший варіант, і ми обрали б його для всього, що виходить за межі особистого проєкту.

На що зважати

  • Диск. Кожна збірка лишає шари образів. Щотижневий cron із docker image prune -f тримає це під контролем; down уже видаляє власний образ превʼю.
  • Гілки, які ніхто не закриває. Агенти відкривають більше гілок, ніж люди. Cron-завдання, яке перелічує контейнери preview-*, старші за тиждень, і запускає для них preview-down, позбавить вас двадцяти забутих превʼю.
  • Секрети. Превʼю мають працювати з тестовими обліковими даними. Пароль basic auth не пускає сторонніх, але не робить прийнятним підключати превʼю до продакшн-бази.
  • Час збирання. Образи збираються на сервері, тож важка збірка конкурує за CPU з робочими превʼю. Якщо збирання триває хвилини, збирайте в CI й тягніть готовий образ.

Сам агент не мусить жити на цьому сервері. Якщо живе — дивіться VPS для Claude Code, а щоб він сам замовляв і доглядав цей сервер, підключіть його до MCP-сервера EQVPS. Та сама машина може тримати й MCP-шлюз вашої команди. Для запуску недовіреного коду взагалі без сервера є пісочниці, а тарифи з власним IPv4 — від $8 на місяць.

Часті питання

Чому не дати агенту деплоїти просто в продакшн?

Бо «тести проходять» і «працює» — не одне й те саме, а агент має вирішувати це останнім. Превʼю дає людині що відкрити ще до того, як щось потрапить до користувачів, ціною однієї додаткової команди.

Який тариф підійде?

Залежить від того, скільки превʼю працює одночасно. AI-Agent-IP ($15/міс., 4 vCPU, 4 ГБ RAM, 40 ГБ диска) спокійно вміщує кілька невеликих вебзастосунків з лімітом 512 МБ кожен. Найважче — збірки Docker, тож за великих образів стежте за диском і памʼяттю.

Потрібен wildcard-сертифікат?

Ні. Caddy сам отримує окремий сертифікат для кожного піддомену превʼю. Wildcard лише DNS-запис. Let's Encrypt дозволяє близько 50 нових сертифікатів на зареєстрований домен на тиждень, цього вистачить, якщо агент не створює сотні гілок.

Чи може агент зламати сервер своїм SSH-ключем?

Цим ключем — ні. Його обмежено однією командою, яка приймає лише «up <гілка>» або «down <гілка>» із безпечною назвою гілки. Shell, зайві аргументи й прокидання портів відхиляються.

Чи працюватиме це на NAT-тарифі?

Ні. Превʼю мають бути доступні на портах 80 і 443 на власних піддоменах, а для цього потрібен публічний IPv4. NAT-тарифи прокидають лише порт SSH.

Коментарі

Поки немає коментарів. Будьте першим.

Залишити коментар

Коментарі проходять модерацію перед публікацією.