−25%

op Windows bij jaarbetaling, tot 31/10. Naar de pakketten

EQVPS
Aan de slag

VPS voor previews van coding-agents: elke gepushte branch krijgt een eigen URL

Een coding-agent is klaar met een branch en zegt dat het werkt. Geef hem een server waar je dat kunt controleren: elke branch wordt in een container gebouwd en opent op een eigen HTTPS-subdomein achter een wachtwoord, en de SSH-sleutel van de agent kan alleen previews starten en stoppen.

Een coding-agent zegt dat de feature werkt. Misschien klopt dat. De tests die hij schreef slagen, en dat bewijst minder dan iedereen zou willen. Wat je eigenlijk wilt, is het ding in een browser openen voordat het ergens bij gebruikers in de buurt komt, zonder de agent aan productie te laten komen.

Preview-omgevingen lossen dat op, en je hebt er geen platform voor nodig. Eén VPS met eigen IPv4, Docker, Caddy en twee korte scripts geven elke branch een eigen URL zoals https://feature-login-form.preview.example.com, achter een wachtwoord, bij elke push opnieuw gebouwd. De agent krijgt een SSH-sleutel die previews kan starten en stoppen en verder niets.

We bouwden dit op een testserver en liepen de hele cyclus door voordat we het opschreven: aanmaken, openen, opnieuw pushen, afbreken, proberen de sleutel te misbruiken.

Wat je nodig hebt

  • Een VPS met een publiek IPv4-adres. AI-Agent-IP heeft ruimte voor meerdere previews tegelijk.
  • Een wildcard-DNS-record: *.preview.example.com → het IP van de server.
  • De repository van je app met een Dockerfile die op poort 8080 serveert. Elke taal werkt.
  • Ubuntu 24.04 of Debian 12/13 met apt install -y docker.io caddy git.

Clone de repository één keer naar /srv/app, met een deploy key met alleen leesrechten.

De twee scripts

/usr/local/bin/preview-up bouwt een branch en zet hem 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 haalt hem weg:

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

Een paar keuzes zijn bewust gemaakt. Containers publiceren alleen op 127.0.0.1 met een willekeurige poort, dus niets bereikt ze behalve via Caddy. Dat is belangrijk, want door Docker gepubliceerde poorten omzeilen ufw. Elke preview krijgt een geheugenlimiet van 512 MB, zodat één ontspoorde branch de andere niet meesleurt. En dezelfde branch opnieuw draaien bouwt hem ter plekke opnieuw: push, up draaien, verversen.

Caddy: één bestand per preview, één wachtwoord voor allemaal

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'

Zet de hash in /etc/caddy/Caddyfile:

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

Daarna systemctl reload caddy. Elke preview zit nu achter dezelfde login, dus een half afgemaakte branch met debug-uitvoer staat niet open op internet. In nieuwere Caddy-versies wordt de directive ook basic_auth gespeld; basicauth werkt in beide nog.

De sleutel van de agent: up en down, verder niets

De agent moet previews kunnen starten, niet de server bezitten. Maak een gebruiker deploy en een gate-script dat precies twee commando's accepteert:

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

Koppel in /home/deploy/.ssh/authorized_keys de publieke sleutel van de agent aan dat script:

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

Vanaf de machine waar de agent werkt:

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

In onze test bouwde en publiceerde up de branch, een tweede up na een nieuwe commit serveerde de nieuwe versie, en down verwijderde de container en de site. Pogingen met cat /etc/shadow, up x; id, branchnamen als -x of a/../b, of een interactieve login met die sleutel kregen allemaal alleen usage: up|down <branch>. Een branchnaam moet met een letter of cijfer beginnen en mag geen .. bevatten.

Aansluiten op de agent

Met Claude Code of Codex is één regel in het instructiebestand van het project genoeg:

Na het pushen van een branch voer je ssh deploy@preview.example.com up <branch> uit en zet je de getoonde URL in je samenvatting. Voer down uit als de branch gemerged of opgegeven is.

Draait je CI al bij elke push, roep dan hetzelfde commando daarvandaan aan en houd de sleutel helemaal uit de omgeving van de agent. Dat is de strengere optie, en die zouden wij kiezen voor alles wat verder gaat dan een persoonlijk project.

Waar je op moet letten

  • Schijf. Elke build laat image-layers achter. Een wekelijkse cron met docker image prune -f houdt dat in toom; down verwijdert het eigen image van de preview al.
  • Branches die niemand sluit. Agents openen meer branches dan mensen. Een cronjob die preview-*-containers ouder dan een week opsomt en er preview-down op draait, bespaart je twintig vergeten previews.
  • Geheimen. Previews horen met testgegevens te draaien. Het basic-auth-wachtwoord houdt vreemden buiten; het maakt het niet goed om een preview op de productiedatabase te laten wijzen.
  • Buildtijd. Images worden op de server gebouwd, dus een zware build concurreert om CPU met draaiende previews. Duren builds minuten, bouw dan in CI en haal het image op.

De agent zelf hoeft niet op deze server te wonen. Doet hij dat wel, lees dan VPS voor Claude Code, en om hem die server zelf te laten bestellen en onderhouden, koppel hem aan de EQVPS MCP-server. Dezelfde machine kan ook de MCP-gateway van je team hosten. Voor onbetrouwbare code zonder server zijn er sandboxes, en de pakketten met eigen IPv4 beginnen bij $8 per maand.

FAQ

Waarom laat je de agent niet direct naar productie deployen?

Omdat „de tests slagen” en „het werkt” niet hetzelfde zijn, en de agent de laatste is die dat moet beslissen. Een preview geeft een mens iets om aan te klikken voordat er iets bij gebruikers komt, voor de prijs van één extra commando.

Welk pakket past?

Dat hangt af van hoeveel previews tegelijk draaien. AI-Agent-IP ($15/mnd, 4 vCPU, 4 GB RAM, 40 GB schijf) draagt met gemak meerdere kleine webapps met elk een limiet van 512 MB. Docker-builds zijn het zwaarste deel, dus let op schijf en geheugen als je images groot zijn.

Heb ik een wildcardcertificaat nodig?

Nee. Caddy haalt zelf voor elk preview-subdomein een eigen certificaat op. Alleen het DNS-record is een wildcard. Let's Encrypt staat ongeveer 50 nieuwe certificaten per geregistreerd domein per week toe, ruim genoeg tenzij de agent honderden branches aanmaakt.

Kan de agent de server stukmaken met zijn SSH-sleutel?

Niet met die sleutel. Die is beperkt tot één commando dat alleen „up <branch>” of „down <branch>” met een veilige branchnaam accepteert. Shell-toegang, extra argumenten en port forwarding worden geweigerd.

Werkt dit op een NAT-pakket?

Nee. Previews moeten op poort 80 en 443 op hun eigen subdomein bereikbaar zijn, en daarvoor is een publiek IPv4-adres nodig. NAT-pakketten sturen alleen de SSH-poort door.

Reacties

Nog geen reacties. Wees de eerste.

Laat een reactie achter

Reacties worden gemodereerd voordat ze verschijnen.