Un agent de code affirme que la fonctionnalité marche. Peut-être bien. Les tests qu'il a écrits passent, ce qui prouve moins qu'on ne le voudrait. Ce que vous voulez vraiment, c'est ouvrir le résultat dans un navigateur avant qu'il n'approche des utilisateurs, sans demander à l'agent de toucher à la production.
Les environnements d'aperçu règlent cela, et il n'y a pas besoin d'une plateforme. Un VPS avec IPv4 propre, Docker, Caddy et deux courts scripts donnent à chaque branche sa propre URL, comme https://feature-login-form.preview.example.com, derrière un mot de passe, reconstruite à chaque push. L'agent reçoit une clé SSH qui peut démarrer et arrêter des aperçus, et rien d'autre.
Nous avons monté cela sur un serveur de test et parcouru tout le cycle avant de l'écrire : créer, ouvrir, pousser à nouveau, démonter, tenter d'abuser de la clé.
Ce qu'il vous faut
- Un VPS avec IPv4 publique. AI-Agent-IP a de la place pour plusieurs aperçus simultanés.
- Un enregistrement DNS wildcard :
*.preview.example.com→ l'IP du serveur. - Le dépôt de votre application avec un
Dockerfilequi sert sur le port 8080. N'importe quel langage convient. - Ubuntu 24.04 ou Debian 12/13 avec
apt install -y docker.io caddy git.
Clonez le dépôt une fois dans /srv/app, avec une clé de déploiement en lecture seule.
Les deux scripts
/usr/local/bin/preview-up construit une branche et la met en ligne :
#!/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 supprime :
#!/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
Certains choix sont délibérés. Les conteneurs ne publient que sur 127.0.0.1 avec un port aléatoire, donc rien ne les atteint sauf via Caddy. C'est important, car les ports publiés par Docker contournent ufw. Chaque aperçu a une limite mémoire de 512 Mo, pour qu'une branche emballée n'emporte pas les autres. Et relancer la même branche la reconstruit sur place : push, up, rafraîchir.
Caddy : un fichier par aperçu, un mot de passe pour tous
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'
Mettez le hash dans /etc/caddy/Caddyfile :
(preview_common) {
basicauth {
reviewer PASTE-THE-HASH-HERE
}
}
import /etc/caddy/previews/*.caddy
Puis systemctl reload caddy. Tous les aperçus sont désormais derrière le même identifiant, donc une branche à moitié finie avec sa sortie de débogage n'est pas exposée sur Internet. Dans les versions récentes de Caddy, la directive s'écrit aussi basic_auth ; basicauth fonctionne toujours dans les deux.
La clé de l'agent : up et down, rien d'autre
L'agent doit pouvoir déclencher des aperçus, pas posséder le serveur. Créez un utilisateur deploy et un script de contrôle qui accepte exactement deux commandes :
#!/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
Dans /home/deploy/.ssh/authorized_keys, liez la clé publique de l'agent à ce script :
restrict,command="/usr/local/bin/preview-gate" ssh-ed25519 AAAA... coding-agent
Depuis la machine où travaille l'agent :
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
Lors de notre test, up a construit et publié la branche, un second up après un nouveau commit a servi la nouvelle version, et down a supprimé le conteneur et le site. Les tentatives avec cat /etc/shadow, up x; id, des noms de branche comme -x ou a/../b, ou une connexion interactive avec cette clé n'ont obtenu que usage: up|down <branch>. Un nom de branche doit commencer par une lettre ou un chiffre et ne peut pas contenir ...
Brancher cela sur l'agent
Avec Claude Code ou Codex, une ligne dans le fichier d'instructions du projet suffit :
Après avoir poussé une branche, exécute
ssh deploy@preview.example.com up <branch>et ajoute l'URL affichée à ton résumé. Exécutedownquand la branche est fusionnée ou abandonnée.
Si votre CI tourne déjà à chaque push, appelez la même commande depuis la CI et gardez entièrement la clé hors de l'environnement de l'agent. C'est l'option la plus stricte, et nous la choisirions pour tout ce qui dépasse un projet personnel.
Points à surveiller
- Disque. Chaque build laisse des couches d'image. Un cron hebdomadaire avec
docker image prune -fgarde cela sous contrôle ;downsupprime déjà l'image propre à l'aperçu. - Branches que personne ne ferme. Les agents ouvrent plus de branches que les humains. Un cron qui liste les conteneurs
preview-*de plus d'une semaine et lancepreview-downdessus vous évite de retrouver vingt aperçus oubliés. - Secrets. Les aperçus doivent tourner avec des identifiants de test. Le mot de passe basic auth tient les inconnus à l'écart ; il ne rend pas acceptable de brancher un aperçu sur la base de production.
- Temps de build. Les images sont construites sur le serveur, donc un build lourd dispute le CPU aux aperçus en cours. Si les builds prennent des minutes, construisez dans la CI et récupérez l'image.
L'agent lui-même n'a pas besoin de vivre sur ce serveur. S'il y vit, voyez VPS pour Claude Code, et pour qu'il commande et entretienne ce serveur lui-même, connectez-le au serveur MCP d'EQVPS. La même machine peut aussi héberger la passerelle MCP de votre équipe. Pour exécuter du code non fiable sans aucun serveur, il y a les sandboxes, et les offres avec IPv4 propre commencent à $8 par mois.
Commentaires
Pas encore de commentaires. Soyez le premier.