Systemd-Services für KI-Server: Production-Setup auf Linux
Ich betreibe einen TickTick-MCP-Server, einen Telegram-Bot, der durch Claude Opus routet, und zehn geplante KI-Agenten auf einem einzelnen Debian-VPS. Keiner davon läuft in Docker. Alle laufen als systemd-Services oder systemd-Timer.
Das hier ist die Setup-Anleitung, wie ich systemd-Services für KI-Server tatsächlich in Produktion betreibe. Unit-Files, Logs, Timer, Resource Limits und das Security-Hardening, das wirklich zählt. Keine Container-Orchestrierung, kein Kubernetes, kein Docker-Compose-YAML. Nur systemd, weil es für Single-Host-KI-Workloads das richtige Werkzeug ist.
Wenn Sie einen LLM-Inference-Server, einen MCP-Server oder eine Flotte bash-basierter Agenten verkabeln, ist das das Pattern, das die Dinge über Reboots, OOM-Kills und die DST-Umstellung am Leben hält, an die niemand gedacht hat.
Warum systemd für kleine KI-Services
Docker ist die Default-Antwort auf “wie deploye ich das”. Für Single-Host-KI-Services ist es meist die falsche. Hier ist, warum ich alles von Containern auf systemd-Units umgezogen habe:
Single-Host-Apps brauchen keine Container-Isolation. Mein MCP-Server ist ein Node-Prozess. Mein Telegram-Bot ist eine Bash-Loop. Beide in Container zu packen fügt eine Indirektionsschicht hinzu, die ein Problem löst, das ich nicht habe.
Logs integrieren mit dem OS. journalctl -u ticktick-mcp -f liefert Live-Tail ohne Extra-Tooling. Kein docker logs-Wrapper, kein Log-Driver-Config, kein Volume-Mount für Persistenz. Rotation läuft automatisch über journald.conf.
Reboots sind kostenlos. systemctl enable --now ticktick-mcp heisst: der Service kommt nach einem Kernel-Upgrade, einer VPS-Migration oder einem 3-Uhr-OOM-Event von alleine zurück. Docker mit --restart=always bringt Sie fast dahin, aber Sie brauchen trotzdem den Docker-Daemon zuerst.
Resource Limits ohne Container-Overhead. MemoryMax=2G in einem Unit-File ist eine Zeile. Kein cgroup-v2-Geknöpel, keine Docker-Runtime-Flags.
Timer behandeln Zeitzonen nativ. Das ist der grosse Punkt für geplante KI-Jobs. OnCalendar=*-*-* 06:30:00 Europe/Berlin regelt DST automatisch. Vixie-Cron unter Debian akzeptiert kein inline TZ= und lässt Ihren 06:30-Job zweimal im Jahr stillschweigend um 05:30 laufen.
Wann Docker trotzdem die richtige Wahl ist: Multi-Tenant-Deployments, CI-Pipelines, die Container-Images als Artefakte ausliefern, Apps mit harten Prozess-Isolation-Anforderungen aus Sicherheitsgründen oder wenn Sie denselben Workload über heterogene Hosts deployen. Für einen einzelnen VPS mit Services, die Sie selbst besitzen, gewinnt systemd.
Wenn Sie bei der Hardware noch nicht entschieden sind: ich nutze für all das einen Hetzner CX22 als Einstiegs-VPS, aufgerüstet auf CX32 sobald Last steigt. EU-Hosting, DSGVO-freundlich, keine US-Datenschiene.
Ein Unit-File, Zeile für Zeile erklärt
Hier ist das tatsächliche Unit-File, das meinen TickTick-MCP-Server in Produktion betreibt. Es kommt nach /etc/systemd/system/ticktick-mcp.service:
[Unit]
Description=TickTick MCP server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=debian
WorkingDirectory=/home/user/ticktick-mcp
ExecStart=/usr/bin/node /home/user/ticktick-mcp/ticktick-mcp-server.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
EnvironmentFile=/home/user/ticktick-mcp/.env
[Install]
WantedBy=multi-user.target
Jede Direktive im Detail:
Description: erscheint insystemctl statusund im Journal-Output. Kurz und identifizierbar halten.After=network-online.target: vor Service-Start auf Netzwerkverfügbarkeit warten. Kritisch für alles, was beim Boot ausgehende API-Calls macht.Wants=network-online.target: zieht das Network-Online-Target tatsächlich hoch.Afterallein bringt nichts, wenn das Target nicht aktiviert wird.Type=simple: der Prozess inExecStartist der Haupt-Service-Prozess. Nutzen SieType=notifyfür Apps mitsd_notify("READY=1")undType=forkingfür Daemons mit Double-Fork.User=debian: nie KI-Services alsrootlaufen lassen. Auf einen dedizierten User wechseln, der nur die Dateirechte hat, die er braucht.WorkingDirectory: löst relative Pfade in Ihrer App auf. Das Auslassen ist der häufigste Grund für “lokal funktioniert es, unter systemd nicht”.ExecStart: absoluter Pfad zum Interpreter, dann absoluter Pfad zum Skript. Relative Pfade scheitern, selbst mit gesetztemWorkingDirectory.which nodezeigt den Binary-Pfad.Restart=on-failure: Neustart, wenn der Prozess mit Non-Zero-Status endet. Nutzen SieRestart=alwaysfür lange laufende Inference-Server, die Memory leaken und periodische Bounces brauchen.RestartSec=5: 5 Sekunden zwischen Neustarts. Verhindert, dass Hot-Loop-Crashes das System hämmern.Environment=: inline Env-Variablen.EnvironmentFile=: lädt Secrets aus einer Datei, die nicht in Git eingecheckt ist. Eine ZeileKEY=valuepro Variable.WantedBy=multi-user.target: das Standard-Boot-Target für Server ohne GUI. Daran hängt sichenableein.
Für die TypeScript-Details des MCP-Server-Prozesses selbst siehe MCP-Server in TypeScript bauen.
Logs sind kostenlos und gut
journalctl ist die ganze Logging-Story. Ich habe nie einen Log-Aggregator für meinen VPS aufgesetzt, weil ich keinen brauche.
# Live mitschneiden
journalctl -u ticktick-mcp -f
# Letzte 50 Zeilen
journalctl -u ticktick-mcp -n 50
# Seit einem Zeitpunkt
journalctl -u ticktick-mcp --since "10 min ago"
journalctl -u ticktick-mcp --since "2026-04-17 06:00"
# Nur Fehler
journalctl -u ticktick-mcp -p err
# Alles seit diesem Boot
journalctl -u ticktick-mcp -b
Rotation passiert automatisch nach /etc/systemd/journald.conf. Per Default begrenzt journald den Disk-Verbrauch auf 10 Prozent des Filesystems. Für einen VPS mit KI-Services, die strukturiertes JSON loggen, hebe ich SystemMaxUse=1G an und gut.
Logging nach stdout aus Ihrem KI-Service ist der richtige Default. Schreiben Sie nicht selbst nach /var/log/myapp.log. Lassen Sie journald das übernehmen, lassen Sie journalctl darauf abfragen.
Resource Limits, die Sie wirklich brauchen
LLM-Inference-Server leaken Speicher. Agent-Loops halten Claude-API-Responses länger fest, als sie sollten. Bash-Skripte, die nach Python ausschellen, können mehr forken als gedacht. Drei Direktiven decken 90 Prozent ab:
[Service]
MemoryMax=4G
CPUQuota=200%
TasksMax=512
MemoryMax=4G: harte Grenze. Erreicht der Service sie, holt der OOM-Killer ihn ab und systemd startet ihn gemäss IhrerRestart=-Policy neu. Genau so überleben Sie ein Memory-Leak in einem lange laufenden Inference-Prozess.CPUQuota=200%: maximal zwei volle CPU-Kerne. Nutzen, wenn ein Service die anderen auf dem Host nicht aushungern darf.TasksMax=512: Kappe für Threads und Prozesse. Fängt Fork-Bomben durch Shell-Agenten, die Subprozesse in Schleifen rufen.
Für einen Inference-Server mit bekanntem Working-Set setze ich MemoryMax auf 80 Prozent dessen, was Modellgewichte plus realistischer KV-Cache brauchen, gepaart mit Restart=always. Bounces passieren, Service bleibt verfügbar.
Für Sizing des darunterliegenden VPS siehe meinen Linux-VPS-Setup-Guide für KI-Entwicklung.
Timer schlagen Cron für geplante KI-Jobs
Ich habe jeden Cron-Job auf meinem VPS auf systemd-Timer umgezogen. Der Grund ist Zeitzonen-Handling.
Vixie-Cron unter Debian ehrt keine inline TZ=-Direktiven. Sie können TZ=Europe/Berlin an den Crontab-Anfang setzen, das landet als Env-Variable im Skript, beeinflusst aber nicht das Scheduling. Ein Job für 06:30 läuft 06:30 UTC. Zweimal im Jahr, zur DST-Umstellung, feuert Ihr “Morning Briefing” eine Stunde zu früh oder zu spät.
systemd-Timer akzeptieren Zeitzonen direkt. Hier mein Morning-Briefing, das täglich 06:30 Berliner Zeit läuft.
/etc/systemd/system/morning-briefing.service:
[Unit]
Description=Morning briefing via Claude
[Service]
Type=oneshot
User=debian
ExecStart=/home/user/ticktick-mcp/morning-briefing.sh
/etc/systemd/system/morning-briefing.timer:
[Unit]
Description=Run morning briefing daily
[Timer]
OnCalendar=*-*-* 06:30:00 Europe/Berlin
Persistent=true
[Install]
WantedBy=timers.target
Dann:
sudo systemctl daemon-reload
sudo systemctl enable --now morning-briefing.timer
systemctl list-timers morning-briefing.timer
Persistent=true heisst: war die Maschine 06:30 aus, läuft der Job beim nächsten Boot nach. OnCalendar mit Europe/Berlin regelt DST. systemctl list-timers zeigt den nächsten Trigger-Zeitpunkt — das ist die Verifikation, bevor Sie weggehen.
Das ist das Pattern für jeden geplanten Agent. Die zehn AI-Agenten als Bash-Skripte in Produktion sind alle Timer-plus-Service-Paare.
Security-Hardening, das zählt
Sie müssen nicht jede Sandbox-Direktive aktivieren. Diese fünf bringen den grössten Isolationsgewinn ohne Ihre App zu zerlegen:
[Service]
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
ReadWritePaths=/home/user/ticktick-mcp /var/log/ticktick-mcp
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
NoNewPrivileges=true: der Service kann keine Privilegien über setuid-Binaries gewinnen. Immer sicher.ProtectSystem=strict: das gesamte Filesystem ist read-only für diesen Service, ausgenommen/dev,/proc,/sys.PrivateTmp=true: eigener/tmp-Namespace. Verhindert Temp-File-Kollisionen und Leaks zwischen Services.ReadWritePaths=: explizite Liste der Verzeichnisse, in die der Service schreiben darf. Damit schnitzen Sie Schreibrechte nach gesetztemProtectSystem=strictzurück.RestrictAddressFamilies=: erlaubt nur IPv4, IPv6 und Unix-Sockets. Blockiert Raw-Sockets, Netlink etc.
Diese Direktiven nach dem Funktionieren der App ergänzen. “Meine App kann ihr Config-File nicht schreiben” zu debuggen, während Sie gleichzeitig das initiale Unit-File schreiben, ist eine schlechte Zeit.
Deployment und Updates
Der Deployment-Flow, jedes Mal:
# 1. Unit-File ablegen
sudo cp ticktick-mcp.service /etc/systemd/system/
# 2. systemd zum Reload bringen, damit das neue Unit gelesen wird
sudo systemctl daemon-reload
# 3. Aktivieren (Boot-Start) und sofort starten
sudo systemctl enable --now ticktick-mcp
# 4. Verifizieren
systemctl status ticktick-mcp
journalctl -u ticktick-mcp -n 50
Update eines laufenden Services:
# App-Code oder Unit-File editieren
sudo systemctl daemon-reload # nur falls Unit-File geändert wurde
sudo systemctl restart ticktick-mcp
journalctl -u ticktick-mcp -f
Niemals daemon-reload nach dem Editieren eines Unit-Files überspringen. systemd cached das geparste Unit. Restart ohne Reload startet die alte Version neu.
Failure-Modi
Drei Patterns decken das meiste, was ich in der Praxis sehe:
Service startet ständig neu. journalctl -u <name> -n 100 prüfen. Meist fehlt eine Env-Variable, das Working-Directory existiert nicht oder ein Config-Pfad stimmt nicht. Die Restart-Loop ist das Symptom; der eigentliche Fehler steht im Log oberhalb der “Stopped”-Zeile.
start-limit-hit. Zu viele Restart-Failures in kurzem Zeitfenster. systemd gibt auf und markiert das Unit als failed. Ursache fixen, dann:
sudo systemctl reset-failed ticktick-mcp
sudo systemctl start ticktick-mcp
Binary nicht gefunden. Type=simple mit relativem Pfad in ExecStart scheitert. Immer absolute Pfade nutzen: /usr/bin/node, nicht node. /home/user/app/run.sh, nicht ./run.sh. WorkingDirectory setzt das CWD für den Prozess, aber das Binary-Lookup passiert davor.
Permission denied beim Schreiben. Sie haben ProtectSystem=strict ergänzt und vergessen, das Log-Verzeichnis in ReadWritePaths= aufzunehmen. Entweder ergänzen oder den Strict-Mode wieder rausnehmen, bis die App läuft.
Wann dieses Pattern, wann Container?
systemd-Services nutzen, wenn:
- Sie einen oder zwei Hosts betreiben und sie selbst besitzen.
- Ihre Services lange laufende Prozesse oder geplante Jobs sind.
- Sie OS-level Log-Aggregation und Resource Limits ohne Extra-Tooling wollen.
- Sie timezone-aware Scheduling brauchen.
Container nutzen, wenn:
- Sie denselben Workload über heterogene Hosts oder Clouds deployen.
- Ihre CI-Pipeline Container-Images als Release-Artefakt produziert.
- Sie starke Prozess-Isolation aus Multi-Tenant-Gründen brauchen.
- Ihr Team grösser als eine Person ist und Container-Orchestrierung Ihr Deployment-Koordinationsmittel ist.
Für einen einzelnen VPS mit MCP-Servern, Inference-Endpunkten und geplanten Agenten reicht systemd. Das Tooling ist bereits installiert, Logs rotieren bereits, die Reboot-Survival-Story funktioniert per Default.
Wenn Sie jemanden brauchen, der diese Infrastruktur einmalig sauber für Ihr Unternehmen aufsetzt: ein externer KI-Engineer macht das tageweise; bei strategischen Architektur-Entscheidungen lohnt eine kurze Session mit einem KI-Berater.