−25%

su Windows con pagamento annuale, fino al 31/10. Vai ai piani

EQVPS
Inizia

VPS per le anteprime degli agenti di coding: ogni branch pubblicato ha il suo URL

Un agente di coding finisce un branch e dice che funziona. Dagli un server dove verificarlo: ogni branch viene costruito in un container e si apre su un proprio sottodominio HTTPS dietro password, e la chiave SSH dell'agente può solo avviare e fermare anteprime.

Un agente di coding dice che la funzionalità va. Forse è vero. I test che ha scritto passano, e questo dimostra meno di quanto tutti vorrebbero. Quello che vuoi davvero è aprirla nel browser prima che si avvicini agli utenti, e vuoi farlo senza chiedere all'agente di toccare la produzione.

Gli ambienti di anteprima risolvono il problema, e non serve una piattaforma. Un VPS con IPv4 propria, Docker, Caddy e due brevi script danno a ogni branch il suo URL, come https://feature-login-form.preview.example.com, dietro password e ricostruito a ogni push. L'agente riceve una chiave SSH che può avviare e fermare anteprime e nient'altro.

L'abbiamo costruito su un server di test e percorso tutto il ciclo prima di scriverlo: creare, aprire, pushare di nuovo, smontare, provare ad abusare della chiave.

Cosa ti serve

  • Un VPS con IPv4 pubblica. AI-Agent-IP ha spazio per più anteprime insieme.
  • Un record DNS wildcard: *.preview.example.com → l'IP del server.
  • Il repository della tua app con un Dockerfile che serve sulla porta 8080. Va bene qualsiasi linguaggio.
  • Ubuntu 24.04 o Debian 12/13 con apt install -y docker.io caddy git.

Clona il repository una volta in /srv/app, con una deploy key in sola lettura.

I due script

/usr/local/bin/preview-up costruisce un branch e lo mette 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 lo rimuove:

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

Alcune scelte sono intenzionali. I container pubblicano solo su 127.0.0.1 con porta casuale, quindi niente li raggiunge se non tramite Caddy. Conta perché le porte pubblicate da Docker scavalcano ufw. Ogni anteprima ha un limite di memoria di 512 MB, così un branch fuori controllo non abbatte gli altri. E rilanciare lo stesso branch lo ricostruisce sul posto: push, up, ricarica.

Caddy: un file per anteprima, una password per tutte

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'

Metti l'hash in /etc/caddy/Caddyfile:

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

Poi systemctl reload caddy. Ogni anteprima ora sta dietro lo stesso login, quindi un branch a metà con l'output di debug non è esposto su internet. Nelle versioni più recenti di Caddy la direttiva si scrive anche basic_auth; basicauth funziona ancora in entrambe.

La chiave dell'agente: up e down, nient'altro

L'agente deve poter lanciare anteprime, non possedere il server. Crea un utente deploy e uno script di controllo che accetti esattamente due comandi:

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

In /home/deploy/.ssh/authorized_keys, lega la chiave pubblica dell'agente a quello script:

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

Dalla macchina dove lavora l'agente:

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

Nel nostro test, up ha costruito e pubblicato il branch, un secondo up dopo un nuovo commit ha servito la nuova versione e down ha rimosso container e sito. I tentativi con cat /etc/shadow, up x; id, nomi di branch come -x o a/../b, o un login interattivo con quella chiave hanno ottenuto solo usage: up|down <branch>. Un nome di branch deve iniziare con una lettera o una cifra e non può contenere ...

Collegarlo all'agente

Con Claude Code o Codex basta una riga nel file di istruzioni del progetto:

Dopo aver pushato un branch, esegui ssh deploy@preview.example.com up <branch> e metti l'URL stampato nel tuo riepilogo. Esegui down quando il branch viene unito o abbandonato.

Se la tua CI gira già a ogni push, chiama lo stesso comando da lì e tieni la chiave del tutto fuori dall'ambiente dell'agente. È l'opzione più rigorosa, e la sceglieremmo per qualsiasi cosa vada oltre un progetto personale.

Cosa tenere d'occhio

  • Disco. Ogni build lascia layer di immagine. Un cron settimanale con docker image prune -f tiene la situazione sotto controllo; down rimuove già l'immagine propria dell'anteprima.
  • Branch che nessuno chiude. Gli agenti aprono più branch delle persone. Un cron che elenca i container preview-* più vecchi di una settimana ed esegue preview-down su di essi ti evita di trovare venti anteprime dimenticate.
  • Segreti. Le anteprime dovrebbero girare con credenziali di test. La password basic auth tiene fuori gli estranei, ma non rende accettabile puntare un'anteprima sul database di produzione.
  • Tempo di build. Le immagini vengono costruite sul server, quindi un build pesante si contende la CPU con le anteprime attive. Se i build durano minuti, costruisci in CI e scarica l'immagine.

L'agente non deve per forza vivere su questo server. Se lo fa, vedi VPS per Claude Code, e per lasciargli ordinare e curare da solo quel server, collegalo al server MCP di EQVPS. La stessa macchina può ospitare anche il gateway MCP del tuo team. Per eseguire codice non affidabile senza alcun server ci sono le sandbox, e i piani con IPv4 propria partono da $8 al mese.

FAQ

Perché non lasciare che l'agente faccia deploy direttamente in produzione?

Perché «i test passano» e «funziona» non sono la stessa cosa, e l'agente è l'ultimo che dovrebbe deciderlo. Un'anteprima dà a una persona qualcosa da aprire prima che qualcosa arrivi agli utenti, al costo di un comando in più.

Quale piano va bene?

Dipende da quante anteprime girano insieme. AI-Agent-IP ($15/mese, 4 vCPU, 4 GB di RAM, 40 GB di disco) regge senza problemi diverse piccole web app con un limite di 512 MB ciascuna. I build Docker sono la parte più pesante, quindi tieni d'occhio disco e memoria se le immagini sono grandi.

Serve un certificato wildcard?

No. Caddy ottiene da solo un certificato per ogni sottodominio di anteprima. Solo il record DNS è wildcard. Let's Encrypt consente circa 50 nuovi certificati per dominio registrato a settimana, più che sufficienti a meno che l'agente non crei centinaia di branch.

L'agente può rompere il server con la sua chiave SSH?

Non con quella chiave. È limitata a un unico comando che accetta solo «up <branch>» o «down <branch>» con un nome di branch sicuro. Accesso shell, argomenti extra e port forwarding vengono rifiutati.

Funziona su un piano NAT?

No. Le anteprime devono essere raggiungibili sulle porte 80 e 443 ai propri sottodomini, il che richiede un IPv4 pubblico. I piani NAT inoltrano solo la porta SSH.

Commenti

Ancora nessun commento. Sii il primo.

Lascia un commento

I commenti sono moderati prima di comparire.