−25%

no Windows com pagamento anual, até 31/10. Ver planos

EQVPS
Começar

VPS para prévias de agentes de código: cada branch enviado ganha sua própria URL

Um agente de código termina um branch e diz que funciona. Dê a ele um servidor onde isso possa ser conferido: cada branch vira um contêiner e abre num subdomínio HTTPS próprio atrás de uma senha, e a chave SSH do agente só consegue iniciar e parar prévias.

Um agente de código diz que a funcionalidade funciona. Talvez funcione. Os testes que ele escreveu passam, o que prova menos do que todo mundo gostaria. O que você quer mesmo é abrir aquilo no navegador antes que chegue perto dos usuários, e quer fazer isso sem pedir ao agente que mexa em produção.

Ambientes de prévia resolvem isso, e você não precisa de uma plataforma. Um VPS com IPv4 própria, Docker, Caddy e dois scripts curtos dão a cada branch sua própria URL, como https://feature-login-form.preview.example.com, atrás de uma senha e reconstruída a cada push. O agente recebe uma chave SSH que inicia e para prévias e não faz mais nada.

Montamos isso num servidor de testes e passamos pelo ciclo inteiro antes de escrever: criar, abrir, fazer push de novo, derrubar, tentar abusar da chave.

O que você precisa

  • Um VPS com IPv4 pública. O AI-Agent-IP tem espaço para várias prévias ao mesmo tempo.
  • Um registro DNS wildcard: *.preview.example.com → o IP do servidor.
  • O repositório da sua app com um Dockerfile que sirva na porta 8080. Qualquer linguagem serve.
  • Ubuntu 24.04 ou Debian 12/13 com apt install -y docker.io caddy git.

Clone o repositório uma vez em /srv/app, com uma deploy key só de leitura.

Os dois scripts

/usr/local/bin/preview-up faz o build de um branch e coloca no ar:

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

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

Algumas escolhas ali são de propósito. Os contêineres publicam só em 127.0.0.1 com porta aleatória, então nada chega a eles a não ser pelo Caddy. Isso importa porque as portas publicadas pelo Docker passam por cima do ufw. Cada prévia tem limite de 512 MB de memória, para um branch descontrolado não derrubar os outros. E rodar o mesmo branch de novo reconstrói no lugar: push, up, recarregar.

Caddy: um arquivo por prévia, uma senha para todas

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'

Coloque o hash em /etc/caddy/Caddyfile:

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

Depois, systemctl reload caddy. Toda prévia passa a ficar atrás do mesmo login, então um branch pela metade com saída de debug não fica aberto na internet. Em versões mais novas do Caddy a diretiva também se escreve basic_auth; basicauth continua funcionando nas duas.

A chave do agente: up e down, nada mais

O agente precisa disparar prévias, não ser dono do servidor. Crie um usuário deploy e um script de controle que aceite exatamente dois comandos:

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

Em /home/deploy/.ssh/authorized_keys, amarre a chave pública do agente a esse script:

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

Da máquina onde o agente trabalha:

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

No nosso teste, up fez o build e publicou o branch, um segundo up depois de um commit novo serviu a nova versão, e down removeu o contêiner e o site. Tentar cat /etc/shadow, up x; id, nomes de branch como -x ou a/../b, ou um login interativo com essa chave só rendeu usage: up|down <branch>. O nome do branch precisa começar com letra ou dígito e não pode conter ...

Ligando ao agente

Com Claude Code ou Codex, basta uma linha no arquivo de instruções do projeto:

Depois de fazer push de um branch, rode ssh deploy@preview.example.com up <branch> e coloque a URL impressa no seu resumo. Rode down quando o branch for mesclado ou abandonado.

Se o seu CI já roda a cada push, chame o mesmo comando de lá e deixe a chave totalmente fora do ambiente do agente. É a opção mais rígida, e a escolheríamos para qualquer coisa além de um projeto pessoal.

O que observar

  • Disco. Cada build deixa camadas de imagem. Um cron semanal com docker image prune -f mantém isso sob controle; down já remove a imagem da própria prévia.
  • Branches que ninguém fecha. Agentes abrem mais branches do que pessoas. Um cron que liste contêineres preview-* com mais de uma semana e rode preview-down neles poupa você de achar vinte prévias esquecidas.
  • Segredos. As prévias devem rodar com credenciais de teste. A senha do basic auth deixa estranhos de fora; não torna aceitável apontar uma prévia para o banco de produção.
  • Tempo de build. As imagens são construídas no servidor, então um build pesado disputa CPU com as prévias em execução. Se os builds levam minutos, faça o build no CI e baixe a imagem.

O agente em si não precisa morar neste servidor. Se morar, veja VPS para Claude Code, e para ele pedir e cuidar desse servidor sozinho, conecte-o ao servidor MCP da EQVPS. A mesma máquina também pode hospedar o gateway MCP da sua equipe. Para rodar código não confiável sem servidor nenhum existem os sandboxes, e os planos com IPv4 própria começam em $8 por mês.

FAQ

Por que não deixar o agente fazer deploy direto em produção?

Porque "os testes passam" e "funciona" não são a mesma coisa, e o agente é o último que deveria decidir isso. Uma prévia dá a uma pessoa algo para clicar antes que qualquer coisa chegue aos usuários, ao custo de um comando a mais.

Qual plano serve?

Depende de quantas prévias rodam ao mesmo tempo. O AI-Agent-IP ($15/mês, 4 vCPU, 4 GB de RAM, 40 GB de disco) aguenta com folga várias apps web pequenas com limite de 512 MB cada. Os builds do Docker são a parte mais pesada, então fique de olho no disco e na memória se suas imagens forem grandes.

Preciso de certificado wildcard?

Não. O Caddy obtém sozinho um certificado separado para cada subdomínio de prévia. Só o registro DNS é wildcard. O Let's Encrypt permite cerca de 50 certificados novos por domínio registrado por semana, mais que suficiente, a menos que o agente crie centenas de branches.

O agente pode quebrar o servidor com a chave SSH?

Não com essa chave. Ela fica presa a um único comando que só aceita "up <branch>" ou "down <branch>" com um nome de branch seguro. Acesso a shell, argumentos extras e encaminhamento de portas são recusados.

Funciona num plano NAT?

Não. As prévias precisam responder nas portas 80 e 443 em subdomínios próprios, o que exige IPv4 pública. Planos NAT só encaminham a porta SSH.

Comentários

Nenhum comentário ainda. Seja o primeiro.

Deixe um comentário

Os comentários são moderados antes de aparecerem.