−25%

en Windows con pago anual, hasta el 31/10. Ver planes

EQVPS
Empezar

VPS para previsualizaciones de agentes de código: cada rama tiene su propia URL

Un agente de código termina una rama y dice que funciona. Dale un servidor donde comprobarlo: cada rama se construye en un contenedor y se abre en su propio subdominio HTTPS tras una contraseña, y la clave SSH del agente solo puede arrancar y parar previsualizaciones.

Un agente de código dice que la funcionalidad va. Puede que sí. Los tests que escribió pasan, y eso demuestra menos de lo que a todos nos gustaría. Lo que de verdad quieres es abrirlo en el navegador antes de que se acerque a los usuarios, y quieres hacerlo sin pedirle al agente que toque producción.

Los entornos de previsualización resuelven eso, y no necesitas una plataforma. Un VPS con IPv4 propia, Docker, Caddy y dos scripts cortos dan a cada rama su propia URL, como https://feature-login-form.preview.example.com, tras una contraseña y reconstruida en cada push. El agente recibe una clave SSH que puede arrancar y parar previsualizaciones y nada más.

Lo montamos en un servidor de pruebas y recorrimos el ciclo completo antes de escribirlo: crear, abrir, volver a hacer push, desmontar e intentar abusar de la clave.

Qué necesitas

  • Un VPS con IPv4 pública. AI-Agent-IP tiene sitio para varias previsualizaciones a la vez.
  • Un registro DNS wildcard: *.preview.example.com → la IP del servidor.
  • El repositorio de tu app con un Dockerfile que sirva en el puerto 8080. Vale cualquier lenguaje.
  • Ubuntu 24.04 o Debian 12/13 con apt install -y docker.io caddy git.

Clona el repositorio una vez en /srv/app con una deploy key de solo lectura.

Los dos scripts

/usr/local/bin/preview-up construye una rama y la publica:

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

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

Algunas decisiones son deliberadas. Los contenedores publican solo en 127.0.0.1 con un puerto aleatorio, así que nada les llega salvo a través de Caddy. Eso importa porque los puertos publicados de Docker se saltan ufw. Cada previsualización tiene un límite de memoria de 512 MB, para que una rama desbocada no tumbe a las demás. Y volver a lanzar la misma rama la reconstruye en el sitio: push, up, recargar.

Caddy: un archivo por previsualización, una contraseña 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'

Pon el hash en /etc/caddy/Caddyfile:

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

Después, systemctl reload caddy. Todas las previsualizaciones quedan tras el mismo login, así que una rama a medio hacer con salida de depuración no está abierta en internet. En versiones recientes de Caddy la directiva también se escribe basic_auth; basicauth sigue funcionando en ambas.

La clave del agente: up y down, nada más

El agente necesita lanzar previsualizaciones, no ser dueño del servidor. Crea un usuario deploy y un script de control que acepte exactamente dos 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

En /home/deploy/.ssh/authorized_keys, ata la clave pública del agente a ese script:

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

Desde la máquina donde trabaja el 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

En nuestra prueba, up construyó y publicó la rama, un segundo up tras un commit nuevo sirvió la versión nueva, y down eliminó el contenedor y el sitio. Probar cat /etc/shadow, up x; id, nombres de rama como -x o a/../b, o un inicio de sesión interactivo con esa clave solo obtuvo usage: up|down <branch>. Un nombre de rama debe empezar por una letra o un dígito y no puede contener ...

Conectarlo al agente

Con Claude Code o Codex basta una línea en el archivo de instrucciones del proyecto:

Después de hacer push de una rama, ejecuta ssh deploy@preview.example.com up <branch> y pon la URL impresa en tu resumen. Ejecuta down cuando la rama se fusione o se abandone.

Si tu CI ya se ejecuta en cada push, llama al mismo comando desde ahí y deja la clave totalmente fuera del entorno del agente. Es la opción más estricta, y la elegiríamos para cualquier cosa que vaya más allá de un proyecto personal.

Qué vigilar

  • Disco. Cada build deja capas de imagen. Un cron semanal con docker image prune -f lo mantiene a raya; down ya elimina la imagen propia de la previsualización.
  • Ramas que nadie cierra. Los agentes abren más ramas que las personas. Un cron que liste los contenedores preview-* de más de una semana y ejecute preview-down sobre ellos te ahorra encontrarte veinte previsualizaciones olvidadas.
  • Secretos. Las previsualizaciones deben usar credenciales de prueba. La contraseña de basic auth deja fuera a los desconocidos, pero no hace aceptable apuntar una previsualización a la base de datos de producción.
  • Tiempo de build. Las imágenes se construyen en el servidor, así que un build pesado compite por CPU con las previsualizaciones en marcha. Si los builds tardan minutos, construye en el CI y descarga la imagen.

El agente no tiene por qué vivir en este servidor. Si lo hace, mira VPS para Claude Code, y para que pida y cuide ese servidor por su cuenta, conéctalo al servidor MCP de EQVPS. La misma máquina puede alojar también el gateway MCP de tu equipo. Para ejecutar código no confiable sin servidor alguno existen los sandboxes, y los planes con IPv4 propia empiezan en $8 al mes.

Preguntas frecuentes

¿Por qué no dejar que el agente despliegue directamente en producción?

Porque «los tests pasan» y «funciona» no son lo mismo, y el agente es el último que debería decidirlo. Una previsualización da a una persona algo que abrir antes de que nada llegue a los usuarios, a cambio de un comando más.

¿Qué plan encaja?

Depende de cuántas previsualizaciones haya a la vez. AI-Agent-IP ($15/mes, 4 vCPU, 4 GB de RAM, 40 GB de disco) aguanta sin problema varias apps web pequeñas con un límite de 512 MB cada una. Los builds de Docker son lo más pesado, así que vigila el disco y la memoria si tus imágenes son grandes.

¿Necesito un certificado wildcard?

No. Caddy obtiene por su cuenta un certificado para cada subdominio de previsualización. Solo el registro DNS es wildcard. Let's Encrypt permite unos 50 certificados nuevos por dominio registrado y semana, de sobra salvo que el agente cree cientos de ramas.

¿Puede el agente romper el servidor con su clave SSH?

No con esa clave. Está limitada a un único comando que solo acepta «up <rama>» o «down <rama>» con un nombre de rama seguro. El acceso a shell, los argumentos extra y el reenvío de puertos se rechazan.

¿Funciona en un plan NAT?

No. Las previsualizaciones deben ser accesibles en los puertos 80 y 443 en sus propios subdominios, lo que requiere una IPv4 pública. Los planes NAT solo reenvían el puerto SSH.

Comentarios

Aún no hay comentarios. Sé el primero.

Deja un comentario

Los comentarios se moderan antes de aparecer.