💡 Hinweis: Dieses Tutorial setzt voraus, dass du einen normalen Benutzer mit sudo-Rechten verwendest — wie im Tutorial Debian 13 Server absichern beschrieben.
1. Einleitung — Was ist systemd? Warum eigene Services?
Wer eine eigene Anwendung auf einem Linux-Server betreibt, kennt das Problem: Die App läuft wunderbar — bis der Server neu startet, die SSH-Session abbricht oder der Prozess abstürzt. Genau hier kommt systemd ins Spiel.
systemd ist das Init-System und der Service-Manager von Debian 13 (Trixie) und praktisch allen modernen Linux-Distributionen. Es ist der erste Prozess, der beim Booten startet (PID 1), und verwaltet den gesamten Lebenszyklus aller Dienste auf dem System — vom Netzwerk über die Datenbank bis hin zu eurer eigenen App.
Warum eigene Services?
Statt eure App manuell mit node app.js & oder nohup python3 main.py zu starten, bietet ein systemd-Service entscheidende Vorteile:
- Automatischer Start beim Booten — Kein manuelles Eingreifen nach einem Reboot
- Automatischer Neustart bei Absturz — Eure App steht nach Sekunden wieder
- Zentrales Logging — Alle Ausgaben landen in journald, abrufbar mit
journalctl - Abhängigkeitsmanagement — Wartet z.B. auf Netzwerk oder Datenbank
- Sicherheit — Sandboxing, eigene User, eingeschränkte Rechte
- Einheitliche Verwaltung —
sudo systemctl start/stop/restart/statusfür alles
In diesem Guide lernt ihr alles, was ihr braucht: von der Anatomie eines Unit-Files über praktische Beispiele mit Node.js, Python und .NET bis hin zu Timern, Hardening und Troubleshooting.
2. Voraussetzungen
Dieses Tutorial setzt einen laufenden Debian 13 (Trixie) Server voraus. Wenn ihr noch keinen habt, schaut euch zunächst unser erstes Tutorial an: Debian 13 Server-Grundsetup.
Was ihr mitbringen solltet:
- Root-Zugang oder ein User mit
sudo-Rechten - SSH-Verbindung zum Server
- Grundkenntnisse der Linux-Kommandozeile
- Optional: Eine lauffähige App (Node.js, Python oder .NET) — wir erstellen in den Beispielen auch einfache Demo-Apps
Stellt sicher, dass euer System aktuell ist:
sudo apt update && sudo apt upgrade -ysystemd ist auf Debian 13 bereits vorinstalliert. Prüft die Version:
systemctl --versionIhr solltet systemd 256 oder neuer sehen — Debian 13 liefert eine aktuelle Version mit.
3. systemd Grundlagen — Units, Targets, Abhängigkeiten
Bevor wir eigene Services schreiben, müssen wir verstehen, wie systemd denkt.
Units — Die Bausteine von systemd
Alles in systemd ist eine Unit. Es gibt verschiedene Typen, erkennbar an der Dateiendung:
| Unit-Typ | Endung | Zweck |
|---|---|---|
| Service | .service | Prozesse/Daemons verwalten |
| Timer | .timer | Zeitgesteuerte Aufgaben (wie Cron) |
| Socket | .socket | Socket-Aktivierung |
| Target | .target | Gruppierung von Units |
| Mount | .mount | Dateisystem-Mounts |
| Path | .path | Dateisystem-Überwachung |
| Slice | .slice | Ressourcen-Gruppierung (cgroups) |
Für dieses Tutorial konzentrieren wir uns auf .service und .timer Units.
Targets — Systemzustände
Targets sind Meilensteine beim Booten. Die wichtigsten:
sysinit.target— Grundlegende Systeminitialisierungnetwork.target— Netzwerk ist konfiguriertnetwork-online.target— Netzwerk ist tatsächlich erreichbarmulti-user.target— Mehrbenutzerbetrieb (entspricht dem alten Runlevel 3)graphical.target— Desktop-Umgebung (für Server irrelevant)
Die meisten Server-Services werden in multi-user.target eingehängt.
Wo liegen Unit-Files?
/lib/systemd/system/— Vom Paketmanager installierte Units (nicht bearbeiten!)/etc/systemd/system/— Eigene Units und Overrides (hier arbeiten wir)/run/systemd/system/— Temporäre Units (zur Laufzeit)
Die Priorität steigt von oben nach unten: Eine Datei in /etc/systemd/system/ überschreibt die gleichnamige in /lib/systemd/system/.
Wichtige systemctl-Befehle
# Service starten / stoppen / neustarten
sudo systemctl start meine-app.service
sudo systemctl stop meine-app.service
sudo systemctl restart meine-app.service
# Sanfter Neustart (Konfiguration neu laden, wenn die App es unterstützt)
sudo systemctl reload meine-app.service
# Status anzeigen
sudo systemctl status meine-app.service
# Beim Booten automatisch starten / deaktivieren
sudo systemctl enable meine-app.service
sudo systemctl disable meine-app.service
# Unit-Dateien neu einlesen (nach Änderungen!)
sudo systemctl daemon-reload
# Alle aktiven Services anzeigen
systemctl list-units --type=service
# Alle installierten Services (auch inaktive)
systemctl list-unit-files --type=service4. Erster eigener Service — Unit-File Anatomie
Ein Service-Unit-File besteht aus drei Sektionen: [Unit], [Service] und [Install]. Schauen wir uns die Anatomie anhand eines kommentierten Beispiels an:
[Unit]
# Beschreibung des Services (erscheint in systemctl status)
Description=Meine Beispiel-Anwendung
# Dokumentation (optional, aber guter Stil)
Documentation=https://example.com/docs
# Startreihenfolge: Erst nach diesen Units starten
After=network-online.target
# Weiche Abhängigkeit: Diese Unit soll mit gestartet werden
Wants=network-online.target
[Service]
# Typ des Services (siehe Erklärung unten)
Type=simple
# Welcher User führt den Prozess aus?
User=appuser
Group=appuser
# Arbeitsverzeichnis
WorkingDirectory=/opt/meine-app
# Umgebungsvariablen
Environment=NODE_ENV=production
Environment=PORT=3000
# Der eigentliche Startbefehl
ExecStart=/usr/bin/node /opt/meine-app/server.js
# Neustart-Verhalten
Restart=on-failure
RestartSec=5
# Maximale Startversuche (5 Versuche in 60 Sekunden)
StartLimitBurst=5
StartLimitIntervalSec=60
# Sauberes Beenden mit SIGTERM, nach 30s SIGKILL
KillSignal=SIGTERM
TimeoutStopSec=30
# Logging: stdout und stderr → journald
StandardOutput=journal
StandardError=journal
SyslogIdentifier=meine-app
[Install]
# In welchem Target soll der Service aktiviert werden?
WantedBy=multi-user.targetDie drei Sektionen erklärt
[Unit] — Metadaten und Abhängigkeiten
Hier beschreibt ihr, was der Service ist und wann er starten soll. Die wichtigsten Felder:
Description— Menschenlesbare BeschreibungAfter— Startreihenfolge (nicht Abhängigkeit!)Wants— Weiche Abhängigkeit (Service startet auch wenn die Abhängigkeit fehlt)Requires— Harte Abhängigkeit (Service scheitert wenn die Abhängigkeit fehlt)
[Service] — Das Herzstück
Hier definiert ihr, wie der Service läuft. Der Type ist besonders wichtig:
| Type | Bedeutung | Typischer Einsatz |
|---|---|---|
simple | Prozess startet und bleibt im Vordergrund | Node.js, Python, .NET, Go |
exec | Wie simple, aber „bereit“ erst nach erfolgreichem exec() | Bevorzugt für neue Services |
forking | Prozess forkt sich in den Hintergrund | Klassische Daemons (Apache, nginx) |
oneshot | Prozess läuft einmal und beendet sich | Initialisierungs-Skripte |
notify | Prozess meldet Bereitschaft an systemd | Anwendungen mit sd_notify()-Support |
Empfehlung: Für die meisten eigenen Apps ist Type=exec oder Type=simple die richtige Wahl.
[Install] — Aktivierung
Definiert, was passiert wenn ihr sudo systemctl enable ausführt. WantedBy=multi-user.target bedeutet: Der Service startet automatisch im normalen Mehrbenutzerbetrieb.
5. Praxis: Node.js App als Service
Starten wir mit einem konkreten Beispiel. Wir erstellen eine einfache Node.js-App und machen sie zum systemd-Service.
Schritt 1: App vorbereiten
# Einen eigenen User erstellen (Sicherheit!)
sudo useradd --system --shell /usr/sbin/nologin --create-home --home-dir /opt/nodeapp nodeapp
# Verzeichnis vorbereiten
sudo mkdir -p /opt/nodeapp
sudo chown nodeapp:nodeapp /opt/nodeapp
# Als nodeapp-User die App erstellen
sudo -u nodeapp bash -c 'cat > /opt/nodeapp/server.js << "EOF"
const http = require("http");
const PORT = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello from systemd-managed Node.js!\n");
});
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// Sauberes Herunterfahren bei SIGTERM
process.on("SIGTERM", () => {
console.log("SIGTERM received, shutting down gracefully...");
server.close(() => process.exit(0));
});
EOF'Schritt 2: Unit-File erstellen
sudo nano /etc/systemd/system/nodeapp.serviceInhalt:
[Unit]
Description=Node.js Demo Application
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
User=nodeapp
Group=nodeapp
WorkingDirectory=/opt/nodeapp
# Node.js-Pfad explizit angeben (which node)
ExecStart=/usr/bin/node server.js
Environment=NODE_ENV=production
Environment=PORT=3000
Restart=on-failure
RestartSec=5
StartLimitBurst=5
StartLimitIntervalSec=60
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nodeapp
[Install]
WantedBy=multi-user.targetSchritt 3: Service aktivieren und starten
# systemd über die neue Unit informieren
sudo systemctl daemon-reload
# Service starten
sudo systemctl start nodeapp
# Status prüfen
sudo systemctl status nodeapp
# Beim Booten automatisch starten
sudo systemctl enable nodeappIhr solltet eine Ausgabe wie diese sehen:
● nodeapp.service - Node.js Demo Application
Loaded: loaded (/etc/systemd/system/nodeapp.service; enabled)
Active: active (running) since ...
Main PID: 12345 (node)
CGroup: /system.slice/nodeapp.service
└─12345 /usr/bin/node server.jsSchritt 4: Testen
# App testen
curl http://localhost:3000
# Logs anschauen
sudo journalctl -u nodeapp -f
# Absturz simulieren — systemd startet die App automatisch neu
sudo kill -9 $(systemctl show nodeapp --property=MainPID --value)6. Praxis: Python App als Service
Python-Apps bringen ein paar Besonderheiten mit: Virtual Environments und die Wahl zwischen WSGI-Servern.
Schritt 1: App vorbereiten
# System-User erstellen
sudo useradd --system --shell /usr/sbin/nologin --create-home --home-dir /opt/pythonapp pythonapp
# Verzeichnis und venv vorbereiten
sudo mkdir -p /opt/pythonapp
sudo chown pythonapp:pythonapp /opt/pythonapp
# Als pythonapp-User arbeiten
sudo -u pythonapp bash -c '
cd /opt/pythonapp
python3 -m venv venv
source venv/bin/activate
pip install flask gunicorn
'
# App erstellen
sudo -u pythonapp cat > /opt/pythonapp/app.py << 'EOF'
from flask import Flask
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
@app.route("/")
def hello():
app.logger.info("Request received")
return "Hello from systemd-managed Python!\n"
@app.route("/health")
def health():
return "OK", 200
EOFSchritt 2: Unit-File erstellen
sudo nano /etc/systemd/system/pythonapp.serviceInhalt:
[Unit]
Description=Python Flask Application (Gunicorn)
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
User=pythonapp
Group=pythonapp
WorkingDirectory=/opt/pythonapp
# Gunicorn aus dem venv starten — kein "source activate" nötig!
ExecStart=/opt/pythonapp/venv/bin/gunicorn \
--workers 3 \
--bind 0.0.0.0:8000 \
--access-logfile - \
--error-logfile - \
app:app
# Umgebungsvariablen für die App
Environment=FLASK_ENV=production
Environment=PYTHONUNBUFFERED=1
Restart=on-failure
RestartSec=5
# Gunicorn braucht etwas länger zum Herunterfahren
TimeoutStopSec=30
KillSignal=SIGTERM
StandardOutput=journal
StandardError=journal
SyslogIdentifier=pythonapp
[Install]
WantedBy=multi-user.targetWichtig: Wir verwenden den absoluten Pfad zum Gunicorn im Virtual Environment (/opt/pythonapp/venv/bin/gunicorn). So müssen wir kein source activate ausführen — systemd braucht das nicht.
PYTHONUNBUFFERED=1 ist wichtig! Ohne diese Variable puffert Python die Ausgabe, und eure Logs erscheinen verzögert oder gar nicht in journald.
Schritt 3: Aktivieren und Testen
sudo systemctl daemon-reload
sudo systemctl start pythonapp
sudo systemctl enable pythonapp
sudo systemctl status pythonapp
# Testen
curl http://localhost:8000
curl http://localhost:8000/health7. Praxis: .NET App als Service
.NET-Anwendungen lassen sich unter Linux hervorragend als systemd-Service betreiben. Microsoft unterstützt sogar native systemd-Integration.
Schritt 1: .NET SDK installieren
# Microsoft-Paketquelle hinzufügen (falls nicht vorhanden)
wget https://packages.microsoft.com/config/debian/13/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
sudo apt update
sudo apt install -y dotnet-sdk-8.0Schritt 2: App erstellen und veröffentlichen
# Beispiel-App erstellen
mkdir -p /tmp/dotnetapp
cd /tmp/dotnetapp
dotnet new web -n MyWebApp
cd MyWebApp
# Für Produktion veröffentlichen
dotnet publish -c Release -o /opt/dotnetapp
# System-User und Berechtigungen
sudo useradd --system --shell /usr/sbin/nologin --create-home --home-dir /opt/dotnetapp dotnetapp
sudo chown -R dotnetapp:dotnetapp /opt/dotnetappSchritt 3: Unit-File erstellen
sudo nano /etc/systemd/system/dotnetapp.serviceInhalt:
[Unit]
Description=.NET Web Application
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
User=dotnetapp
Group=dotnetapp
WorkingDirectory=/opt/dotnetapp
ExecStart=/usr/bin/dotnet /opt/dotnetapp/MyWebApp.dll
# .NET-spezifische Umgebungsvariablen
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://0.0.0.0:5000
Environment=DOTNET_CLI_TELEMETRY_OPTOUT=1
# .NET unterstützt sd_notify() → Type=notify möglich
# Damit weiß systemd genau, wann die App bereit ist
Restart=on-failure
RestartSec=10
TimeoutStopSec=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=dotnetapp
[Install]
WantedBy=multi-user.targetHinweis: .NET-Apps ab ASP.NET Core 8.0 unterstützen Type=notify nativ mit dem Microsoft.Extensions.Hosting.Systemd-Paket. Fügt in eurer Program.cs hinzu:
builder.Host.UseSystemd();Und in der .csproj:
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.0" />Schritt 4: Aktivieren
sudo systemctl daemon-reload
sudo systemctl start dotnetapp
sudo systemctl enable dotnetapp
curl http://localhost:50008. Wichtige Direktiven im Detail
Nachdem wir drei Praxisbeispiele gesehen haben, schauen wir uns die wichtigsten Direktiven genauer an.
Restart — Neustart-Verhalten
| Wert | Bedeutung |
|---|---|
no | Kein automatischer Neustart (Standard) |
on-failure | Nur bei Fehler (Exit-Code ≠ 0, Signal, Timeout) |
on-abnormal | Bei Signal, Timeout, Watchdog |
on-abort | Nur bei unkontrolliertem Signal (SIGABRT, SIGSEGV) |
always | Immer — auch nach sauberem Exit |
Empfehlung: on-failure für die meisten Dienste. always nur wenn der Dienst wirklich nie aufhören soll (z.B. Worker-Prozesse).
RestartSec — Verzögerung vor Neustart
RestartSec=5Wartet 5 Sekunden vor dem Neustart. Verhindert eine Endlosschleife bei sofortigem Absturz. Werte zwischen 3 und 10 Sekunden sind sinnvoll.
WorkingDirectory — Arbeitsverzeichnis
WorkingDirectory=/opt/meine-appSetzt das Arbeitsverzeichnis des Prozesses. Relative Pfade in der App beziehen sich dann auf dieses Verzeichnis. Muss existieren, sonst scheitert der Start.
Environment vs. EnvironmentFile
# Direkt im Unit-File
Environment=NODE_ENV=production
Environment=PORT=3000
# Oder aus einer Datei laden (besser für Secrets!)
EnvironmentFile=/opt/meine-app/.envDas Format der .env-Datei:
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost/mydb
SECRET_KEY=supersecret123Wichtig: Die Datei darf keine export-Statements enthalten und keine Quotes um Werte. Sichert die Datei mit restriktiven Rechten:
sudo chmod 600 /opt/meine-app/.env
sudo chown appuser:appuser /opt/meine-app/.envUser und Group — Prozess-Identität
User=appuser
Group=appuserNiemals eigene Apps als root laufen lassen! Erstellt immer einen dedizierten System-User:
sudo useradd --system --shell /usr/sbin/nologin --create-home --home-dir /opt/meine-app appuserDie Flags bedeuten:
--system— System-User (keine UID aus dem normalen Bereich, kein Aging)--shell /usr/sbin/nologin— Kein Login möglich--create-home— Home-Verzeichnis anlegen
ExecStartPre / ExecStartPost — Vor- und Nachbereitung
# Vor dem Start: Konfiguration prüfen
ExecStartPre=/opt/meine-app/check-config.sh
# Vor dem Start: Verzeichnis anlegen
ExecStartPre=/usr/bin/mkdir -p /var/run/meine-app
# Nach dem Start: Health-Check
ExecStartPost=/usr/bin/curl -sf http://localhost:3000/healthEin - vor dem Befehl bedeutet: Fehler ignorieren (nicht kritisch):
ExecStartPre=-/usr/bin/mkdir -p /var/run/meine-appExecReload — Konfiguration neu laden
# SIGHUP senden (viele Daemons laden daraufhin die Config neu)
ExecReload=/bin/kill -HUP $MAINPIDDanach könnt ihr sudo systemctl reload meine-app verwenden — ein Neustart der App ohne Downtime.
9. Logging mit journalctl
Einer der größten Vorteile von systemd: Zentrales, strukturiertes Logging mit journald. Alles, was eure App auf stdout/stderr schreibt, landet automatisch im Journal.
Logs lesen
# Alle Logs eines Services
sudo journalctl -u meine-app
# Nur die letzten 50 Zeilen
sudo journalctl -u meine-app -n 50
# Live-Log (wie tail -f)
sudo journalctl -u meine-app -f
# Logs seit dem letzten Boot
sudo journalctl -u meine-app -b
# Logs eines bestimmten Zeitraums
sudo journalctl -u meine-app --since "2025-01-15 10:00" --until "2025-01-15 12:00"
# Nur Fehler (Priority error und höher)
sudo journalctl -u meine-app -p err
# JSON-Ausgabe (für Weiterverarbeitung)
sudo journalctl -u meine-app -o json-prettyLog-Prioritäten
| Priorität | Keyword | Bedeutung |
|---|---|---|
| 0 | emerg | System ist unbrauchbar |
| 1 | alert | Sofortiges Handeln nötig |
| 2 | crit | Kritischer Zustand |
| 3 | err | Fehlerbedingungen |
| 4 | warning | Warnungen |
| 5 | notice | Normal, aber bemerkenswert |
| 6 | info | Informationen |
| 7 | debug | Debug-Meldungen |
Log-Rotation und Speicherplatz
journald verwaltet den Speicherplatz automatisch. Die Konfiguration findet ihr in /etc/systemd/journald.conf:
sudo nano /etc/systemd/journald.confWichtige Einstellungen:
[Journal]
# Maximale Größe auf der Festplatte
SystemMaxUse=500M
# Maximale Größe einer einzelnen Datei
SystemMaxFileSize=50M
# Wie lange Logs aufbewahren
MaxRetentionSec=30day
# Komprimierung aktivieren
Compress=yes
# Persistent speichern (überlebt Reboot)
Storage=persistentNach Änderungen:
sudo systemctl restart systemd-journaldLogs persistent machen
Standardmäßig speichert Debian Logs persistent, aber stellt sicher, dass das Verzeichnis existiert:
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journalDisk-Verbrauch prüfen
# Wie viel Speicher nutzen die Logs?
journalctl --disk-usage
# Alte Logs aufräumen (auf 200MB begrenzen)
sudo journalctl --vacuum-size=200M
# Logs älter als 14 Tage löschen
sudo journalctl --vacuum-time=14d10. systemd Timer statt Cron
systemd Timer sind der moderne Ersatz für Cron-Jobs — mit besserer Logging-Integration, Abhängigkeitsmanagement und flexiblerer Zeitsteuerung.
Aufbau: Service + Timer
Ein Timer besteht aus zwei Dateien: einem .service (was soll passieren?) und einem .timer (wann soll es passieren?).
Beispiel: Tägliches Backup-Skript
Zuerst der Service (/etc/systemd/system/backup.service):
[Unit]
Description=Tägliches Datenbank-Backup
[Service]
Type=oneshot
User=backup
ExecStart=/opt/scripts/backup.sh
StandardOutput=journal
SyslogIdentifier=backupKein [Install]-Abschnitt nötig — der Timer kümmert sich um die Aktivierung.
Dann der Timer (/etc/systemd/system/backup.timer):
[Unit]
Description=Tägliches Backup um 03:00
[Timer]
# Kalenderbasiert (wie Cron)
OnCalendar=*-*-* 03:00:00
# Verpasste Ausführungen nachholen (z.B. nach Downtime)
Persistent=true
# Zufällige Verzögerung (verhindert Lastspitzen)
RandomizedDelaySec=300
# Genauigkeit (1s = sofort, Standard ist 1min)
AccuracySec=1s
[Install]
WantedBy=timers.targetTimer aktivieren
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
# Prüfen wann der nächste Lauf ist
systemctl list-timers backup.timer
# Timer-Status anschauen
systemctl status backup.timer
# Manuell auslösen (zum Testen)
sudo systemctl start backup.serviceOnCalendar — Zeitformat
Das Format ist flexibel und lesbarer als Cron-Syntax:
# Jeden Tag um 03:00
OnCalendar=*-*-* 03:00:00
# Kurzform: Jeden Tag um 03:00
OnCalendar=daily
# Jeden Montag um 06:00
OnCalendar=Mon *-*-* 06:00:00
# Alle 4 Stunden
OnCalendar=*-*-* 00/4:00:00
# Erster Tag jeden Monats
OnCalendar=*-*-01 00:00:00
# Alle 15 Minuten
OnCalendar=*-*-* *:00/15:00
# Wochentags (Mo-Fr) um 09:00
OnCalendar=Mon..Fri *-*-* 09:00:00Tipp: Testet eure Zeitangaben mit systemd-analyze calendar:
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
# Zeigt: Nächster Auslösezeitpunkt, Normalisierte Form, etc.Monotone Timer (relativ zum Boot)
[Timer]
# 5 Minuten nach dem Booten
OnBootSec=5min
# Alle 30 Minuten (nach dem ersten Lauf)
OnUnitActiveSec=30min
# Kombination
OnBootSec=5min
OnUnitActiveSec=1hVergleich: Cron vs. systemd Timer
| Feature | Cron | systemd Timer |
|---|---|---|
| Logging | Manuell (Redirect) | Automatisch (journald) |
| Verpasste Läufe | Verloren | Persistent=true |
| Abhängigkeiten | Keine | Volle systemd-Abhängigkeiten |
| Zufällige Verzögerung | Manuell (sleep) | RandomizedDelaySec |
| Ressourcen-Limits | Keine | cgroups, Sandboxing |
| Überwachung | Schwierig | sudo systemctl list-timers |
11. Hardening — Service absichern
systemd bietet umfangreiche Sandboxing-Optionen, um Services abzusichern. Selbst wenn eure App kompromittiert wird, begrenzt das Hardening den Schaden.
Basis-Hardening (immer verwenden)
[Service]
# Verhindert Rechte-Eskalation
NoNewPrivileges=true
# Eigenes /tmp-Verzeichnis
PrivateTmp=true
# /home, /root, /run/user unzugänglich
ProtectHome=true
# Dateisystem schreibschützen
ProtectSystem=strict
# Schreibzugriff nur wo nötig
ReadWritePaths=/opt/meine-app/data /var/log/meine-app
# Kernel-Variablen schützen
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
# Control-Groups schützen
ProtectControlGroups=true
# Kein Zugriff auf /proc-Informationen anderer Prozesse
ProtectProc=invisible
ProcSubset=pid
# Clock nicht veränderbar
ProtectClock=true
# Hostname nicht änderbar
ProtectHostname=trueNetzwerk einschränken
# Nur bestimmte Netzwerk-Familien erlauben
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# Kein Netzwerkzugriff (für reine Batch-Jobs)
# PrivateNetwork=trueSystemcalls einschränken
# Nur bestimmte Syscall-Gruppen erlauben
SystemCallFilter=@system-service
SystemCallArchitectures=native
SystemCallErrorNumber=EPERMRessourcen begrenzen
# Maximal 512 MB RAM
MemoryMax=512M
# Maximal 50% CPU
CPUQuota=50%
# Maximal 100 Prozesse
LimitNPROC=100
# Maximal 1024 offene Dateien
LimitNOFILE=1024Vollständiges Hardening-Beispiel
Hier ein komplettes Unit-File mit allen sinnvollen Hardening-Optionen für eine Webanwendung:
[Unit]
Description=Gehärtete Webanwendung
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
User=webapp
Group=webapp
WorkingDirectory=/opt/webapp
ExecStart=/opt/webapp/venv/bin/gunicorn --bind 127.0.0.1:8000 app:app
Restart=on-failure
RestartSec=5
# === HARDENING ===
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/opt/webapp/data
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallFilter=@system-service
SystemCallArchitectures=native
PrivateDevices=true
PrivateUsers=true
MemoryMax=512M
CPUQuota=50%
LockPersonality=true
RemoveIPC=true
UMask=0077
[Install]
WantedBy=multi-user.targetHardening analysieren
systemd hat ein eingebautes Analyse-Tool:
# Sicherheits-Score anzeigen (0 = perfekt, 10 = unsicher)
systemd-analyze security meine-app.serviceDieses Tool zeigt für jede Option, ob sie gesetzt ist und wie sie den Score beeinflusst. Ziel ist ein Wert unter 5.
12. Abhängigkeiten und Reihenfolge
systemd unterscheidet strikt zwischen Reihenfolge (ordering) und Abhängigkeit (dependency). Das ist einer der häufigsten Stolpersteine.
Reihenfolge: After / Before
# Starte NACH postgresql
After=postgresql.service
# Starte VOR nginx
Before=nginx.serviceAfter und Before definieren nur die Reihenfolge, nicht ob die andere Unit überhaupt gestartet wird!
Abhängigkeit: Wants / Requires / BindsTo
# Weiche Abhängigkeit: Versuche postgresql zu starten, aber mein Service
# startet auch wenn postgresql nicht verfügbar ist
Wants=postgresql.service
# Harte Abhängigkeit: Starte postgresql. Wenn es scheitert, scheitere auch ich
Requires=postgresql.service
# Noch härter: Wenn postgresql stoppt oder abstürzt, stoppe auch ich
BindsTo=postgresql.serviceKombination ist der Schlüssel
Typisches Muster — App die eine Datenbank braucht:
[Unit]
Description=Meine App (braucht PostgreSQL)
# Reihenfolge: Starte nach PostgreSQL und Netzwerk
After=network-online.target postgresql.service
# Abhängigkeit: PostgreSQL soll mitgestartet werden
Wants=network-online.target
Requires=postgresql.serviceWarum beides? Requires=postgresql.service ohne After=postgresql.service würde beide Services gleichzeitig starten — was nicht funktioniert, wenn eure App beim Start sofort die Datenbank braucht.
Abhängigkeiten visualisieren
# Alle Abhängigkeiten eines Services anzeigen
systemctl list-dependencies meine-app.service
# Umgekehrt: Wer hängt von diesem Service ab?
systemctl list-dependencies --reverse meine-app.service
# Grafische Darstellung der Boot-Reihenfolge
systemd-analyze dot meine-app.service | dot -Tsvg > deps.svgReihenfolge der Startzeit analysieren
# Wie lange hat der Boot gedauert?
systemd-analyze
# Welche Services haben am längsten gebraucht?
systemd-analyze blame
# Kritischer Pfad (was hat den Boot am meisten verzögert?)
systemd-analyze critical-chain meine-app.service13. Zusammenfassung und Checkliste
Hier eine Checkliste für jeden neuen systemd-Service:
Checkliste: Neuen Service einrichten
- System-User erstellen
sudo useradd --system --shell /usr/sbin/nologin --create-home --home-dir /opt/app appuser - App-Verzeichnis vorbereiten
sudo chown -R appuser:appuser /opt/app - Unit-File erstellen
sudo nano /etc/systemd/system/app.service - Secrets in EnvironmentFile auslagern
sudo chmod 600 /opt/app/.env - daemon-reload ausführen
sudo systemctl daemon-reload - Service starten und Status prüfen
sudo systemctl start app sudo systemctl status app - Logs prüfen
sudo journalctl -u app -f - Autostart aktivieren
sudo systemctl enable app - Hardening hinzufügen — Mindestens:
NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true - Sicherheits-Score prüfen
systemd-analyze security app.service - Reboot testen!
sudo reboot # Nach Reboot: systemctl status app
Schnellreferenz: Die wichtigsten Befehle
| Aktion | Befehl |
|---|---|
| Service starten | sudo systemctl start app |
| Service stoppen | sudo systemctl stop app |
| Service neustarten | sudo systemctl restart app |
| Status prüfen | sudo systemctl status app |
| Autostart an | sudo systemctl enable app |
| Autostart aus | sudo systemctl disable app |
| Unit-Dateien neu laden | sudo systemctl daemon-reload |
| Logs lesen | sudo journalctl -u app |
| Live-Logs | sudo journalctl -u app -f |
| Timer auflisten | sudo systemctl list-timers |
| Sicherheits-Check | systemd-analyze security app |
14. Troubleshooting
Wenn euer Service nicht startet oder sich seltsam verhält, geht systematisch vor.
Service startet nicht
# 1. Status anschauen — meistens steht hier schon die Ursache
sudo systemctl status meine-app.service
# 2. Detaillierte Logs
sudo journalctl -u meine-app -n 50 --no-pager
# 3. Unit-File auf Syntaxfehler prüfen
sudo systemd-analyze verify /etc/systemd/system/meine-app.service
# 4. Manuell als der Service-User testen
sudo -u appuser /usr/bin/node /opt/meine-app/server.jsHäufige Fehler und Lösungen
203/EXEC — Programm nicht gefunden
# Ursache: Pfad in ExecStart ist falsch
# Lösung: Absoluten Pfad prüfen
which node
# → /usr/bin/node (diesen Pfad in ExecStart verwenden)217/USER — User existiert nicht
# Ursache: Der in User= angegebene User existiert nicht
# Lösung:
sudo useradd --system appuser200/CHDIR — WorkingDirectory existiert nicht
# Ursache: Das Verzeichnis in WorkingDirectory existiert nicht
# Lösung:
sudo mkdir -p /opt/meine-app
sudo chown appuser:appuser /opt/meine-appService startet und stoppt sofort (Restart-Loop)
# Ursache: App crasht sofort — Start-Limit erreicht
# Diagnose:
sudo journalctl -u meine-app --since "5 min ago"
# Start-Limit zurücksetzen:
sudo systemctl reset-failed meine-app
sudo systemctl start meine-appBerechtigungsprobleme
# Ursache: App kann nicht auf Dateien zugreifen
# Häufig bei ProtectSystem=strict — ReadWritePaths vergessen!
# Diagnose: Was versucht die App zu tun?
sudo journalctl -u meine-app | grep -i "permission\|denied\|EACCES"
# Lösung: ReadWritePaths ergänzen
ReadWritePaths=/opt/meine-app/data /var/log/meine-appDependency failed
# Ursache: Eine Abhängigkeit (Requires=) ist fehlgeschlagen
# Diagnose:
systemctl list-dependencies meine-app --plain | head -20
systemctl status postgresql.service # oder andere Abhängigkeit
# Lösung: Abhängigen Service erst reparieren und startendaemon-reload vergessen
# Symptom: Änderungen am Unit-File haben keine Wirkung
# systemd zeigt diese Warnung:
# Warning: The unit file, source configuration file or drop-ins of meine-app.service changed on disk.
# Run 'systemctl daemon-reload' to reload units.
sudo systemctl daemon-reload
sudo systemctl restart meine-appNützliche Debug-Befehle
# Alle Properties eines Services anzeigen
systemctl show meine-app.service
# Nur bestimmte Properties
systemctl show meine-app --property=MainPID,ActiveState,SubState
# Welche Umgebungsvariablen sieht der Prozess?
sudo cat /proc/$(systemctl show meine-app --property=MainPID --value)/environ | tr '\0' '\n'
# Boot-Analyse: Was hat wann gestartet?
systemd-analyze plot > boot.svg15. Nächste Schritte
Ihr habt jetzt das Handwerkszeug, um eigene Services auf Debian 13 zu erstellen und professionell zu betreiben. Hier einige Ideen, wie ihr weitermachen könnt:
- Reverse Proxy mit Nginx/Caddy — Eure App hinter einen Webserver setzen (kommt im nächsten Tutorial!)
- Socket-Aktivierung — Services erst bei Bedarf starten (spart Ressourcen)
- Watchdog — systemd überwacht regelmäßig ob eure App noch reagiert
- Template Units — Ein Unit-File für mehrere Instanzen (
app@1.service,app@2.service) - User Services — Services ohne Root-Rechte verwalten (
sudo systemctl --user) - Monitoring — systemd-Metriken mit Prometheus/Grafana überwachen
Weiterführende Ressourcen
- systemd Dokumentation (freedesktop.org)
man systemd.service— Lokale Man-Page mit allen Direktivenman systemd.exec— Alle Ausführungs-Optionen (Hardening etc.)man systemd.timer— Timer-Dokumentation- Arch Wiki: systemd — Exzellente Referenz
Viel Erfolg beim Deployen eurer Apps! Bei Fragen hinterlasst gerne einen Kommentar.
Schreibe einen Kommentar