Cron funktioniert seit Jahrzehnten, und für ein Skript, das nichts Wichtiges tut, reicht es weiter. Sobald etwas davon abhängt, Sicherungen, Zertifikate, Aufräumläufe, fehlen drei Dinge, die systemd mitbringt: ein Protokoll, das man findet, ein Nachholen verpasster Läufe und eine Begrenzung dessen, was der Lauf sich nehmen darf.
Die drei Stellen, an denen Cron still versagt#
1. Die Ausgabe verschwindet oder landet in einer Mail, die niemand liest. Cron schickt stdout und stderr per Mail an den Benutzer. Ohne Mailsystem ist die Ausgabe schlicht weg.
2. War die Maschine aus, fällt der Lauf aus. Ein VPS, der um 3:00 neu startet, hat den 2:30-Lauf verloren, ersatzlos.
3. Die Umgebung ist eine andere. Cron startet mit einem minimalen PATH und ohne die Variablen aus deiner Anmeldesitzung. Das Skript, das von Hand läuft, scheitert im Cron, mit einer Meldung, die niemand sieht (siehe Punkt 1).
Der Aufbau: zwei Dateien#
Ein Timer sagt wann, ein Service sagt was. Getrennt, weil man den Dienst dann auch von Hand starten kann.
# /etc/systemd/system/aufraeumen.service
[Unit]
Description=Alte Dateien aufräumen
[Service]
Type=oneshot
User=www-data
ExecStart=/usr/local/bin/aufraeumen.sh
# /etc/systemd/system/aufraeumen.timer
[Unit]
Description=Aufräumen, täglich
[Timer]
OnCalendar=*-*-* 03:15
RandomizedDelaySec=10m
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now aufraeumen.timer
Der Dateiname ist die Verbindung: aufraeumen.timer startet aufraeumen.service. Wer einen anderen Namen will, setzt Unit= im Timer-Abschnitt.
Die Optionen, die den Unterschied machen#
| Option | Was sie tut |
|---|---|
Persistent=true | holt einen verpassten Lauf beim nächsten Start nach |
RandomizedDelaySec= | streut den Start; wichtig, wenn viele Server dasselbe Ziel treffen |
OnBootSec= / OnUnitActiveSec= | „5 Minuten nach dem Start, dann alle 15 Minuten“ statt fester Uhrzeit |
AccuracySec=1s | genaue Uhrzeit; ohne das darf systemd bis zu einer Minute schieben |
# Beispiel: alle 15 Minuten, aber erst 5 Minuten nach dem Hochfahren
[Timer]
OnBootSec=5min
OnUnitActiveSec=15min
Nachsehen, was passiert ist#
Das ist der eigentliche Gewinn:
systemctl list-timers --all # was läuft wann, wann zuletzt
systemctl status aufraeumen.service # Ergebnis des letzten Laufs
journalctl -u aufraeumen.service -n 50 # dessen Ausgabe
journalctl -u aufraeumen.service --since "3 days ago" -p err
Alles, was das Skript ausgibt, steht im Journal, ohne Mailsystem, ohne Umleitung in eine Logdatei, mit Zeitstempel und Rückgabewert.
# Nur die Läufe, die schiefgingen:
journalctl -u aufraeumen.service | grep -i "failed\|error"
systemctl show aufraeumen.service -p ExecMainStatus
Grenzen setzen#
Ein Aufräumlauf darf nicht den Webserver ausbremsen. Das geht direkt in der Unit, ohne nice und ionice im Skript:
[Service]
Nice=10
IOSchedulingClass=idle
CPUQuota=50%
MemoryMax=512M
# Nach einer Stunde ist Schluss — hängende Läufe blockieren sonst den nächsten
TimeoutStartSec=1h
Läuft der Service noch, während der Timer erneut auslöst, startet systemd ihn nicht ein zweites Mal, der Lauf wird übersprungen. Bei Cron würden sich zwei Instanzen ins Gehege kommen. Das ist ein Vorteil, aber man sollte es wissen: Ein Lauf, der dauerhaft zu lange braucht, wird still seltener ausgeführt als gedacht. systemctl list-timers zeigt es an der Spalte „LAST“.
Härten, wo es sich lohnt#
Ein zeitgesteuerter Lauf braucht selten Zugriff auf das ganze System:
[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/daten /var/log/meinlauf
Das kostet vier Zeilen und begrenzt den Schaden, wenn das Skript eines Tages etwas anderes tut als gedacht, etwa weil es eine fremde Datei ausführt.
Umstellen, was schon läuft#
crontab -l # eigene Jobs
ls -la /etc/cron.d/ /etc/cron.daily/
Für jeden Eintrag: Wer führt ihn aus, wie oft, was passiert bei Fehler? Danach eine Unit schreiben, den Cron-Eintrag auskommentieren statt löschen, eine Woche beobachten, dann entfernen.
Ein Sonderfall bleibt: @reboot. Dafür gibt es keinen Timer, sondern eine normale Unit mit WantedBy=multi-user.target, was ohnehin die sauberere Lösung ist.
Häufige Fragen#
Muss ich daemon-reload nach jeder Änderung ausführen?#
Ja, sonst arbeitet systemd mit der alten Fassung. Der häufigste Grund für „meine Änderung wirkt nicht“.
Wie teste ich, ohne auf die Uhrzeit zu warten?#
systemctl start aufraeumen.service, der Dienst läuft sofort, unabhängig vom Timer. Genau dafür ist die Trennung da.
Was ist mit Benutzer-Timern?#
systemctl --user funktioniert genauso, ohne Root. Wichtig: loginctl enable-linger <benutzer>, sonst laufen sie nur, solange der Benutzer angemeldet ist, dieselbe Falle wie bei Podman ohne Root.
Kann ich Cron-Syntax weiterverwenden?#
Nein, OnCalendar hat eine eigene Schreibweise. systemd-analyze calendar "Mon *-*-* 04:00:00" zeigt dir, wann der Ausdruck das nächste Mal zündet, damit lässt sich jede Übersetzung prüfen.
Und wenn ein Lauf fehlschlägt?#
OnFailure= in der Unit startet einen zweiten Dienst, der benachrichtigt. Das ist der Weg, Fehlschläge aktiv zu melden, statt darauf zu hoffen, dass jemand ins Journal sieht.
Kurz gesagt#
- Timer sagt wann, Service sagt was, getrennt, damit du von Hand starten kannst.
Persistent=trueholt verpasste Läufe nach. Cron kann das nicht.- Alles landet im Journal, mit Zeitstempel und Rückgabewert.
- Grenzen (
Nice,CPUQuota,TimeoutStartSec) gehören in die Unit, nicht ins Skript. - Beim Umstellen: Cron-Eintrag erst auskommentieren, eine Woche beobachten, dann löschen.
Geschrieben aus dem laufenden Betrieb: MRMedia betreibt eigene Proxmox- Hosts, einen Backup-Server und Kundendienste in der EU. Fehler in einer Anleitung sind Fehler im eigenen Betrieb. Korrekturen bitte über Discord.