En kodeagent siger, at funktionen virker. Måske gør den. Testene, den skrev, består, og det beviser mindre, end alle gerne ville. Det, du egentlig vil, er at åbne det i en browser, før det kommer i nærheden af brugerne, og det uden at bede agenten om at røre produktionen.
Forhåndsvisningsmiljøer løser det, og du behøver ingen platform til dem. Én VPS med egen IPv4, Docker, Caddy og to korte scripts giver hver gren sin egen URL som https://feature-login-form.preview.example.com, bag en adgangskode og genbygget ved hvert push. Agenten får en SSH-nøgle, der kan starte og stoppe forhåndsvisninger og intet andet.
Vi byggede det på en testserver og kørte hele forløbet igennem, før vi skrev det ned: oprette, åbne, pushe igen, nedlægge, forsøge at misbruge nøglen.
Det skal du bruge
- En VPS med offentlig IPv4. AI-Agent-IP har plads til flere forhåndsvisninger ad gangen.
- En wildcard-DNS-record:
*.preview.example.com→ serverens IP. - Din apps repository med en
Dockerfile, der serverer på port 8080. Ethvert sprog virker. - Ubuntu 24.04 eller Debian 12/13 med
apt install -y docker.io caddy git.
Klon repositoryet én gang til /srv/app med en deploy-nøgle med kun læseadgang.
De to scripts
/usr/local/bin/preview-up bygger en gren og lægger den 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 fjerner den:
#!/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
Et par valg er bevidste. Containerne publicerer kun på 127.0.0.1 med en tilfældig port, så intet når dem undtagen gennem Caddy. Det betyder noget, fordi porte, som Docker publicerer, går uden om ufw. Hver forhåndsvisning har en hukommelsesgrænse på 512 MB, så én løbsk gren ikke tager de andre med sig. Og at køre den samme gren igen genbygger den på stedet: push, kør up, genindlæs.
Caddy: én fil pr. forhåndsvisning, én adgangskode til alle
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'
Sæt hashen ind i /etc/caddy/Caddyfile:
(preview_common) {
basicauth {
reviewer PASTE-THE-HASH-HERE
}
}
import /etc/caddy/previews/*.caddy
Kør så systemctl reload caddy. Hver forhåndsvisning ligger nu bag samme login, så en halvfærdig gren med debug-output står ikke åbent på internettet. I nyere Caddy-versioner staves direktivet også basic_auth; basicauth virker stadig i begge.
Agentens nøgle: up og down, intet andet
Agenten skal kunne starte forhåndsvisninger, ikke eje serveren. Opret en bruger deploy og et gate-script, der accepterer præcis to kommandoer:
#!/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
I /home/deploy/.ssh/authorized_keys bindes agentens offentlige nøgle til det script:
restrict,command="/usr/local/bin/preview-gate" ssh-ed25519 AAAA... coding-agent
Fra den maskine, hvor agenten arbejder:
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
I vores test byggede og publicerede up grenen, et andet up efter et nyt commit serverede den nye version, og down fjernede containeren og sitet. Forsøg med cat /etc/shadow, up x; id, grennavne som -x eller a/../b eller et interaktivt login med nøglen fik alle kun usage: up|down <branch>. Et grennavn skal begynde med et bogstav eller et ciffer og må ikke indeholde ...
Kobl det på agenten
Med Claude Code eller Codex er én linje i projektets instruktionsfil nok:
Når du har pushet en gren, så kør
ssh deploy@preview.example.com up <branch>og sæt den udskrevne URL i dit resumé. Kørdown, når grenen er merget eller opgivet.
Kører din CI allerede ved hvert push, så kald den samme kommando derfra og hold nøglen helt ude af agentens miljø. Det er den strengere mulighed, og vi ville vælge den til alt ud over et personligt projekt.
Hvad du skal holde øje med
- Disk. Hvert build efterlader image-lag. Et ugentligt cron-job med
docker image prune -fholder det nede;downfjerner allerede forhåndsvisningens eget image. - Grene, ingen lukker. Agenter åbner flere grene end mennesker. Et cron-job, der lister
preview-*-containere ældre end en uge og kørerpreview-downpå dem, sparer dig for at finde tyve glemte forhåndsvisninger. - Hemmeligheder. Forhåndsvisninger bør køre med testlegitimation. Basic auth-adgangskoden holder fremmede ude; den gør det ikke i orden at pege en forhåndsvisning mod produktionsdatabasen.
- Byggetid. Images bygges på serveren, så et tungt build konkurrerer om CPU med kørende forhåndsvisninger. Tager builds minutter, så byg i CI og træk imaget i stedet.
Agenten behøver ikke selv at bo på denne server. Gør den det, så se VPS til Claude Code, og for at lade den bestille og passe serveren selv, forbind den til EQVPS MCP-serveren. Samme maskine kan også køre dit teams MCP-gateway. Til upålidelig kode helt uden server findes sandboxe, og planer med egen IPv4 starter ved $8 om måneden.
Kommentarer
Ingen kommentarer endnu. Vær den første.