−25%

на годовую оплату Windows, до 31.10. К тарифам

EQVPS
Начать

VPS для превью coding-агента: у каждой ветки, которую пушит агент, свой адрес

Coding-агент закончил ветку и говорит, что всё работает. Дайте ему сервер, где это можно проверить: каждая ветка собирается в контейнер и открывается на своём HTTPS-поддомене за паролем, а SSH-ключ агента не умеет ничего, кроме запуска и остановки превью.

Coding-агент говорит, что фича работает. Может, и правда. Тесты, которые он сам написал, проходят, а это доказывает меньше, чем всем хотелось бы. На самом деле нужно открыть результат в браузере, прежде чем он доберётся до пользователей, и при этом не подпускать агента к проду.

Это решают превью-окружения, и платформа для них не нужна. Один VPS со своим IPv4, Docker, Caddy и два коротких скрипта дают каждой ветке свой адрес вроде https://feature-login-form.preview.example.com — за паролем и с пересборкой на каждый пуш. Агент получает 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 МБ, и одна сошедшая с ума ветка не уронит остальные. А повторный запуск той же ветки пересобирает её на месте: запушили, сделали 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 <ветка> и вставь выведенный адрес в итог. Когда ветку слили или бросили, выполни down.

Если CI уже срабатывает на пуш, вызывайте ту же команду оттуда, а ключ вообще не давайте агенту. Это строже, и для всего серьёзнее личного проекта мы бы выбрали этот вариант.

За чем следить

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

Самому агенту жить на этом сервере не обязательно. Если хотите, чтобы жил, — см. VPS для Claude Code, а чтобы он сам заказывал и обслуживал этот сервер, подключите его к MCP-серверу EQVPS. На той же машине можно держать и MCP-шлюз команды. Чтобы запускать недоверенный код вообще без сервера, есть песочницы, а тарифы со своим IPv4 начинаются от $8 в месяц.

Готовы развернуть? Оплата криптой, без KYC — сервер за минуту.

Развернуть →

Частые вопросы

Почему бы не дать агенту выкатывать сразу в прод?

Потому что «тесты проходят» и «работает» — не одно и то же, а решать это агенту последнему. Превью даёт человеку что-то, по чему можно кликнуть, прежде чем изменения увидят пользователи. Цена — одна лишняя команда.

Какой тариф подойдёт?

Зависит от того, сколько превью работает одновременно. 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.

Комментарии

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

Оставить комментарий

Комментарии проходят модерацию перед публикацией.