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
Dockerfilecare 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ădowncâ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-downpe 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ă.
Comentarii
Încă nu există comentarii. Fii primul.