Ga naar hoofdinhoud
50% korting alle plannen, beperkte tijd. Vanaf $2.48/mo
15 min left
AI en machine learning

Hoe je AI-agents 's nachts laat draaien op een VPS

S Door Sajjad 15 min leestijd
Schedule AI Agents Overnight: a dark terminal showing a 02:00 timestamp and a green exit code 0 line, next to a clock and a completed job card

Om 2 uur 's nachts start een geplande taak op een VPS die nooit in slaap viel. Een headless run met claude -p werkt een taak uit de wachtrij af in een gekloonde repo zonder iemand iets te vragen en stopt daarna. Als je 's ochtends kijkt, ligt er een commit klaar, of een rapport, of een log dat precies laat zien waar het stopte en waarom. Niemand heeft het zien gebeuren.

Dat is iets anders dan een terminal de hele nacht open laten staan en hopen dat de SSH-verbinding het volhoudt. Een veelvoorkomend faalpunt bij een nachtelijke agent-run is de host zelf: een laptop gaat in slaapstand, de klep gaat dicht, het netwerk valt weg, of een OS-update herstart de machine midden in de taak. Authenticatiefouten, API-fouten en vastlopende permissievragen kunnen de job nog steeds slopen, maar een host die altijd aan staat haalt de makkelijkste faalmodus weg.

Deze gids behandelt het echte mechanisme: de headless-vlaggen die elke grote coding-agent-CLI meelevert, de twee manieren om een run volgens schema te starten en welke je moet kiezen, wat de host eronder nodig heeft, en de vangrails die voorkomen dat een onbeheerde run meer kost of kapotmaakt dan je later zou willen uitleggen.

De korte versie

  • Elke grote coding-agent-CLI levert een gedocumenteerde niet-interactieve modus die één prompt volledig uitvoert en dan stopt. Claude Code heeft claude -p, Codex CLI heeft codex exec, en Gemini CLI heeft gemini -p. Dit is geen workaround, maar een officiële functie.
  • Claude Code heeft ook eigen planning: Routines, geplande Desktop-taken en /loop. Voor sommige lezers is dat echt genoeg, en er is minder te onderhouden dan bij een VPS.
  • Cron voldoet prima voor een nachtelijke job. Een systemd-timer is de betere standaard op een machine die kan herstarten, omdat Persistent=true een run opvangt die cron stilzwijgend zou hebben overgeslagen.
  • De CLI zelf is licht, omdat de inferentie op de API van de provider draait. Dimensioneer de VPS voor de commando's die hij gaat draaien (tests, builds, containers, parallelle jobs), niet voor het model.
  • Het zijn de vangrails (afgebakende tools, een limiet op het aantal beurten, vertakken op de exit-code) die het veilig maken om een planning haar gang te laten gaan. De planning zelf is niet het veiligheidsmechanisme.

Wat Je Nodig Hebt

Zorg dat je deze vijf dingen klaar hebt voordat je ook maar één crontab-regel of unit-bestand schrijft:

  • Een VPS waar je via SSH op kunt, met een systemd-gebaseerde Linux-distributie.
  • De agent-CLI geïnstalleerd op die VPS: Claude Code, Codex CLI of Gemini CLI.
  • Een niet-interactieve credential voor de CLI die je kiest. De bare-modus van Claude Code leest geen accountlogin, dus die heeft ANTHROPIC_API_KEY in de omgeving, of een apiKeyHelper in de instellingen nodig. Een gewone run in print-modus, Codex en Gemini kunnen ook de gedocumenteerde accountlogin gebruiken.
  • Een repo of taakmap waar de agent op werkt.
  • Shell-toegang met rechten om een crontab te bewerken of een systemd-unit-bestand te schrijven.

Een agent draaien zonder gekoppelde sessie

Vergelijking van headless-modi: Claude Code draait claude -p met text-, json- of stream-json-uitvoer, Codex CLI draait codex exec met een JSONL-stream en een sandboxbeleid, en Gemini CLI draait gemini -p zonder TTY

Elke grote coding-agent-CLI levert een niet-interactieve modus die precies hiervoor is gemaakt. Claude Code neemt -p, ook geschreven als --print. Codex CLI neemt codex exec. Gemini CLI neemt -p, ook geschreven als --prompt. Elk ervan neemt een prompt aan, voert die volledig uit en stopt. Geen chatloop, geen terminal die open moet blijven, niets om opnieuw aan te koppelen.

Kan Claude Code draaien zonder actieve sessie? Ja. Met -p draait de prompt in niet-interactieve modus: Claude Code voert hem volledig uit, print het resultaat en stopt. Er is geen chatloop en niets om in leven te houden, en het draait op dezelfde Agent SDK als de interactieve CLI, aldus de eigen headless-modus-documentatie van Anthropic.

CLINiet-interactieve vlagGedragGestructureerde uitvoer
Claude Code-p / --printVoert de prompt volledig uit, print het resultaat, stopt--output-format ingesteld op text, json of stream-json
Codex CLIcodex execStreamt voortgang naar stderr, schrijft het eindbericht naar stdout, stopt--json voor een JSONL-eventstream
Gemini CLI-p / --promptVoert de prompt niet-interactief uit, stopt--output-format json

Hier tellen vooral de eigen vlaggen van Claude Code, want daar ga je echt tegenaan scripten. Twee ervan laten een run doorgaan zonder te stoppen voor een permissie die niemand wakker is om te verlenen: --allowedTools, dat specifieke tools vooraf goedkeurt, en --permission-mode, dat de basislijn voor de hele run bepaalt. --max-turns begrenst hoeveel agent-beurten een run mag nemen voordat hij met een fout stopt.

--bare slaat hooks, skills, plugins, MCP-servers en projectinstructies zoals CLAUDE.mdover, voor een snellere en meer deterministische scriptrun. Dat betekent ook dat elke instructie waar de job van afhangt in de prompt of het commando moet staan. De bare-modus leest je accountlogin evenmin, dus zegt de documentatie van Anthropic dat je een API-sleutel in de omgeving moet zetten voordat je hem draait. Claude Code weigert --bg botweg wanneer die wordt gecombineerd met -p, en weigert --cloud op dezelfde manier wanneer je er een taakbeschrijving bij geeft. Het benoemt het conflict en stopt in plaats van iets dubbelzinnigs te doen.

Een uitgewerkte aanroep; pas de prompt en de toollijst aan je taak aan:

claude --bare -p "Review open PRs in this repo and summarize any blockers in NOTES.md" \
  --allowedTools "Bash(gh pr list *),Bash(gh pr view *),Bash(gh pr diff *),Read,Edit" \
  --permission-mode dontAsk \
  --max-turns 8 \
  --max-budget-usd 5.00 \
  --output-format json

Stem het budget en de commandopatronen af op de taak; dit voorbeeld gaat er ook van uit dat de GitHub CLI-authenticatie al is ingesteld voor het uitvoerende account.

Als je Claude Code op een verse VPS opzet en de stappen wilt om het te authenticeren op een machine zonder browser, dan staat dat apart beschreven in hoe je Claude Code op een headless server authenticeert; de korte versie hierboven is genoeg om een geplande run werkend te krijgen.

De modus exec van Codex CLI, beschreven in de OpenAI-documentatie over de niet-interactieve modus, neemt --sandbox om een beleid te kiezen. read-only is de standaardwaarde, workspace-write laat de agent binnen zijn workspace schrijven, en --json maakt van stdout een machineleesbare eventstream in plaats van platte tekst. Vermijd danger-full-access bij een onbeheerde job, tenzij het proces geïsoleerd is en dat risico bewust is genomen.

De headless-modus van Gemini CLI, gedocumenteerd in de eigen headless-documentatie van het project, wordt automatisch actief in een omgeving zonder TTY, of expliciet met -p. Het stopt met een specifieke exit-code ongelijk aan nul voor een algemene fout, een invoerfout of een bereikte beurtlimiet, in plaats van met één generieke foutcode.

Waar de planning thuishoort

Voordat je aan al dit installatiewerk begint: misschien plant de leverancier van de agent dit al voor je. Claude Code biedt drie ingebouwde opties, en een daarvan past mogelijk echt beter dan een zelfbeheerde VPS.

Cloud (Routines)Geplande Desktop-taak/loop
Draait opDe cloud van AnthropicJe eigen machineJe eigen machine
Machine moet aan staanNiet verplichtVereistVereist
Open sessie vereistNiet verplichtNiet verplichtVereist
Minimuminterval1 uur1 minuut1 minuut
Toegang tot lokale bestandenGeen, het draait vanuit een verse kloonVolledige toegangVolledige toegang

De eigen documentatie van Anthropic over geplande taken presenteert dit als een echte keuze uit drie, niet als een hiërarchie met de VPS bovenaan. Als je taak geen lokale staat nodig heeft, een ondergrens van een uur aankan en je alleen Claude Code gebruikt, is Routines minder onderhoud dan wat hierna komt: Anthropic draait het in de cloud vanuit een verse kloon terwijl jouw machine uit staat.

/loop is goed om te kennen maar past niet bij dit gebruik, want het vereist een open, inactieve sessie, precies de beperking die je wilt wegnemen. Dezelfde documentatie noemt ook GitHub Actions als vierde optie, voor teams waarvan de trigger toch al in de CI zit in plaats van in een planning die aan één specifieke machine hangt.

De zelfbeheerde VPS verdient zijn plek wanneer de job volledige toegang tot het lokale bestandssysteem en de tools nodig heeft, wanneer je hetzelfde mechanisme identiek wilt laten werken over Claude Code, Codex CLI en Gemini CLI heen, of wanneer het interval dat Routines toestaat te grof is. Een gewone serverless functie past hier meestal slecht, omdat die credentials moet herstellen, de repo moet klonen en binnen de runtime-limieten van het platform moet afronden. Een kortstondige CI-runner zoals GitHub Actions blijft een geldige derde weg wanneer een verse checkout per run acceptabel is. Als je toch al hardware hebt die altijd aan staat en niets doet, kan een homelab-machine ook; de ruil is de betrouwbaarheid van je thuisnetwerk en je externe toegang in plaats van die van een provider.

Cron of een systemd-timer?

Cron tegenover een systemd-timer: links één crontab-regel en een gemiste run die simpelweg wordt overgeslagen, rechts een paar van .service en .timer met inhalen via Persistent=true, logging via journald en overlapbeheer met één instantie

Beide tools kunnen hetzelfde commando op hetzelfde schema afvuren, maar ze verschillen in wat er gebeurt als de machine herstart en in hoeveel opzetwerk elk kost:

cronsystemd-timer
OpzetwerkEén crontab-regelEen .timer- en een .service-bestand
Gemiste runs inhalenGeen, een overgeslagen run is gewoon wegPersistent=true draait hem zodra het systeem weer op is
AanmeldenHandmatig, je leidt de uitvoer zelf omAutomatisch, opgevangen door journald
Volgorde van afhankelijkhedenNietsVolledige systemd-ordening met After= en Requires=

Gewone cron voldoet prima voor een nachtelijke job op een machine die zelden herstart. De valkuil is de omgeving: cron start met een minimale PATHPATH, gaat je repo niet voor je binnen en start vrolijk een tweede kopie terwijl de eerste nog draait. Zet het repopad, het smalle agentcommando en het laden van credentials in een beveiligd wrapper-script en gebruik daarna flock om overlappende runs te voorkomen.

# /usr/local/bin/agent-nightly
#!/usr/bin/env bash
set -euo pipefail
export PATH=/usr/local/bin:/usr/bin:/bin
export ANTHROPIC_API_KEY="$(
  cat "$HOME/.config/agent-nightly/anthropic_api_key"
)"
cd /srv/myrepo
exec /usr/local/bin/claude --bare -p \
  "Run the nightly dependency audit and write the findings to NOTES.md" \
  --allowedTools "Bash(npm audit *),Read,Edit" \
  --permission-mode dontAsk \
  --max-turns 8 \
  --max-budget-usd 5.00 \
  --output-format json
# crontab -e
0 2 * * * /usr/bin/flock -n "$HOME/.local/state/agent-runs/nightly.lock" /usr/local/bin/agent-nightly >> "$HOME/.local/state/agent-runs/nightly.log" 2>&1

Maak de mappen voor credentials en logs eenmalig aan en maak het wrapper-script daarna uitvoerbaar:

install -d -m 700 \
  "$HOME/.config/agent-nightly" \
  "$HOME/.local/state/agent-runs"
touch "$HOME/.config/agent-nightly/anthropic_api_key"
chmod 600 "$HOME/.config/agent-nightly/anthropic_api_key"
"${EDITOR:-nano}" \
  "$HOME/.config/agent-nightly/anthropic_api_key"
sudo chmod 755 /usr/local/bin/agent-nightly

Plak alleen de API-sleutel in het credential-bestand. Zet hem niet rechtstreeks in de crontab.

Een systemd-timer vraagt meer opzetwerk en levert je twee dingen op die cron niet heeft: logging via journald zonder zelfgeknutselde omleiding, en Persistent=true. Het voorbeeld hieronder gaat ervan uit dat een apart account agent-runner de eigenaar is van /srv/myrepo. Bewaar de API-sleutel in een credential-bestand dat alleen root kan lezen, in plaats van hem in de unit te zetten.

Per de systemd.timer-handleiding, betekent het instellen van Persistent=true betekent dat "de service-unit onmiddellijk wordt gestart als hij minstens één keer gestart zou zijn in de tijd dat de timer inactief was". Een run die zou zijn afgegaan terwijl je VPS herstartte voor een kernel-update, start dus zodra hij weer terug is, in plaats van stilletjes te verdwijnen tot het volgende geplande moment.

Maak het credential-bestand aan dat alleen root kan lezen en dat de service gebruikt:

sudo install -d -m 700 /etc/agent-nightly
sudo touch /etc/agent-nightly/anthropic_api_key
sudo chmod 600 /etc/agent-nightly/anthropic_api_key
sudoedit /etc/agent-nightly/anthropic_api_key

Plak alleen de API-sleutel in het bestand.

# /etc/systemd/system/agent-nightly.service
[Unit]
Description=Nightly scoped agent run
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=agent-runner
Group=agent-runner
WorkingDirectory=/srv/myrepo
Environment=HOME=/home/agent-runner
Environment=PATH=/usr/local/bin:/usr/bin:/bin
LoadCredential=anthropic_api_key:/etc/agent-nightly/anthropic_api_key
ExecStart=/bin/sh -c 'export ANTHROPIC_API_KEY="$(cat "$CREDENTIALS_DIRECTORY/anthropic_api_key")"; exec /usr/local/bin/claude --bare -p "Run the nightly dependency audit and write the findings to NOTES.md" --allowedTools "Bash(npm audit *),Read,Edit" --permission-mode dontAsk --max-turns 8 --max-budget-usd 5.00 --output-format json'
StandardOutput=journal
StandardError=journal
UMask=0077
# /etc/systemd/system/agent-nightly.timer
[Unit]
Description=Run agent-nightly.service at 2am daily, catching up missed runs

[Timer]
# Uses the VPS's configured local timezone
OnCalendar=*-*-* 02:00:00
Persistent=true
Unit=agent-nightly.service

[Install]
WantedBy=timers.target

Herlaad systemd, zet de timer aan en draai de service meteen één keer, zodat problemen met credentials, permissies en paden nu naar boven komen in plaats van om 2 uur 's nachts:

sudo systemctl daemon-reload
sudo systemctl enable --now agent-nightly.timer
sudo systemctl start agent-nightly.service
systemctl list-timers agent-nightly.timer
sudo journalctl \
  -u agent-nightly.service \
  -n 100 \
  --no-pager

Persistent=true is het doorslaggevende verschil: de timer onthoudt een gemiste kalenderrun in plaats van hem stilzwijgend te laten vallen.

Wat de VPS echt nodig heeft

Dit is het stuk dat mensen verrast die dit voor het eerst dimensioneren: de CLI zelf is licht, omdat de inferentie op de API van de provider draait. Maar de agent kan lokaal nog steeds builds, tests, package managers, language servers en containers starten, dus de werklast van de repo bepaalt de echte ondergrens.

Neem 1 tot 2 vCPU en 2 tot 4 GB RAM met NVMe-opslag als startpunt voor één lichte geplande job. Grote repo's, compilers, Docker-builds, testsuites of gelijktijdige runs kunnen veel meer nodig hebben. Wat je dwingt op te schalen is het zwaarste lokale commando dat de agent draait, niet het model achter de API. Als je op deze VPS al Docker-workloads draait en een completer beeld van je budget wilt, een buildmachine dimensioneren en beveiligen loopt dezelfde afweging door voor een andere onbeheerde werklast.

Nog iets om rekening mee te houden: een onbeheerde run produceert elke nacht logs, of er nu iets misging of niet. Voeg logrotate toe als cron naar een bestand schrijft, en controleer de bewaarlimieten van journald in plaats van aan te nemen dat de standaardwaarden bij de schijf van de VPS passen.

De hele aanpak hangt af van een host die om 2 uur 's nachts wakker is en dat blijft, ongeacht wat je laptop doet. Dat is precies de taak waarvoor een Linux VPS met root-toegang is gebouwd. Niets zet hem in slaapstand, en je deelt hem niet met andermans cronjobs.

Bekijk Linux-plannen

Bouw op een Linux VPS met root-toegang, NVMe en AMD EPYC-kracht.

Bekijk Linux-plannen

Voorkomen dat een onbeheerde run misgaat

Het grootste verschil tussen een geplande run die werkt en een die dat niet doet, is of de taak smal genoeg is om af te ronden zonder dat een mens halverwege een vraag beantwoordt. Ambitieuze prompts blijven hangen op een beslissing die niemand kan nemen; smalle, op zichzelf staande taken lopen af en stoppen netjes.

De twee permissievlaggen bestaan zodat een run om 2 uur 's nachts niet blijft hangen op een vraag, maar kale Bash toegang is geen smalle vangrail: die kan bijna alles wat het serviceaccount kan. Kies liever commandospecifieke regels zoals Bash(git status *), combineer ze met --permission-mode dontAsk, en draai de service onder een apart niet-root-account. Aantal beurten en uitgaven krijgen elk hun eigen plafond: --max-turns begrenst hoe lang de agent kan ronddwalen, en --max-budget-usd zet een plafond op wat één run aan API-aanroepen mag uitgeven.

Pro-tip: draai met --output-format json en log het veld total_cost_usd van elke aanroep mee. Dat is het schoonste aanknopingspunt om te volgen wat een geplande run per nacht echt kost, en om te waarschuwen wanneer één run merkbaar meer kost dan de rest. De vijf minuten om het op te zetten zijn het waard, want het is jouw rekening die wordt gevolgd, geen abstractie.

Kostenoverschrijdingen bij onbeheerde runs zijn niet hypothetisch. In een bericht op Hacker Newsmeldde een gebruiker een bruto AWS Bedrock-rekening van $37.901,73 door een dagelijkse coding-agent-workflow waarin prompt caching maar deels werkte, waardoor zo'n 6,47 miljard invoertokens ongecachet bleven. Dat gebeurde in een andere stack, niet in de headless-modus van Claude Code, maar het laat zien waarom kostenlogging en een hard budget per run in de planning thuishoren.

Pro-tip: Claude Code stopt met code 0 bij succes en met een code ongelijk aan nul bij een fout. Een wrapper-script dat de exit-status controleert kan je bij een fout een melding sturen, zodat een slechte nacht de volgende ochtend opvalt in plaats van drie dagen later, wanneer je toevallig kijkt.

Draai elke job minstens op een aparte branch of een wegwerpbare worktree en eis menselijke review voor de merge. Beperkte credentials, isolatie van het bestandssysteem en beheersing van de schadestraal op serverniveau vormen een groter onderwerp dat een eigen behandeling verdient in plaats van een alinea achteraan een planningsgids.

Het zijn de vangrails die het veilig maken om de planning met rust te laten: de planning zelf is niet het veiligheidsmechanisme.

Wanneer cron niet meer volstaat

Eén prompt op een timer heeft niets meer nodig dan wat hier al staat. Drie geketende stappen met een voorwaarde, een retry en een Slack-melding vragen om iets anders.

Drie opties zijn het waard om te kennen, elk om een andere reden een stap hoger:

  • Dagu is de lichtste stap omhoog: op zichzelf staande, in YAML gedefinieerde jobs met DAG-afhankelijkheden, retries en een web-UI om te zien wat er gedraaid heeft.
  • n8n past het beste wanneer de agent-run één knooppunt is tussen meerdere integraties en meldingen, en niet de hele workflow.
  • Kestra is de zwaarste van de drie, gebouwd voor het orkestreren van data- en infrastructuurpipelines, en het juiste antwoord wanneer het inplannen van de agent onderdeel is van een grotere pipeline in plaats van het doel ervan.

Voor de lezer die één prompt per nacht draait zijn alle drie overkill, en dat mag je gewoon zeggen in plaats van je een zwaardere opzet aan te praten dan je nodig hebt. Als een keten van stappen er uiteindelijk toch een rechtvaardigt: Dagu, n8n, en Kestra zijn allemaal met één klik uit te rollen, wat een echt gemak is op precies het moment dat je afweegt of de opzetkosten het waard zijn.

Multi-agent-orkestratieframeworks zoals LangChain of CrewAI zijn een heel ander onderwerp: agentsystemen bouwen in plaats van een CLI inplannen die al bestaat.

Veelgestelde vragen

Kan Claude Code draaien zonder actieve sessie?

Ja. Met -p draait de prompt in niet-interactieve modus: Claude Code voert hem volledig uit, print het resultaat en stopt, zonder chatloop en zonder sessie die open moet blijven.

Heb ik een VPS nodig als Claude Code al Routines heeft?

Niet altijd. Routines draait in de cloud van Anthropic terwijl je machine uit staat en start vanuit een verse kloon, maar kan geen bestanden benaderen die alleen op jouw machine staan, en heeft een minimuminterval van een uur. Een zelfbeheerde VPS verdient zijn plek wanneer de taak lokale bestanden, willekeurige intervallen of een mechanisme nodig heeft dat bij meerdere leveranciers-CLI's hetzelfde werkt.

Moet ik cron of een systemd-timer gebruiken voor een geplande agent?

Een systemd-timer, als de VPS ooit herstart voor onderhoud. Persistent=true draait een job die tijdens de downtime zou zijn afgegaan zodra het systeem terug is, en daar heeft cron geen equivalent voor. Cron voldoet prima voor een nachtelijke job op een machine die aan blijft.

Hoeveel RAM heeft een geplande AI-agent nodig op een VPS?

Begin met ongeveer 1 tot 2 vCPU en 2 tot 4 GB RAM voor één lichte geplande job, en dimensioneer daarna op het zwaarste lokale commando dat de agent draait. Builds, tests, Docker, grote repository's en gelijktijdige runs wegen veel zwaarder dan de externe modelinferentie.

Verandert het factureren als je een agent volgens schema draait?

Plannen creëert geen aparte facturatiemodus. Claude Code -p kan abonnementscredentials of een API-sleutel gebruiken, maar --bare negeert de abonnementslogin, dus die heeft ANTHROPIC_API_KEY in de omgeving, of een apiKeyHelper in de instellingen nodig. Codex en Gemini volgen de authenticatiemethode die je voor hun CLI hebt ingesteld. Omdat prijzen en gebruiksvoorwaarden snel veranderen, controleer je bij het opzetten de actuele prijzen van de provider en je eigen gebruiksgegevens. Voor Claude Code-runs via de API kun je daarnaast het veld total_cost_usd uit de JSON-uitvoer loggen.

Delen

Discussie

Reacties

Log in om mee te praten.

Meer van de blog

Blijf lezen.

Klaar om uit te rollen? Vanaf $2,48/mnd.

Onafhankelijke cloud, sinds 2008. AMD EPYC, NVMe, 40 Gbps. 14 dagen niet-goed-geld-terug.