Cronjobs liefern dir keinen Exit-Status, kein strukturiertes Log und keine Möglichkeit, einen Lauf nachzuholen, der während eines Reboots ausgefallen ist. systemd-Timer lösen genau diese Punkte: Jeder Lauf ist eine eigene Unit mit Statuscode, Journal-Eintrag und optionalen Ressourcenlimits. Diese Anleitung zeigt dir am Beispiel eines nächtlichen Datenbank-Dumps, wie du einen bestehenden Cronjob auf einen systemd-Timer umstellst und das Ergebnis verifizierst.
Was ist ein systemd-Timer?
Ein systemd-Timer ist eine Unit-Datei mit der Endung .timer, die eine gleichnamige .service-Unit zu festgelegten Zeitpunkten startet und jeden Lauf mit Exit-Status, Dauer und Ausgabe im systemd-Journal protokolliert.
Ein Timer besteht immer aus zwei Dateien: der .service-Unit mit der eigentlichen Arbeit und der .timer-Unit mit dem Zeitplan. systemd verknüpft beide automatisch über den identischen Basisnamen, backup-db.timer startet also backup-db.service. Nur wenn du einen abweichenden Namen brauchst, setzt du im Timer explizit Unit=.
systemd kennt zwei Timer-Typen. Kalender-Timer (OnCalendar=) feuern zu Wanduhrzeiten und ersetzen den klassischen Crontab-Eintrag. Monotone Timer (OnBootSec=, OnUnitActiveSec=) feuern relativ zum Systemstart oder zum letzten Lauf. Für „15 Minuten nach dem Boot" oder „alle 6 Stunden ab dem letzten erfolgreichen Lauf" gibt es in Cron kein Äquivalent.
Warum systemd-Timer statt Cronjobs?
Systemd-Timer bieten gegenüber Cronjobs vier konkrete Vorteile: durchsuchbare Logs im Journal, das Nachholen verpasster Läufe über Persistent=true, echte Abhängigkeiten zu anderen Units und Ressourcenlimits wie MemoryMax pro Job.
| Eigenschaft | Cron | systemd-Timer |
|---|---|---|
| Ausgabe | Mail an lokalen User | journalctl -u <unit> |
| Exit-Status sichtbar | Nein | systemctl status <unit> |
| Verpasster Lauf nachholbar | Nein (nur anacron) | Persistent=true |
| Abhängigkeit zu Netzwerk/Mount | Nein | After=, Requires= |
| Ressourcenlimits | Nein | MemoryMax=, CPUQuota= |
| Nächster Lauf abfragbar | Nein | systemctl list-timers |
| Überlappende Läufe | Möglich | Verhindert (Service läuft bereits) |
Der letzte Punkt ist in der Praxis der wichtigste. Ein Cronjob, der länger läuft als sein Intervall, startet fröhlich ein zweites Mal. systemd startet einen bereits laufenden Service nicht erneut und schreibt stattdessen einen Hinweis ins Journal.
Voraussetzungen
- Ein Linux-Host mit systemd ab Version 230 (Debian 10+, Ubuntu 18.04+, Rocky/AlmaLinux 8+). Prüfe mit
systemctl --version. - Ein Benutzerkonto mit
sudo-Rechten - Ein ausführbares Skript oder Kommando, das bisher per Cron lief
Zum Nachbauen reicht eine skalierbare Cloud-VM mit einer aktuellen Linux-Distribution. Alle Beispiele nutzen den Pfad /usr/local/bin/backup-db.sh als Platzhalter für dein eigenes Skript, ersetze ihn entsprechend.
$ systemctl --version | head -n 1
systemd 252 (252.22-1~deb12u1)Ablauf eines Timer-Laufs
Bevor du die Units schreibst, hilft ein Blick auf die Kette vom Boot bis zum Journal-Eintrag. Besonders die Verzweigung bei einem verpassten Zeitpunkt entscheidet darüber, ob ein nächtlicher Job nach einem Reboot am Morgen noch läuft.
graph TD
A["timers.target beim Boot"] --> B["backup-db.timer aktiv"]
B --> C{"OnCalendar-Zeitpunkt erreicht?"}
C -->|"ja, System läuft"| E["backup-db.service starten"]
C -->|"nein, System war aus"| F{"Persistent=true gesetzt?"}
F -->|ja| E
F -->|nein| G["Lauf wird übersprungen"]
E --> H["ExecStart ruft /usr/local/bin/backup-db.sh auf"]
H --> I{"Exit-Code 0?"}
I -->|ja| J["Status inactive (dead), Erfolg im Journal"]
I -->|nein| K["Status failed, Fehler im Journal"]
Passende Infrastruktur bei centron
Zum Mitmachen braucht es keine eigene Hardware: ccloud³ VMs mit vollem Root-Zugriff, stundengenau abgerechnet und in Sekunden startklar. Cloud-Server mieten →
Service-Unit anlegen
Lege zuerst die Unit an, die die eigentliche Arbeit erledigt. Type=oneshot sagt systemd, dass der Prozess sich beendet und das kein Fehler ist.
[Unit]
Description=Datenbank-Dump nach /var/backups
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/usr/local/bin/backup-db.sh
MemoryMax=512M
Nice=10Speichere die Datei als /etc/systemd/system/backup-db.service. Die wichtigsten Direktiven im Detail:
Type=oneshot: systemd betrachtet den Service als erfolgreich, wenn der Prozess mit Exit-Code 0 endet. Ohne diese Angabe (Type=simple) gilt ein sich beendender Prozess als Fehlschlag.User=/Group=: Der Job läuft nicht als root. Lege das Konto vorher an oder setze einen bestehenden Systembenutzer ein.After=network-online.target: Nötig, wenn das Skript eine Remote-Datenbank oder ein NFS-Ziel erreicht. Bei rein lokalen Jobs kannst du beidenetwork-Zeilen weglassen.MemoryMax=: Harte Speichergrenze über cgroups. Der Job wird beendet statt den Host in den OOM-Killer zu treiben.
Eine [Install]-Sektion braucht die Service-Unit nicht. Sie wird nie direkt aktiviert, sondern ausschließlich vom Timer gestartet.
Timer-Unit anlegen
Die Timer-Unit trägt denselben Basisnamen und enthält nur den Zeitplan. Speichere sie als /etc/systemd/system/backup-db.timer.
[Unit]
Description=Startet backup-db.service taeglich um 02:30 Uhr
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.targetDrei Direktiven bestimmen das Verhalten:
OnCalendar=: Der Zeitplan im FormatWochentag Jahr-Monat-Tag Stunde:Minute:Sekunde. Ein*steht für „jeder Wert".Persistent=true: systemd speichert den Zeitpunkt des letzten Laufs unter/var/lib/systemd/timers/. War der Host zum geplanten Zeitpunkt aus, startet der Service unmittelbar nach dem nächsten Boot nach.RandomizedDelaySec=300: Verzögert den Start um einen zufälligen Wert zwischen 0 und 300 Sekunden. Auf mehreren Hosts mit identischem Zeitplan verhinderst du damit, dass alle gleichzeitig auf dasselbe Backup-Ziel schreiben.
Die Genauigkeit steuert AccuracySec=, der Standardwert liegt bei 60 Sekunden. systemd darf den Start innerhalb dieses Fensters verschieben, um Wakeups zu bündeln. Setze AccuracySec=1s nur, wenn der exakte Zeitpunkt wirklich zählt.
OnCalendar-Ausdrücke aus Cron übersetzen
| Crontab | OnCalendar |
|---|---|
30 2 * * * |
*-*-* 02:30:00 |
0 * * * * |
hourly |
*/15 * * * * |
*:0/15 |
0 4 * * 1 |
Mon *-*-* 04:00:00 |
0 0 1 * * |
*-*-01 00:00:00 |
0 3 * * 1-5 |
Mon..Fri *-*-* 03:00:00 |
Rate nie bei einem Ausdruck. systemd-analyze calendar löst ihn auf und zeigt dir die nächsten Auslösezeitpunkte:
$ systemd-analyze calendar --iterations=3 "Mon..Fri *-*-* 03:00:00"
Original form: Mon..Fri *-*-* 03:00:00
Normalized form: Mon..Fri *-*-* 03:00:00
Next elapse: Mon 2026-08-17 03:00:00 CEST
From now: 8h leftMehrere OnCalendar=-Zeilen in einer Unit addieren sich. Eine leere Zeile OnCalendar= setzt alle vorherigen zurück, das brauchst du bei Overrides via systemctl edit.
Timer aktivieren und verifizieren
Nach jeder Änderung an einer Unit-Datei muss systemd die Konfiguration neu einlesen. Danach aktivierst du den Timer, nicht den Service.
$ sudo systemd-analyze verify /etc/systemd/system/backup-db.timer
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now backup-db.timer
Created symlink /etc/systemd/system/timers.target.wants/backup-db.timer → /etc/systemd/system/backup-db.timer.Prüfe anschließend, ob der Timer registriert ist und wann er das nächste Mal feuert:
$ systemctl list-timers backup-db.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2026-08-17 02:30:00 CEST 9h left n/a n/a backup-db.timer backup-db.serviceSteht dort eine plausible Zeit in NEXT, greift der Zeitplan. Teste die Service-Unit einmal manuell, ohne auf den Zeitpunkt zu warten:
$ sudo systemctl start backup-db.service
$ systemctl status backup-db.service
● backup-db.service - Datenbank-Dump nach /var/backups
Loaded: loaded (/etc/systemd/system/backup-db.service; static)
Active: inactive (dead) since Sun 2026-08-16 17:42:11 CEST; 4s ago
Process: 18422 ExecStart=/usr/local/bin/backup-db.sh (code=exited, status=0/SUCCESS)status=0/SUCCESS und inactive (dead) sind bei Type=oneshot das erwartete Ergebnis. Die vollständige Ausgabe des Skripts liegt im Journal:
$ journalctl -u backup-db.service --since "1 hour ago" --no-pagerWenn du solche Läufe über mehrere Hosts hinweg auswerten willst, leite das Journal an einen zentralen Log-Empfänger weiter. Läuft die Datenbank selbst auf einer weiteren Cloud-VM mit dedizierten CPU-Kernen, trennst du Backup-Last und Produktivlast sauber voneinander.
Bestehende Cronjobs abschalten
Beide Mechanismen parallel laufen zu lassen führt zu doppelten Läufen. Entferne den alten Eintrag erst, nachdem der Timer einmal erfolgreich gefeuert hat:
$ sudo crontab -u backup -l
$ sudo crontab -u backup -e
$ ls -l /etc/cron.d/ /etc/cron.daily/Prüfe alle vier Orte: die User-Crontabs, /etc/crontab, /etc/cron.d/ und die Verzeichnisse cron.hourly bis cron.monthly.
Timer im User-Kontext betreiben
Für Jobs, die kein root und keinen Systemdienst brauchen, legst du die Units unter ~/.config/systemd/user/ ab und arbeitest mit --user:
$ systemctl --user daemon-reload
$ systemctl --user enable --now backup-db.timer
$ sudo loginctl enable-linger $USERloginctl enable-linger ist zwingend. Ohne diese Einstellung beendet systemd die User-Instanz mit der letzten Session, und der Timer feuert nach dem Logout nicht mehr.
Troubleshooting
Der Timer taucht nicht in list-timers auf. Meist wurde versehentlich der Service aktiviert. Prüfe mit systemctl is-enabled backup-db.timer. Liefert der Befehl disabled, hole sudo systemctl enable --now backup-db.timer nach.
Der Service scheitert mit 203/EXEC. Der Pfad in ExecStart= existiert nicht oder das Skript hat kein Executable-Bit. ExecStart verlangt einen absoluten Pfad, $PATH wird nicht ausgewertet. Setze sudo chmod +x /usr/local/bin/backup-db.sh.
Das Skript funktioniert in der Shell, aber nicht als Unit. systemd startet Services mit minimaler Umgebung, ohne die Variablen aus .bashrc oder /etc/profile. Setze benötigte Variablen explizit mit Environment="PGPASSFILE=/etc/backup/.pgpass" oder lade sie über EnvironmentFile=/etc/backup/env.
Der Zeitplan wird nicht akzeptiert. systemctl status backup-db.timer meldet dann einen Parse-Fehler. Prüfe den Ausdruck isoliert mit systemd-analyze calendar "<dein-ausdruck>", bevor du ihn in die Unit schreibst.
Fazit
Mit zwei Unit-Dateien, systemctl enable --now und einem Blick in systemctl list-timers ersetzt du einen Cronjob durch einen Timer, dessen Läufe nachvollziehbar im Journal stehen. Setze Persistent=true bei allen Jobs, deren Ausfall nach einem Reboot relevant ist, und prüfe jeden neuen OnCalendar-Ausdruck vorab mit systemd-analyze calendar. Als nächsten Schritt lohnt sich OnFailure= in der Service-Unit: Damit startest du bei einem Fehlschlag automatisch eine Benachrichtigungs-Unit.
Testen Sie Ihr Setup auf ccloud³
Registrieren Sie sich in der ccloud³ und erhalten Sie 200 € Startguthaben für Ihr Projekt – z. B. für eine PostgreSQL-VM mit automatischen Backups.