💡 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 Verwaltungsudo systemctl start/stop/restart/status fü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 -y

systemd ist auf Debian 13 bereits vorinstalliert. Prüft die Version:

systemctl --version

Ihr 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-TypEndungZweck
Service.serviceProzesse/Daemons verwalten
Timer.timerZeitgesteuerte Aufgaben (wie Cron)
Socket.socketSocket-Aktivierung
Target.targetGruppierung von Units
Mount.mountDateisystem-Mounts
Path.pathDateisystem-Überwachung
Slice.sliceRessourcen-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 Systeminitialisierung
  • network.target — Netzwerk ist konfiguriert
  • network-online.target — Netzwerk ist tatsächlich erreichbar
  • multi-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=service

4. 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.target

Die 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 Beschreibung
  • After — 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:

TypeBedeutungTypischer Einsatz
simpleProzess startet und bleibt im VordergrundNode.js, Python, .NET, Go
execWie simple, aber „bereit“ erst nach erfolgreichem exec()Bevorzugt für neue Services
forkingProzess forkt sich in den HintergrundKlassische Daemons (Apache, nginx)
oneshotProzess läuft einmal und beendet sichInitialisierungs-Skripte
notifyProzess meldet Bereitschaft an systemdAnwendungen 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.service

Inhalt:

[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.target

Schritt 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 nodeapp

Ihr 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.js

Schritt 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
EOF

Schritt 2: Unit-File erstellen

sudo nano /etc/systemd/system/pythonapp.service

Inhalt:

[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.target

Wichtig: 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/health

7. 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.0

Schritt 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/dotnetapp

Schritt 3: Unit-File erstellen

sudo nano /etc/systemd/system/dotnetapp.service

Inhalt:

[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.target

Hinweis: .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:5000

8. Wichtige Direktiven im Detail

Nachdem wir drei Praxisbeispiele gesehen haben, schauen wir uns die wichtigsten Direktiven genauer an.

Restart — Neustart-Verhalten

WertBedeutung
noKein automatischer Neustart (Standard)
on-failureNur bei Fehler (Exit-Code ≠ 0, Signal, Timeout)
on-abnormalBei Signal, Timeout, Watchdog
on-abortNur bei unkontrolliertem Signal (SIGABRT, SIGSEGV)
alwaysImmer — 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=5

Wartet 5 Sekunden vor dem Neustart. Verhindert eine Endlosschleife bei sofortigem Absturz. Werte zwischen 3 und 10 Sekunden sind sinnvoll.

WorkingDirectory — Arbeitsverzeichnis

WorkingDirectory=/opt/meine-app

Setzt 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/.env

Das Format der .env-Datei:

NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost/mydb
SECRET_KEY=supersecret123

Wichtig: 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/.env

User und Group — Prozess-Identität

User=appuser
Group=appuser

Niemals 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 appuser

Die 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/health

Ein - vor dem Befehl bedeutet: Fehler ignorieren (nicht kritisch):

ExecStartPre=-/usr/bin/mkdir -p /var/run/meine-app

ExecReload — Konfiguration neu laden

# SIGHUP senden (viele Daemons laden daraufhin die Config neu)
ExecReload=/bin/kill -HUP $MAINPID

Danach 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-pretty

Log-Prioritäten

PrioritätKeywordBedeutung
0emergSystem ist unbrauchbar
1alertSofortiges Handeln nötig
2critKritischer Zustand
3errFehlerbedingungen
4warningWarnungen
5noticeNormal, aber bemerkenswert
6infoInformationen
7debugDebug-Meldungen

Log-Rotation und Speicherplatz

journald verwaltet den Speicherplatz automatisch. Die Konfiguration findet ihr in /etc/systemd/journald.conf:

sudo nano /etc/systemd/journald.conf

Wichtige 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=persistent

Nach Änderungen:

sudo systemctl restart systemd-journald

Logs 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/journal

Disk-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=14d

10. 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=backup

Kein [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.target

Timer 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.service

OnCalendar — 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:00

Tipp: 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=1h

Vergleich: Cron vs. systemd Timer

FeatureCronsystemd Timer
LoggingManuell (Redirect)Automatisch (journald)
Verpasste LäufeVerlorenPersistent=true
AbhängigkeitenKeineVolle systemd-Abhängigkeiten
Zufällige VerzögerungManuell (sleep)RandomizedDelaySec
Ressourcen-LimitsKeinecgroups, Sandboxing
ÜberwachungSchwierigsudo 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=true

Netzwerk einschränken

# Nur bestimmte Netzwerk-Familien erlauben
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

# Kein Netzwerkzugriff (für reine Batch-Jobs)
# PrivateNetwork=true

Systemcalls einschränken

# Nur bestimmte Syscall-Gruppen erlauben
SystemCallFilter=@system-service
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM

Ressourcen begrenzen

# Maximal 512 MB RAM
MemoryMax=512M

# Maximal 50% CPU
CPUQuota=50%

# Maximal 100 Prozesse
LimitNPROC=100

# Maximal 1024 offene Dateien
LimitNOFILE=1024

Vollstä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.target

Hardening analysieren

systemd hat ein eingebautes Analyse-Tool:

# Sicherheits-Score anzeigen (0 = perfekt, 10 = unsicher)
systemd-analyze security meine-app.service

Dieses 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.service

After 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.service

Kombination 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.service

Warum 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.svg

Reihenfolge 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.service

13. Zusammenfassung und Checkliste

Hier eine Checkliste für jeden neuen systemd-Service:

Checkliste: Neuen Service einrichten

  1. System-User erstellen
    sudo useradd --system --shell /usr/sbin/nologin --create-home --home-dir /opt/app appuser
  2. App-Verzeichnis vorbereiten
    sudo chown -R appuser:appuser /opt/app
  3. Unit-File erstellen
    sudo nano /etc/systemd/system/app.service
  4. Secrets in EnvironmentFile auslagern
    sudo chmod 600 /opt/app/.env
  5. daemon-reload ausführen
    sudo systemctl daemon-reload
  6. Service starten und Status prüfen
    sudo systemctl start app
    sudo systemctl status app
  7. Logs prüfen
    sudo journalctl -u app -f
  8. Autostart aktivieren
    sudo systemctl enable app
  9. Hardening hinzufügen — Mindestens:
    NoNewPrivileges=true
    PrivateTmp=true
    ProtectSystem=strict
    ProtectHome=true
  10. Sicherheits-Score prüfen
    systemd-analyze security app.service
  11. Reboot testen!
    sudo reboot
    # Nach Reboot:
    systemctl status app

Schnellreferenz: Die wichtigsten Befehle

AktionBefehl
Service startensudo systemctl start app
Service stoppensudo systemctl stop app
Service neustartensudo systemctl restart app
Status prüfensudo systemctl status app
Autostart ansudo systemctl enable app
Autostart aussudo systemctl disable app
Unit-Dateien neu ladensudo systemctl daemon-reload
Logs lesensudo journalctl -u app
Live-Logssudo journalctl -u app -f
Timer auflistensudo systemctl list-timers
Sicherheits-Checksystemd-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.js

Hä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 appuser

200/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-app

Service 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-app

Berechtigungsprobleme

# 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-app

Dependency 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 starten

daemon-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-app

Nü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.svg

15. 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

Viel Erfolg beim Deployen eurer Apps! Bei Fragen hinterlasst gerne einen Kommentar.