−25%

la planurile Windows anuale, până pe 31 oct. Vezi planurile

EQVPS
Începe

VPS pentru previzualizările agenților de cod: fiecare ramură publicată are propriul URL

Un agent de cod termină o ramură și spune că merge. Dă-i un server unde se poate verifica: fiecare ramură e construită într-un container și se deschide pe propriul subdomeniu HTTPS în spatele unei parole, iar cheia SSH a agentului poate doar porni și opri previzualizări.

Un agent de cod spune că funcționalitatea merge. Poate că da. Testele pe care le-a scris trec, iar asta dovedește mai puțin decât ne-am dori. Ce vrei de fapt e să deschizi rezultatul în browser înainte să ajungă aproape de utilizatori, fără să-i ceri agentului să atingă producția.

Mediile de previzualizare rezolvă asta și nu ai nevoie de o platformă pentru ele. Un VPS cu IPv4 propriu, Docker, Caddy și două scripturi scurte dau fiecărei ramuri propriul URL, de exemplu https://feature-login-form.preview.example.com, în spatele unei parole și reconstruit la fiecare push. Agentul primește o cheie SSH care poate porni și opri previzualizări și nimic altceva.

Am construit asta pe un server de test și am parcurs tot ciclul înainte să-l descriem: creare, deschidere, push din nou, demontare, încercare de abuz al cheii.

Ce îți trebuie

  • Un VPS cu IPv4 public. AI-Agent-IP are loc pentru mai multe previzualizări simultan.
  • O înregistrare DNS wildcard: *.preview.example.com → IP-ul serverului.
  • Depozitul aplicației cu un Dockerfile care servește pe portul 8080. Orice limbaj merge.
  • Ubuntu 24.04 sau Debian 12/13 cu apt install -y docker.io caddy git.

Clonează depozitul o singură dată în /srv/app, cu o cheie de deploy doar pentru citire.

Cele două scripturi

/usr/local/bin/preview-up construiește o ramură și o pune online:

#!/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 o elimină:

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

Câteva alegeri sunt intenționate. Containerele publică doar pe 127.0.0.1 cu un port aleatoriu, deci nimic nu ajunge la ele decât prin Caddy. Contează, pentru că porturile publicate de Docker ocolesc ufw. Fiecare previzualizare are o limită de memorie de 512 MB, ca o ramură scăpată de sub control să nu le doboare pe celelalte. Iar rularea aceleiași ramuri din nou o reconstruiește pe loc: push, up, reîmprospătare.

Caddy: un fișier pe previzualizare, o parolă pentru toate

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'

Pune hash-ul în /etc/caddy/Caddyfile:

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

Apoi systemctl reload caddy. Fiecare previzualizare stă acum în spatele aceluiași login, așa că o ramură neterminată cu output de depanare nu stă deschisă pe internet. În versiunile mai noi de Caddy directiva se scrie și basic_auth; basicauth funcționează în continuare în ambele.

Cheia agentului: up și down, nimic altceva

Agentul trebuie să declanșeze previzualizări, nu să dețină serverul. Creează un utilizator deploy și un script de control care acceptă exact două comenzi:

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

În /home/deploy/.ssh/authorized_keys, leagă cheia publică a agentului de acel script:

restrict,command="/usr/local/bin/preview-gate" ssh-ed25519 AAAA... coding-agent

De pe mașina pe care lucrează agentul:

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

În testul nostru, up a construit și publicat ramura, un al doilea up după un commit nou a servit versiunea nouă, iar down a eliminat containerul și site-ul. Încercările cu cat /etc/shadow, up x; id, nume de ramuri ca -x sau a/../b ori o autentificare interactivă cu acea cheie au primit doar usage: up|down <branch>. Numele ramurii trebuie să înceapă cu o literă sau o cifră și nu poate conține ...

Conectarea la agent

Cu Claude Code sau Codex ajunge un rând în fișierul de instrucțiuni al proiectului:

După ce faci push la o ramură, rulează ssh deploy@preview.example.com up <branch> și pune URL-ul afișat în rezumatul tău. Rulează down când ramura e unită sau abandonată.

Dacă CI-ul tău rulează deja la fiecare push, apelează aceeași comandă de acolo și ține cheia complet în afara mediului agentului. E varianta mai strictă și am alege-o pentru orice dincolo de un proiect personal.

La ce să fii atent

  • Disc. Fiecare build lasă straturi de imagine. Un cron săptămânal cu docker image prune -f ține lucrurile sub control; down șterge deja imaginea proprie a previzualizării.
  • Ramuri pe care nu le închide nimeni. Agenții deschid mai multe ramuri decât oamenii. Un cron care listează containerele preview-* mai vechi de o săptămână și rulează preview-down pe ele te scutește de douăzeci de previzualizări uitate.
  • Secrete. Previzualizările ar trebui să ruleze cu date de test. Parola basic auth îi ține pe străini afară, dar nu face acceptabil să legi o previzualizare de baza de date de producție.
  • Timp de build. Imaginile se construiesc pe server, deci un build greu concurează pentru CPU cu previzualizările active. Dacă buildurile durează minute, construiește în CI și descarcă imaginea.

Agentul nu trebuie să locuiască pe acest server. Dacă locuiește, vezi VPS pentru Claude Code, iar ca să comande și să îngrijească singur serverul, conectează-l la serverul MCP EQVPS. Aceeași mașină poate găzdui și gateway-ul MCP al echipei. Pentru cod nesigur fără niciun server există sandboxurile, iar planurile cu IPv4 propriu pornesc de la $8 pe lună.

Întrebări frecvente

De ce să nu lași agentul să facă deploy direct în producție?

Pentru că „testele trec” și „funcționează” nu sunt același lucru, iar agentul ar trebui să fie ultimul care decide asta. O previzualizare îi dă unui om ceva pe care să dea clic înainte ca ceva să ajungă la utilizatori, cu prețul unei singure comenzi în plus.

Ce plan se potrivește?

Depinde câte previzualizări rulează simultan. AI-Agent-IP ($15/lună, 4 vCPU, 4 GB RAM, 40 GB disc) duce fără probleme mai multe aplicații web mici cu o limită de 512 MB fiecare. Buildurile Docker sunt partea cea mai grea, deci urmărește discul și memoria dacă imaginile sunt mari.

Am nevoie de un certificat wildcard?

Nu. Caddy obține singur un certificat separat pentru fiecare subdomeniu de previzualizare. Doar înregistrarea DNS e wildcard. Let's Encrypt permite circa 50 de certificate noi pe domeniu înregistrat pe săptămână, suficient dacă agentul nu creează sute de ramuri.

Poate agentul să strice serverul cu cheia lui SSH?

Nu cu acea cheie. E limitată la o singură comandă care acceptă doar „up <ramură>” sau „down <ramură>” cu un nume de ramură sigur. Accesul la shell, argumentele suplimentare și redirecționarea porturilor sunt refuzate.

Merge pe un plan NAT?

Nu. Previzualizările trebuie să fie accesibile pe porturile 80 și 443 pe propriile subdomenii, ceea ce cere un IPv4 public. Planurile NAT redirecționează doar portul SSH.

Comentarii

Încă nu există comentarii. Fii primul.

Lasă un comentariu

Comentariile sunt moderate înainte să apară.