−25%

på Windows vid årsbetalning, till 31/10. Till paketen

EQVPS
Kom igång

VPS för förhandsvisningar från kodagenter: varje gren som agenten pushar får en egen URL

En kodagent blir klar med en gren och säger att den fungerar. Ge den en server där det går att kontrollera: varje gren byggs till en container och öppnas på en egen HTTPS-underdomän bakom ett lösenord, och agentens SSH-nyckel kan bara starta och stoppa förhandsvisningar.

En kodagent säger att funktionen fungerar. Kanske gör den det. Testerna den skrev går igenom, vilket bevisar mindre än alla skulle vilja. Det du egentligen vill är att öppna saken i en webbläsare innan den kommer i närheten av användarna, och det utan att be agenten röra produktionen.

Förhandsvisningsmiljöer löser det, och du behöver ingen plattform för dem. En VPS med egen IPv4, Docker, Caddy och två korta skript ger varje gren en egen URL som https://feature-login-form.preview.example.com, bakom ett lösenord och ombyggd vid varje push. Agenten får en SSH-nyckel som kan starta och stoppa förhandsvisningar och inget annat.

Vi byggde det här på en testserver och körde hela cykeln innan vi skrev ner det: skapa, öppna, pusha igen, riva, försöka missbruka nyckeln.

Det här behöver du

  • En VPS med publik IPv4. AI-Agent-IP har plats för flera förhandsvisningar samtidigt.
  • En wildcard-DNS-post: *.preview.example.com → serverns IP.
  • Appens repo med en Dockerfile som serverar på port 8080. Vilket språk som helst.
  • Ubuntu 24.04 eller Debian 12/13 med apt install -y docker.io caddy git.

Klona repot en gång till /srv/app med en deploy-nyckel med enbart läsrätt.

De två skripten

/usr/local/bin/preview-up bygger en gren och lägger ut den:

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

Några val är medvetna. Containrarna publicerar bara på 127.0.0.1 med en slumpmässig port, så inget når dem utom via Caddy. Det spelar roll, eftersom portar som Docker publicerar går förbi ufw. Varje förhandsvisning har en minnesgräns på 512 MB, så en skenande gren inte tar ner de andra. Och att köra samma gren igen bygger om den på plats: pusha, kör up, ladda om.

Caddy: en fil per förhandsvisning, ett lösenord för alla

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'

Lägg in hashen i /etc/caddy/Caddyfile:

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

Kör sedan systemctl reload caddy. Varje förhandsvisning ligger nu bakom samma inloggning, så en halvfärdig gren med felsökningsutskrifter står inte öppen på internet. I nyare Caddy-versioner stavas direktivet också basic_auth; basicauth fungerar fortfarande i båda.

Agentens nyckel: up och down, inget annat

Agenten behöver starta förhandsvisningar, inte äga servern. Skapa en användare deploy och ett grindskript som tar emot exakt två kommandon:

#!/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, lås agentens publika nyckel till skriptet:

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

Från maskinen där agenten arbetar:

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 vårt test byggde och publicerade up grenen, ett andra up efter en ny commit serverade den nya versionen, och down tog bort containern och sajten. Försök med cat /etc/shadow, up x; id, grennamn som -x eller a/../b eller en interaktiv inloggning med nyckeln fick alla bara usage: up|down <branch>. Ett grennamn måste börja med en bokstav eller siffra och får inte innehålla ...

Koppla in det i agenten

Med Claude Code eller Codex räcker en rad i projektets instruktionsfil:

När du har pushat en gren, kör ssh deploy@preview.example.com up <branch> och lägg den utskrivna URL:en i din sammanfattning. Kör down när grenen är sammanslagen eller övergiven.

Om din CI redan körs vid varje push, anropa samma kommando därifrån och håll nyckeln helt borta från agentens miljö. Det är det striktare alternativet, och vi skulle välja det för allt utöver ett personligt projekt.

Att hålla koll på

  • Disk. Varje bygge lämnar avbildningslager. En veckovis cron med docker image prune -f håller det i schack; down tar redan bort förhandsvisningens egen avbildning.
  • Grenar som ingen stänger. Agenter öppnar fler grenar än människor. Ett cron-jobb som listar preview-*-containrar äldre än en vecka och kör preview-down på dem besparar dig tjugo bortglömda förhandsvisningar.
  • Hemligheter. Förhandsvisningar ska köras med testuppgifter. Basic auth-lösenordet håller främlingar ute; det gör det inte okej att peka en förhandsvisning mot produktionsdatabasen.
  • Byggtid. Avbildningar byggs på servern, så ett tungt bygge konkurrerar om CPU med körande förhandsvisningar. Tar byggen minuter, bygg i CI och hämta avbildningen i stället.

Agenten själv behöver inte bo på den här servern. Gör den det, se VPS för Claude Code, och för att låta den beställa och sköta servern själv, anslut den till EQVPS MCP-server. Samma maskin kan också köra teamets MCP-gateway. För obetrodd kod helt utan server finns sandboxar, och planer med egen IPv4 börjar på $8 i månaden.

FAQ

Varför inte låta agenten deploya direkt till produktion?

För att ”testerna går igenom” och ”det fungerar” inte är samma sak, och agenten är den sista som borde avgöra det. En förhandsvisning ger en människa något att klicka på innan något når användarna, till priset av ett extra kommando.

Vilken plan passar?

Det beror på hur många förhandsvisningar som körs samtidigt. AI-Agent-IP ($15/mån, 4 vCPU, 4 GB RAM, 40 GB disk) klarar bekvämt flera små webbappar med 512 MB-gräns var. Docker-byggen är den tyngsta delen, så håll koll på disk och minne om dina avbildningar är stora.

Behöver jag ett wildcard-certifikat?

Nej. Caddy hämtar själv ett eget certifikat för varje förhandsvisningsunderdomän. Bara DNS-posten är wildcard. Let's Encrypt tillåter runt 50 nya certifikat per registrerad domän och vecka, gott och väl om inte agenten skapar hundratals grenar.

Kan agenten förstöra servern med sin SSH-nyckel?

Inte med den nyckeln. Den är begränsad till ett enda kommando som bara tar emot ”up <gren>” eller ”down <gren>” med ett säkert grennamn. Skalåtkomst, extra argument och portvidarebefordran nekas.

Fungerar det på en NAT-plan?

Nej. Förhandsvisningar måste nås på port 80 och 443 på egna underdomäner, vilket kräver en publik IPv4. NAT-planer vidarebefordrar bara SSH-porten.

Kommentarer

Inga kommentarer än. Bli först.

Lämna en kommentar

Kommentarer modereras innan de visas.