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 в месяц.
Комментарии
Пока нет комментариев. Будьте первым.