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
Dockerfilesom 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ördownnä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 -fhåller det i schack;downtar 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örpreview-downpå 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.
Kommentarer
Inga kommentarer än. Bli först.