−25%

på Windows ved årlig betaling, til 31/10. Se pakkerne

EQVPS
Kom i gang

VPS til forhåndsvisninger fra kodeagenter: hver gren agenten pusher får sin egen URL

En kodeagent bliver færdig med en gren og siger, at den virker. Giv den en server, hvor det kan tjekkes: hver gren bygges til en container og åbner på sit eget HTTPS-underdomæne bag en adgangskode, og agentens SSH-nøgle kan kun starte og stoppe forhåndsvisninger.

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ør down, 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 -f holder det nede; down fjerner 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ører preview-down på 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.

FAQ

Hvorfor ikke lade agenten deploye direkte til produktion?

Fordi „testene består“ og „det virker“ ikke er det samme, og agenten er den sidste, der bør afgøre det. En forhåndsvisning giver et menneske noget at klikke på, før noget når brugerne, for prisen af én ekstra kommando.

Hvilken plan passer?

Det afhænger af, hvor mange forhåndsvisninger der kører på én gang. AI-Agent-IP ($15/md., 4 vCPU, 4 GB RAM, 40 GB disk) klarer fint flere små webapps med en grænse på 512 MB hver. Docker-builds er den tungeste del, så hold øje med disk og hukommelse, hvis dine images er store.

Skal jeg bruge et wildcard-certifikat?

Nej. Caddy henter selv et separat certifikat til hvert forhåndsvisningsunderdomæne. Kun DNS-recorden er wildcard. Let's Encrypt tillader omkring 50 nye certifikater pr. registreret domæne om ugen, rigeligt medmindre agenten laver hundredvis af grene.

Kan agenten ødelægge serveren med sin SSH-nøgle?

Ikke med den nøgle. Den er låst til én kommando, der kun accepterer „up <gren>“ eller „down <gren>“ med et sikkert grennavn. Shell-adgang, ekstra argumenter og portvideresendelse afvises.

Virker det på en NAT-plan?

Nej. Forhåndsvisninger skal kunne nås på port 80 og 443 på deres egne underdomæner, og det kræver en offentlig IPv4. NAT-planer videresender kun SSH-porten.

Kommentarer

Ingen kommentarer endnu. Vær den første.

Skriv en kommentar

Kommentarer modereres, før de vises.