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
Dockerfileque 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. Rodedownquando 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 -fmantém isso sob controle;downjá 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 rodepreview-downneles 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.
Comentários
Nenhum comentário ainda. Seja o primeiro.