Linux-VPS für KI-Entwicklung: Debian, Claude Code, MCP

March 24, 2026 · 10 min read · linux, vps, devops, ai-development, debian, dach
Linux-VPS für KI-Entwicklung: Debian, Claude Code, MCP

Mein Laptop schläft. Meine Agenten nicht. Genau deswegen läuft mein KI-Setup auf einem Linux-VPS und nicht in einem lokalen Python-venv.

Alles, was ich ausliefere — den TickTick-MCP-Server, den Telegram-Bot, der Claude Opus per Long-Polling abfragt, die Cron-getriebenen Morgen-Briefings, die Customer-Profiling-Pipeline — läuft auf einer einzigen Debian-Maschine. Kein Kubernetes. Kein Docker Swarm. Nur systemd, Bash und das Anthropic-SDK. Diese Anleitung ist die exakte Sequenz, die ich nutze, wenn ich einen neuen VPS für ein KI-Projekt aufsetze: vom frischen Hetzner-Image bis zur funktionierenden Claude-Code-CLI mit verkabelten MCP-Clients.

Vorab das Verdikt: Debian 13 auf einem Hetzner-Cloud-CX22 oder CX32 ist der richtige Default für einen Linux-VPS in der KI-Entwicklung. Sie brauchen keine GPU für Agent-Orchestrierung — Sie brauchen Persistenz, eine öffentliche IP und systemd. GPU-Spend reservieren Sie für tatsächliche Inferenz und halten die Orchestrierungsschicht auf einer 5- bis 20-Euro-Maschine.

Warum überhaupt ein Linux-VPS für KI-Entwicklung?

Ein Laptop ist das falsche Substrat für Agenten. In dem Moment, in dem Sie den Deckel zumachen, stoppen Cron-Jobs, gehen Webhook-Endpunkte dunkel, und ein nächtlicher Eval-Run stirbt um 23:47, wenn der Energiesparmodus zuschlägt.

Ein self-hosted KI-Entwicklungs-VPS löst fünf konkrete Probleme:

  1. Persistenter State. Cron-Jobs feuern unabhängig davon, ob Sie an der Tastatur sind. Lange laufende Agenten halten eine Redis-Queue tagelang warm. Eval-Runs durch die Nacht beenden sich, während Sie schlafen.
  2. Volle Runtime-Kontrolle. Ich kann beliebige System-Pakete installieren, ein tmpfs für heisse Caches mounten, Kernel-Parameter setzen und drei Node-Versionen parallel betreiben. Ein Sandbox-Notebook-Host lässt Sie nichts davon tun.
  3. Vorhersagbare Kosten. Ein CX22 sind ein paar Euro im Monat. Ein AX41-Bare-Metal liegt bei rund 40. Keine Egress-Überraschungen, keine “Sie haben das Notebook nicht gestoppt”-Rechnungen.
  4. Always-on für Bots und Webhooks. Mein Telegram-Bot pollt die Telegram-API in einer Schleife. Stripe- und Claude-Webhooks brauchen eine öffentliche IP, die auf Port 443 antwortet. Das ist VPS-Aufgabe, nicht Laptop-Aufgabe.
  5. Eine Shell, viele Projekte. Zehn Agenten teilen sich denselben Python-Toolchain, dasselbe systemd, dasselbe journald. Projektwechsel ist cd, keine Cloud-Konsole.

Wenn Sie Kontext zur Skalierung wollen: ich habe das Pattern in Zehn KI-Agenten in Produktion, alle als Bash-Skripte ausführlich beschrieben.

Provider- und OS-Wahl

Für die meiste KI-Dev-Arbeit nehme ich Hetzner Cloud in einer EU-Region. Ehrliches Netzwerk-Pricing, schnelles NVMe, eine API, die sich mit Terraform sauber bedienen lässt. AWS, GCP und Azure ergeben Sinn, wenn Sie spezifisch einen Managed-Service in derselben Region wie Ihre Orchestrierung brauchen — Bedrock, Vertex, diese Klasse. DSGVO-Vorteil obendrein: EU-Hosting ohne Schrems-II-Diskurs.

Beim OS: Debian 13 (stable). Nicht Ubuntu, nicht Alpine.

  • Debian statt Ubuntu, weil Ubuntu ein Derivat ist, das abdriftet. Snap drückt Updates rein, die Sie nicht angefragt haben, der Default-Kernel bringt zusätzliche Telemetrie mit, und apt-Pinning ist mehr Arbeit als Nutzen. Debian-stable bewegt sich absichtlich langsam — genau das, was Sie unter einer Reihe lange laufender Agenten wollen.
  • Debian statt Alpine, weil musl Python- und Node-Wheel-Installationen auf nervige, schwer zu debuggende Weise bricht. psycopg2-binary, cryptography, grpcio — alle glücklicher mit glibc. Ein Debian-KI-Dev-Environment ist der Pfad des geringsten Widerstands.

VPS mit vorhinterlegtem SSH-Public-Key provisionieren. Keine Passwort-Logins ab Minute eins.

Erste 10 Minuten: Hardening

Das ist exakt der Block, den ich nach dem ersten SSH einkippe. Einfügen, warten, fertig.

# 1. Update und Core-Tools
sudo apt update && sudo apt upgrade -y
sudo apt install -y ufw fail2ban unattended-upgrades \
  htop btop tmux vim git curl jq ripgrep fd-find

# 2. Non-Root-User anlegen (falls Provider das nicht gemacht hat)
sudo adduser --disabled-password --gecos "" user
sudo usermod -aG sudo user
sudo mkdir -p /home/user/.ssh
sudo cp ~/.ssh/authorized_keys /home/user/.ssh/
sudo chown -R user:user /home/user/.ssh
sudo chmod 700 /home/user/.ssh
sudo chmod 600 /home/user/.ssh/authorized_keys

# 3. SSH abdichten
sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart ssh

# 4. Firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw --force enable

# 5. fail2ban gegen SSH-Brute-Force
sudo systemctl enable --now fail2ban

# 6. Automatische Sicherheits-Updates
sudo dpkg-reconfigure -plow unattended-upgrades

# 7. Zeitzone (lokal Berlin, in Skripten UTC)
sudo timedatectl set-timezone Europe/Berlin

Eine meinungsstarke Notiz zur Zeitzone: Vixie-Cron honoriert kein inline TZ= pro Zeile. Wenn Sie zeitzonenbewusstes Scheduling brauchen, nutzen Sie systemd-Timer, nicht Cron. Das musste ich auf die harte Tour lernen, als eine DST-Umstellung mein Morgen-Briefing für drei Tage eine Stunde verschob.

Core-Dev-Tooling

Sobald die Maschine abgedichtet ist, Sprach-Toolchains installieren. System-Python in Ruhe lassen, überall venvs nutzen.

# Node via nvm (LTS)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm alias default lts/*

# Python
sudo apt install -y python3 python3-venv python3-pip pipx
# Optional, deutlich schneller als pip
pipx install uv

# Go (apt hinkt, Tarball holen)
GO_VERSION=1.23.4
curl -LO https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc

# Tools, die ich täglich nutze
sudo apt install -y httpie sqlite3

Für Git: Commits signieren. Ich halte einen dedizierten GPG-Key pro VPS, niemals den Laptop-Key auf einem Server.

gpg --full-generate-key   # ed25519, kein Ablauf oder 2J
KEYID=$(gpg --list-secret-keys --keyid-format=long | grep sec | awk '{print $2}' | cut -d/ -f2)
git config --global user.signingkey $KEYID
git config --global commit.gpgsign true
git config --global tag.gpgsign true

Claude Code und Anthropic-Stack installieren

Hier wird oft überdacht. Zwei Kommandos.

npm install -g @anthropic-ai/claude-code
claude --version

Auth: entweder API-Key exportieren oder einmalig interaktiv einloggen.

# Variante A: Env-Variable (gut für headless)
echo 'export ANTHROPIC_API_KEY=sk-ant-...' >> ~/.bashrc
source ~/.bashrc

# Variante B: Interaktive Auth (nur wenn der VPS einen Browser hat — meist nein)
claude auth

Auf einer Headless-Maschine ist die Env-Variable einfacher. End-to-End-Test:

claude -p "Sag in einem Wort hallo." --model claude-sonnet-4-6

Wenn das ein Wort zurückgibt, sind Sie verkabelt. Ich nutze exakt dieses Pattern aus Cron-Jobs heraus. Der Telegram-Bot auf dieser Maschine pipelinet eingehende Nachrichten in claude -p --model claude-opus-4-7 mit aktivierten MCP-Tools und gibt das Ergebnis in den Chat zurück.

Das Python-SDK für Skript-Arbeit:

python3 -m venv ~/.venvs/ai
source ~/.venvs/ai/bin/activate
uv pip install anthropic openai pydantic python-dotenv httpx

Ein Baseline-Skript, das den Stack beweist:

# ~/ai-test/hello.py
import os
from anthropic import Anthropic

client = Anthropic()
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=128,
    messages=[{"role": "user", "content": "Gib die aktuelle UTC-Stunde als Integer zurück."}],
)
print(resp.content[0].text)
print("usage:", resp.usage.input_tokens, "->", resp.usage.output_tokens)

Diese Token-Counts loggen — wir nutzen sie später für Cost-Tracking.

MCP-Clients auf dem VPS

Claude Code auf einem VPS liest seine Konfiguration aus ~/.claude.json. Dort registrieren Sie Model-Context-Protocol-Server, damit claude -p Custom-Tools rufen kann. Ein Minimal-Block:

{
  "mcpServers": {
    "ticktick": {
      "command": "node",
      "args": ["/home/user/ticktick-mcp/ticktick-mcp-server.js"],
      "env": {
        "TICKTICK_USERNAME": "[email protected]",
        "TICKTICK_PASSWORD": "$TICKTICK_PASSWORD"
      }
    }
  }
}

Genau diese Form nutze ich für den TickTick-MCP-Server, den ich geschrieben habe. Er läuft als systemd-Service und claude -p spricht über stdio mit ihm. Den vollständigen Walk-Through zum Schreiben eigener MCP-Server finden Sie in MCP-Server in TypeScript bauen.

Secrets und Env-Variablen

Regel eins: niemals eine .env-Datei committen. Regel zwei: niemals Secrets in ein systemd-Unit pasten.

Ich halte eine .env pro Projekt, geladen via EnvironmentFile= im Service-Unit:

# /etc/systemd/system/myagent.service
[Service]
EnvironmentFile=/etc/myagent.env
ExecStart=/home/user/myagent/run.sh
User=user
Restart=on-failure

Die .env lebt ausserhalb des Repos, gehört root, Mode 600:

sudo install -o root -g root -m 600 /dev/null /etc/myagent.env
sudo editor /etc/myagent.env

Für Python-Loading: python-dotenv in Dev und EnvironmentFile in Prod, sodass das Service-Unit die einzige Wahrheit ist.

Wenn Sie mehr als drei oder vier Projekte auf einem VPS betreiben, graduieren Sie zu pass (GPG-gestützt) oder age für verschlüsselte Secret-Files. Beide integrieren mit systemd via ExecStartPre=, das nach /run/ (tmpfs) entschlüsselt — Cleartext berührt nie die Platte.

Persistente Services laufen lassen

Alles Lange-Laufende auf dem VPS wird ein systemd-Unit. Kein nohup, kein screen, kein pm2. Telegram-Bot, MCP-Server, Webhook-Receiver — alle.

Den Pattern habe ich in einem fokussierten Begleitbeitrag dokumentiert: systemd-Services für KI-Server. Die Kurzfassung:

sudo systemctl daemon-reload
sudo systemctl enable --now myagent.service
sudo systemctl status myagent
journalctl -u myagent -f

Für geplante Jobs systemd-Timer statt Cron. Timer respektieren Zeitzonen, überleben DST und loggen ins gleiche Journal wie der Service, den sie triggern. Cron behalte ich nur für eine Handvoll Legacy-Skripte.

Ein typisches Cron- oder Timer-Pattern für einen KI-Agenten auf dieser Maschine:

#!/usr/bin/env bash
# /home/user/bin/morning-briefing.sh
set -euo pipefail
LOG=/var/log/morning-briefing.log

PROMPT="Fasse meine TickTick-Tasks für heute in 5 Bullets zusammen."
OUTPUT=$(env -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT \
  claude -p --model claude-sonnet-4-6 "$PROMPT" 2>>"$LOG")

curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
  -d chat_id="${TG_CHAT_ID}" \
  --data-urlencode "text=$OUTPUT" >>"$LOG" 2>&1

Diese Logs rotieren. Eine Zeile in /etc/logrotate.d/ai-agents:

/var/log/*-briefing.log /var/log/*-followups.log {
    weekly
    rotate 8
    compress
    missingok
    notifempty
    create 0644 user user
}

Monitoring und Cost-Tracking

Live-Sicht: htop oder btop. Service-Logs: journalctl -u <unit> -f. Für ein Browser-Dashboard mit unter einem Prozent Overhead netdata:

bash <(curl -sS https://my-netdata.io/kickstart.sh) --dont-wait
sudo ufw allow from <your-ip> to any port 19999

Das gibt Ihnen CPU, RAM, Disk und Per-Service-Stats in einer Web-UI. 19999 nicht öffentlich exponieren, nur Ihre Heim-IP whitelisten.

Den Punkt, den die meisten überspringen, ist LLM-Cost-Tracking. Jeden Call in eine SQLite loggen, wöchentlich abfragen:

# ~/ai-test/track.py
import sqlite3, datetime
DB = "/home/user/.llm-usage.db"

def log_call(model: str, input_t: int, output_t: int, project: str):
    con = sqlite3.connect(DB)
    con.execute("""
      CREATE TABLE IF NOT EXISTS calls (
        ts TEXT, model TEXT, project TEXT,
        input_tokens INT, output_tokens INT
      )
    """)
    con.execute(
      "INSERT INTO calls VALUES (?,?,?,?,?)",
      (datetime.datetime.utcnow().isoformat(), model, project, input_t, output_t),
    )
    con.commit()
    con.close()

Jeden client.messages.create(...)-Call damit umfassen. Am Wochenende eine Aggregat-Query pro Modell, und Sie wissen exakt, wo das Geld geblieben ist. Das ist der einzige Weg, den ich gefunden habe, um zu verhindern, dass ein fehlverhaltender Agent stillschweigend Ihre Monatsrechnung verdoppelt.

Backups

restic nach Backblaze B2 oder einem beliebigen S3-kompatiblen Store. Verschlüsselt, dedupliziert, billig.

sudo apt install -y restic
export RESTIC_REPOSITORY=b2:my-bucket:/vps
export RESTIC_PASSWORD_FILE=/root/.restic-pw
restic init

# Tägliches Snapshot der Dinge, die zählen
restic backup \
  /home/user \
  /etc/systemd/system \
  /etc/nginx \
  --exclude '/home/user/.cache' \
  --exclude '/home/user/.venvs'

Auf einen systemd-Timer wiren, nächtlich um 03:00. Wenn Sie Postgres- oder SQLite-Datenbanken für Agenten betreiben, davor einen pg_dump oder sqlite3 .backup und das Dump-Verzeichnis in den restic-Run einbeziehen.

Restore einmal pro Quartal auf einem Wegwerf-VPS testen. Ein Backup, das Sie nicht restored haben, ist eine Hoffnung, kein Backup.

Eine persönliche KI-App deployen

Zwei Patterns, die ich tatsächlich nutze.

Pattern A, das einfache. SSH einloggen, git pull, systemctl restart. Das war’s.

ssh user@vps "cd ~/myagent && git pull && sudo systemctl restart myagent"

Für ein Solo-Projekt mit einer Handvoll Deploys pro Woche reicht das. Nicht überengineeren.

Pattern B, der Push-Deploy. Ein Bare-Git-Repo auf dem VPS mit Post-Receive-Hook. Sie machen git push vps main vom Laptop, der Hook baut und startet neu.

# Auf dem VPS einmal
mkdir -p ~/repos/myagent.git && cd $_
git init --bare

cat > hooks/post-receive <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
WORK=/home/user/myagent
git --work-tree=$WORK --git-dir=$PWD checkout -f main
cd $WORK
npm ci --omit=dev
sudo systemctl restart myagent
EOF
chmod +x hooks/post-receive

# Auf Ihrem Laptop
git remote add vps [email protected]:repos/myagent.git
git push vps main

user ALL=(ALL) NOPASSWD: /bin/systemctl restart myagent in /etc/sudoers.d/myagent, damit der Hook nicht nach Passwort fragt.

Was Sie nicht tun sollten

Eine kurze Liste an Fehlern, die ich auf einem Hetzner-KI-VPS gemacht habe, damit Sie es nicht müssen:

  • Keine LLM-Inferenz auf einem CPU-VPS. Ein 7B-Modell auf einem CX32 gibt Ihnen zwei Tokens pro Sekunde und heizt das Rechenzentrum. API für Orchestrierung, GPU-Instanz on-demand für Inferenz, beides getrennt halten.
  • Keine MCP-Server auf öffentlichen Ports ohne Auth. Ein MCP-Server über stdio auf localhost ist sicher. Ein MCP-Server an 0.0.0.0 ohne Token ist eine Prompt-Injection-Pipeline aus dem offenen Internet in Ihre Shell.
  • Keine API-Keys in systemd-Units hardcoden. Sie landen in journalctl-Output, im Backup, in Pastebins, wenn Sie um Hilfe bitten. EnvironmentFile= und chmod 600.
  • Nicht auf Cron für zeitzonen-sensible Jobs verlassen. systemd-Timer regeln DST. Cron nicht.
  • Firewall nicht überspringen. ufw ab Tag eins. Frische VPS-Instanzen werden innerhalb von sechs Stunden gebrute-forced.

Welches Setup für Sie?

Wenn Sie diese Woche ein neues KI-Projekt starten, ist der Default-Stack: Debian 13, Hetzner CX22, der Hardening-Block oben, Node LTS, Python 3.12 mit uv, Go aus Tarball, Claude Code CLI, systemd für alles Lange-Laufende, .env-Dateien via EnvironmentFile=, restic nach B2 nächtlich. Sie können in unter einer Stunde ab ssh root@... Agenten ausliefern.

Maschine erst hochskalieren, wenn Sie sustained CPU über 70 Prozent oder Ihren Agent-Count über 4 GB RAM treibend sehen. Ein einzelner CX32 hat bei mir 10 Agenten, einen Telegram-Bot, einen MCP-Server, einen nginx-Reverse-Proxy und eine Postgres-Instanz problemlos getragen. Sie brauchen mit hoher Wahrscheinlichkeit nicht mehr Hardware, sondern bessere systemd-Units und engere Cron-Fenster.

Wenn Sie das einmalig sauber für Ihr Unternehmen aufgesetzt haben wollen, statt selbst zu wiren: ein externer KI-Engineer macht das tageweise; bei Architektur-Entscheidungen mit DSGVO-Bezug lohnt eine kurze Session mit einem erfahrenen KI-Berater.

Weiterführende Artikel

Schriftliches Angebot in 24 Stunden

5 Felder. Ich antworte innerhalb von 24 Stunden – entweder mit einem Festpreis-Angebot samt Umsetzungsdauer oder mit einer klaren Absage inklusive Begründung.

Anfrage eingegangen

Ich antworte innerhalb von 24 Stunden mit einer ehrlichen Einschätzung.

Lieber direkt sprechen? 30-Minuten-Roadmap-Gespräch →