Cronjob einrichten: Zeitplan, Rechte und die häufigsten Fehler

Veröffentlicht am 18 Min. Lesezeit

Der Job läuft in der Shell, aber nicht in cron. Dieser Leitfaden erklärt die fünf Zeitfelder, den Unterschied zwischen Benutzer-Crontab und /etc/cron.d, die PATH-Falle und wie du echte Erfolge nachweist.

Ein Cronjob ist in fünf Minuten eingerichtet und kostet danach oft Stunden. Der Befehl läuft in der Shell einwandfrei, aus cron heraus passiert nichts, und im Log steht bestenfalls, dass cron etwas gestartet hat. Dieser Artikel behandelt genau die Stellen, an denen die üblichen Anleitungen aufhören: die echten Fehlermeldungen, die Unterschiede zwischen den Distributionen und die Frage, woran du erkennst, dass ein Job wirklich durchgelaufen ist und nicht nur gestartet wurde.

Alle Angaben beziehen sich auf Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS und Ubuntu 22.04 LTS. Diese vier verwenden denselben cron (die Debian-Variante von Vixie Cron), nicht cronie wie Red Hat und Fedora. Dieser Unterschied ist später bei der Zeitzone entscheidend.

Läuft der cron-Dienst überhaupt?

In vollständigen Installationen ist cron vorhanden. In minimalen Cloud-Images, Container-Basisimages und schlanken Netinst-Installationen fehlt das Paket regelmäßig. Das ist die erste Frage, die du klären solltest, bevor du irgendetwas debuggst.

command -v crontab
systemctl status cron

Absichtlich command -v crontab und nicht command -v cron: der Dienst selbst liegt in /usr/sbin, und dieses Verzeichnis steht auf Debian nur im Suchpfad von root. Als normaler Benutzer bekommst du dort schlicht keine Ausgabe, obwohl cron installiert ist. Unter Ubuntu 24.04 funktioniert die Abfrage auch als unprivilegierter Benutzer. crontab liegt dagegen überall in /usr/bin und ist damit für jeden sichtbar, und systemctl status cron beantwortet ohnehin die wichtigere Frage, nämlich ob der Dienst auch läuft.

Fehlt der Dienst, installierst du ihn nach:

apt-get update
apt-get install -y cron
systemctl enable --now cron

Der Dienst heißt auf Debian und Ubuntu cron, nicht crond. Wer sich an Red Hat gewöhnt hat, bekommt hier ein Unit crond.service could not be found. und sucht an der falschen Stelle.

Eine Neustart-Pflicht gibt es im Normalbetrieb nicht: der Debian-cron überwacht die Crontab-Verzeichnisse per inotify und liest Änderungen selbst ein. Du musst nach dem Bearbeiten einer Crontab also nichts neu starten. Ausnahmen sind der Wechsel der Systemzeitzone und Änderungen an /etc/default/cron.

Die fünf Felder, und die Falle im fünften

Jede Zeile beginnt mit fünf Zeitfeldern, danach folgt der Befehl.

FeldBereichHinweis
Minute0 bis 59
Stunde0 bis 2324-Stunden-Format, keine Zeitzone
Tag im Monat1 bis 31
Monat1 bis 12auch jan bis dec
Wochentag0 bis 70 und 7 sind beide Sonntag, auch sun bis sat
30 4 * * *      /usr/local/bin/backup.sh      # täglich 04:30
*/10 * * * *    /usr/local/bin/check.sh       # alle 10 Minuten
0 2 * * 0       /usr/local/bin/weekly.sh      # sonntags 02:00
15 3 1 * *      /usr/local/bin/monthly.sh     # am 1. um 03:15
0 9-17 * * 1-5  /usr/local/bin/business.sh    # werktags stündlich 9 bis 17

Zwei Details werden fast immer falsch verstanden.

Tag im Monat und Wochentag sind ein ODER, kein UND. Sobald beide Felder eingeschränkt sind, also keines ein Sternchen enthält, läuft der Job, wenn eines von beiden passt. 0 3 13 * 5 bedeutet nicht "Freitag, der 13.", sondern "jeden 13. und zusätzlich jeden Freitag". Willst du wirklich Freitag den 13., prüfst du das im Skript selbst.

Schrittweiten teilen nicht gleichmäßig. */7 * * * * läuft zu Minute 0, 7, 14, 21, 28, 35, 42, 49 und 56, danach springt der Zähler auf die nächste Stunde. Zwischen 56 und 0 liegen also nur vier Minuten. Das gilt für jedes Intervall, das nicht glatt in 60 beziehungsweise 24 aufgeht. Für "alle 90 Minuten" gibt es in cron keine saubere Schreibweise, hier ist ein systemd-Timer die bessere Wahl.

crontab -e richtig benutzen

Die Benutzer-Crontab bearbeitest du niemals direkt in /var/spool/cron/crontabs/, sondern immer über das Werkzeug:

crontab -e

Beim ersten Aufruf meldet sich das Programm mit no crontab for root - using an empty one. Auf frisch installierten Systemen folgt danach eine Editorauswahl. Fehlt jeder Editor, etwa in einem schlanken Image, bricht der Aufruf mit einer Meldung wie /usr/bin/sensible-editor: 25: editor: not found ab. Abhilfe: einen Editor installieren oder den gewünschten Editor explizit setzen.

apt-get install -y nano
EDITOR=nano crontab -e

Der große Vorteil von crontab -e gegenüber dem direkten Schreiben der Datei ist die Prüfung beim Speichern. Ist eine Zeile kaputt, siehst du:

"/tmp/crontab.7hK2mn/crontab":3: bad minute
errors in crontab file, can't install.
Do you want to retry the same edit? (y/n)

Die Zeilennummer stimmt, das Feld in der Meldung nicht immer: bad minute erscheint auch dann, wenn schlicht ein Feld zu wenig vorhanden ist, weil der Parser dann alles nach links verschoben liest. Bei Erfolg endet der Vorgang mit crontab: installing new crontab. Nur diese Zeile bedeutet, dass die Änderung übernommen wurde.

Weitere Befehle, die du kennen solltest:

crontab -l                         # anzeigen
crontab -l > /root/crontab.bak     # sichern
crontab -u www-data -l             # fremde Crontab lesen (als root)
crontab -i -r                      # löschen, mit Rückfrage (nur am Terminal)

Ein Hinweis zur Sicherung: crontab -l > /root/crontab.bak meldet no crontab for root und liefert den Rückgabewert 1, solange für den Benutzer noch gar keine Crontab existiert. Die Zieldatei entsteht trotzdem, dann mit 0 Byte. In der Reihenfolge dieses Artikels fällt das nicht auf, in einem Skript, das den Rückgabewert prüft oder eine ältere Sicherung überschreibt, sehr wohl.

crontab -r ohne -i löscht die komplette Crontab sofort und ohne Nachfrage. Da r und e auf der Tastatur nicht weit auseinanderliegen, ist das ein realer Datenverlustfall. Gewöhne dir am Terminal deshalb crontab -i -r an und sichere vorher mit crontab -l.

Wichtig ist dabei das Wort Terminal. crontab -i -r ist ein rein interaktiver Befehl. Hängt die Standardeingabe nicht an einem Terminal, also in einem Skript, in einem Cronjob oder in einem Einzeiler wie ssh host "crontab -i -r", dreht sich die Rückfrage endlos im Kreis: crontab: really delete root's crontab? (y/n) Please enter Y or N: Please enter Y or N: ... wiederholt sich unbegrenzt und schreibt in Sekunden hunderte Kilobyte Ausgabe, beendet wird der Aufruf nur noch durch einen Abbruch von außen. Für alles Automatisierte nimmst du deshalb die nicht interaktive Variante und sicherst vorher:

crontab -l > /root/crontab.bak     # sichern, bevor gelöscht wird
crontab -r                         # löschen, ohne Rückfrage
printf "y\n" | crontab -i -r       # Rückfrage behalten, Antwort vorgeben

Existieren /etc/cron.allow oder /etc/cron.deny, entscheiden sie, wer überhaupt Crontabs anlegen darf. Betroffene Nutzer bekommen: You (username) are not allowed to use this program (crontab). Ist /etc/cron.allow vorhanden, gilt sie exklusiv, jeder nicht gelistete Benutzer ist gesperrt.

Benutzer-Crontab, /etc/crontab und /etc/cron.d

Es gibt vier Orte, an denen Zeitpläne stehen können, und sie haben unterschiedliche Formate. Genau hier entstehen die Fehler, die am schwersten zu finden sind.

Benutzer-Crontab: fünf Felder

Über crontab -e gepflegt, läuft unter dem Benutzer, dem sie gehört, und hat kein Benutzerfeld. Schreibst du trotzdem eines hinein, versucht cron den Benutzernamen als Befehl auszuführen und du findest in der Mail oder im Log:

/bin/sh: 1: root: not found

/etc/cron.d: sechs Felder

Dateien in /etc/cron.d/ sind Systemcrontabs und haben zwischen Zeitfeldern und Befehl ein Benutzerfeld. Das ist der richtige Ort für Jobs, die zu einer Anwendung oder zu einem Konfigurationsmanagement gehören, weil jede Datei einzeln ausgetauscht werden kann.

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

0 3 * * * root /usr/local/bin/backup.sh >> /var/log/kh-backup.log 2>&1

Vergisst du das Benutzerfeld, interpretiert cron das erste Wort des Befehls als Benutzernamen und die Zeile scheitert stillschweigend, weil dieser Benutzer nicht existiert.

Drei Regeln für /etc/cron.d/ werden regelmäßig verletzt:

  • Der Dateiname darf keinen Punkt enthalten. Erlaubt sind ausschließlich Buchstaben, Ziffern, Unterstriche und Bindestriche. backup.cron oder kh-backup.sh werden kommentarlos ignoriert. Der Grund ist die Paketverwaltung: so werden Reste wie .dpkg-dist oder .dpkg-old nie ausgeführt. Nenne die Datei einfach kh-backup.
  • Rechte und Eigentümer müssen stimmen. Erwartet werden root:root und Modus 0644. Andernfalls landen im Log Meldungen wie (*system*) WRONG FILE OWNER, (*system*) BAD FILE MODE oder ein Hinweis auf einen unsicheren, für Gruppe oder andere schreibbaren Modus. Der Job läuft dann nicht.
  • Die Datei muss mit einem Zeilenumbruch enden. cron verlangt, dass jeder Eintrag mit einem Newline abschließt. Endet die letzte Zeile ohne Umbruch, wird sie übergangen, und zwar kommentarlos: im Gegentest mit einer sonst identischen Datei ohne abschließenden Umbruch lief der Job kein einziges Mal, ohne Fehlermeldung und ohne Logzeile. Wer die Datei mit echo -n, mit einem printf ohne abschließendes \n oder aus einem Template ohne Leerzeile am Ende erzeugt, verliert genau den letzten Job.
chown root:root /etc/cron.d/kh-backup
chmod 0644 /etc/cron.d/kh-backup

/etc/crontab und die cron.*-Verzeichnisse

/etc/crontab hat ebenfalls sechs Felder und gehört der Distribution. Ändere sie nur, wenn du weißt, warum. Sie ruft über run-parts die Verzeichnisse /etc/cron.hourly, cron.daily, cron.weekly und cron.monthly auf. Skripte dort brauchen das Ausführbit und dürfen ebenfalls keinen Punkt im Namen tragen. Ein backup.sh in /etc/cron.daily/ wird nie ausgeführt, ein backup schon. Testen kannst du das ohne Ausführung:

run-parts --test /etc/cron.daily

Ausgegeben werden nur die Skripte, die run-parts tatsächlich starten würde. Fehlt deines in der Liste, liegt es am Namen oder am Ausführbit.

Warum PATH in cron ein anderer ist

Das ist mit Abstand die häufigste Ursache für "läuft in der Shell, aber nicht in cron". cron startet keine Login-Shell. Weder ~/.bashrc noch ~/.profile noch /etc/profile werden gelesen. Der Debian-cron setzt für Benutzer-Crontabs einen minimalen Suchpfad:

PATH=/usr/bin:/bin

Damit fehlen /usr/local/bin und /usr/sbin. Auf Debian 12, Debian 13, Ubuntu 22.04 und Ubuntu 24.04 sind /usr/bin und /usr/sbin weiterhin getrennte Verzeichnisse, der Usr-Merge betrifft nur /bin und /sbin. Alles, was du selbst nach /usr/local/bin gelegt hast, alles aus pip install, alles aus einem nvm-Node und Systemwerkzeuge wie ufw oder iptables sind für cron schlicht nicht vorhanden. Die Fehlermeldung lautet dann:

/bin/sh: 1: backup.sh: not found

Der zweite Teil der Falle: cron setzt SHELL=/bin/sh. Auf Debian und Ubuntu ist /bin/sh ein Verweis auf dash, nicht auf bash. Jedes Bash-Idiom im Skriptkopf oder direkt in der Crontab-Zeile scheitert:

/bin/sh: 1: [[: not found
/bin/sh: 1: source: not found
/bin/sh: 1: Syntax error: "(" unexpected

Drei Gegenmaßnahmen, in dieser Reihenfolge:

  1. Absolute Pfade verwenden. /usr/local/bin/backup.sh statt backup.sh, /usr/bin/php statt php. Das ist die robusteste Variante, weil sie unabhängig von jeder Umgebungsvariable funktioniert. Lies den Pfad aber ab, statt ihn aus dem Gedächtnis zu tippen: command -v date liefert auf Debian 13, Debian 12, Ubuntu 24.04 und Ubuntu 22.04 /usr/bin/date, auf älteren Systemen wie Debian 11 dagegen /bin/date. Eine Zeile mit falschem Pfad nimmt crontab kommentarlos und ohne Warnung an, sie scheitert erst zur Laufzeit und dann still. Zur Kontrolle einmal crontab -l und einmal env -i /bin/sh -c "/usr/bin/date", das meldet einen falschen Pfad sofort mit not found.
  2. PATH und SHELL am Anfang der Crontab setzen. Die Zuweisungen gelten für alle folgenden Zeilen. Wichtig: cron expandiert dabei keine Variablen, PATH=$PATH:/opt/bin funktioniert nicht, schreibe den Pfad vollständig aus.
  3. Ein Wrapper-Skript benutzen. Die Crontab-Zeile ruft nur das Skript auf, das Skript setzt seine eigene Umgebung.
#!/bin/bash
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cd /srv/app
exec ./do-the-work.sh

Wenn du wissen willst, was cron deinem Job wirklich mitgibt, lass es dir zeigen. Trage für eine Minute ein:

* * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1

Nur dieser Weg zeigt die Umgebung, die cron tatsächlich setzt. Eine mit env -i nachgebaute Shell kommt lediglich in die Nähe, weil sie ihren eigenen Vorgabepfad mitbringt. Danach vergleichst du /tmp/cron-env.txt mit deiner interaktiven Umgebung. Auffällig sind meist nicht nur PATH und SHELL, sondern auch die fehlenden Locale-Variablen. Ohne LANG läuft alles in der C-Locale, was Sortierreihenfolgen, Datumsformate und die Ausgabe von Umlauten verändert. Skripte, die auf LANG=de_DE.UTF-8 bauen, verhalten sich in cron anders. Auch der ssh-agent fehlt, weshalb Jobs mit SSH-Zugriff einen passphrasenlosen Schlüssel und ein explizites -i brauchen.

Die letzte kleine Gemeinheit in dieser Kategorie: das Prozentzeichen. In einer Crontab-Zeile wird ein nicht maskiertes % in einen Zeilenumbruch verwandelt, alles danach geht als Standardeingabe an den Befehl. Datumsformate müssen also maskiert werden.

0 2 * * * /usr/bin/tar -czf /backup/web-$(date +\%F).tar.gz /var/www

Ausgabe umleiten, Mails und "No MTA installed"

Standardmäßig schickt cron alles, was ein Job auf stdout oder stderr ausgibt, per Mail an den Besitzer der Crontab. Auf einem Server ohne Mailsystem landet das im Log:

(CRON) info (No MTA installed, discarding output)

Das ist kein Fehler des Jobs. Es bedeutet nur, dass Ausgabe angefallen ist und niemand sie entgegennehmen konnte. Ein stiller Job erzeugt diese Zeile nicht. Sie ist damit sogar ein nützliches Signal: taucht sie plötzlich auf, hat dein Job angefangen, etwas auszugeben, meist eine Fehlermeldung.

Für die Umleitung gibt es drei sinnvolle Muster:

# alles in eine Logdatei, inklusive Fehler
0 3 * * * /usr/local/bin/backup.sh >> /var/log/kh-backup.log 2>&1

# normale Ausgabe weg, Fehler weiterhin per Mail
0 3 * * * /usr/local/bin/backup.sh > /dev/null

# alles ins Journal, sauber markiert
0 3 * * * /usr/local/bin/backup.sh 2>&1 | /usr/bin/logger -t kh-backup

Die Variante > /dev/null 2>&1 ist beliebt und gefährlich: sie wirft auch alle Fehlermeldungen weg. Ein Job, der seit vier Monaten scheitert, sieht dann exakt aus wie einer, der läuft. Wenn du auf Mails verzichten willst, setz lieber MAILTO="" am Anfang der Crontab und schreib die Ausgabe in eine Datei. Willst du Mails an eine bestimmte Adresse, setzt du MAILTO=alerts@example.org, brauchst dafür aber ein installiertes Mailsystem.

Eigene Logdateien in /var/log/ wachsen unbegrenzt. Leg dafür eine kleine Regel in /etc/logrotate.d/ an, sonst füllt dir der erfolgreichste Cronjob irgendwann die Platte.

Woran du erkennst, dass es wirklich lief

Hier trennt sich die schnelle Anleitung von der belastbaren Prüfung. cron protokolliert den Start eines Jobs. Es protokolliert weder das Ende noch den Exit-Code. Eine CMD-Zeile im Log beweist also nur, dass cron die Shell gestartet hat, nicht dass dein Skript erfolgreich war.

Die Logs liest du je nach System unterschiedlich:

journalctl -u cron --since "30 min ago"
journalctl -t CRON --since today

Auf Debian 12 und Debian 13 ist das der einzige Weg, denn seit Bookworm wird rsyslog nicht mehr standardmäßig installiert. Eine Datei /var/log/syslog existiert dort in der Regel nicht mehr. Wer sie sucht und nicht findet, hält den Job fälschlich für nicht gestartet.

Auf Ubuntu 22.04 und 24.04 ist rsyslog üblicherweise vorhanden, dort funktioniert zusätzlich:

grep CRON /var/log/syslog

Eine eigene /var/log/cron.log gibt es auf keiner der vier Versionen ab Werk. Die entsprechende Regel in /etc/rsyslog.d/50-default.conf ist auskommentiert. Such nicht nach dieser Datei, sie existiert nur, wenn jemand sie aktiviert hat.

Ein sauberer Nachweis kommt deshalb aus dem Job selbst. Lass das Skript am Ende einen Zeitstempel und den Exit-Code schreiben:

#!/bin/bash
set -euo pipefail
trap 'echo "$(date -Is) kh-backup beendet, exit $?" >> /var/log/kh-backup.log' EXIT
# eigentliche Arbeit

Damit hast du drei Belege statt einem: die CRON-Zeile im Journal (cron hat gestartet), die Endezeile in deiner Logdatei (das Skript ist bis zum Ende gekommen) und den Exit-Code (es ist sauber beendet). Erst wenn alle drei stimmen, funktioniert der Job.

Wenn ein Job länger laufen kann als sein Intervall, sichere ihn zusätzlich gegen Überlappung ab. Sonst starten irgendwann zehn Instanzen parallel und ziehen den Server herunter:

*/5 * * * * /usr/bin/flock -n /var/lock/kh-sync.lock /usr/local/bin/sync.sh

flock -n beendet sich sofort, wenn bereits eine Instanz läuft. Das Werkzeug steckt in util-linux und ist auf allen vier Systemen vorhanden.

@reboot und warum systemd meistens besser ist

Mit @reboot lässt sich ein Befehl beim Start ausführen. Es gibt daneben @daily, @hourly, @weekly, @monthly und @yearly, die jeweils die fünf Zeitfelder ersetzen.

@reboot /usr/local/bin/start-app.sh

Das sieht bequem aus und hat drei ernste Schwächen:

  • Der Zeitpunkt ist nicht "nach dem Boot", sondern "wenn cron startet". Ob Netzwerk, Datenbank oder ein Mount zu diesem Zeitpunkt bereit sind, ist reine Glückssache. Der übliche Notbehelf ist ein sleep 30 davor, was das Problem nur verschiebt.
  • Ein Neustart des cron-Dienstes löst @reboot erneut aus. Ein systemctl restart cron, etwa nach einem Paketupdate, startet deine Anwendung ein zweites Mal, obwohl die erste noch läuft.
  • Es gibt keine Überwachung. Kein Exit-Code, kein Neustart bei Absturz, kein Status.

Für alles, was dauerhaft laufen soll, gehört ein systemd-Service her, kein Cronjob. Für wiederkehrende Aufgaben ist ein Timer die bessere Wahl. Zwei Dateien genügen:

[Unit]
Description=KernelHost Backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
[Unit]
Description=KernelHost Backup täglich

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=900

[Install]
WantedBy=timers.target

Aktiviert wird der Timer, nicht der Service:

systemctl daemon-reload
systemctl enable --now kh-backup.timer
systemctl list-timers kh-backup.timer

Die drei entscheidenden Vorteile gegenüber cron: Persistent=true holt einen verpassten Lauf nach dem nächsten Start nach, cron überspringt ihn wortlos. Der Exit-Code landet in systemctl status kh-backup.service, du siehst also ohne eigenes Logging, ob es geklappt hat. Und die komplette Ausgabe steht mit journalctl -u kh-backup.service zur Verfügung, ohne Umleitung und ohne Mail. Der Suchpfad ist bei systemd übrigens großzügiger als bei cron und enthält standardmäßig auch /usr/local/bin, ganz ohne eigene Zutat bleibt er aber trotzdem knapp.

Für Startaufgaben ersetzt du @reboot durch einen Service mit klaren Abhängigkeiten:

[Unit]
After=network-online.target
Wants=network-online.target

Damit läuft dein Job garantiert erst, wenn das Netzwerk tatsächlich steht. Mehr zu Units und deren Aufbau findest du unter systemd-Service erstellen.

Zeitzone, UTC und die Sommerzeit

cron rechnet immer in der Systemzeitzone aus /etc/localtime. Auf vielen Servern und in fast allen Cloud-Images ist das UTC. Ein Job um 0 3 * * * läuft dann im Sommer um 05:00 Uhr mitteleuropäischer Sommerzeit, nicht um 03:00 Uhr. Prüfe zuerst, womit du es zu tun hast:

readlink -f /etc/localtime
timedatectl show -p Timezone --value
date
date -u

readlink -f /etc/localtime nennt den Pfad in /usr/share/zoneinfo und damit die tatsächlich gültige Zone, etwa /usr/share/zoneinfo/Etc/UTC oder /usr/share/zoneinfo/Europe/Vienna. Er funktioniert auf allen vier Versionen und auch dort, wo systemd nicht läuft. timedatectl show -p Timezone --value gibt dieselbe Information kurz und maschinenlesbar aus, setzt aber systemd voraus.

Was du dir abgewöhnen solltest, ist der Blick in /etc/timezone. Debian 13 liefert diese Datei nicht mehr aus, ein cat /etc/timezone endet dort mit cat: /etc/timezone: No such file or directory, und auch eine Installation von tzdata bringt sie nicht zurück. Auf Debian 12, Ubuntu 24.04 und Ubuntu 22.04 gibt es sie noch, die Antwort fällt je nach Version also unterschiedlich aus. Maßgeblich ist in jedem Fall allein der Symlink /etc/localtime: ein von Hand in /etc/timezone geschriebener Wert ändert an der Systemzeitzone nichts und damit auch nichts am Auslösezeitpunkt deiner Jobs.

Umstellen kannst du die Systemzeitzone jederzeit, danach solltest du cron neu starten, damit der Dienst die Änderung sicher übernimmt:

timedatectl set-timezone Europe/Vienna
systemctl restart cron

Auf Systemen ohne laufendes systemd setzt du stattdessen den Symlink direkt, das Ergebnis ist dasselbe und lässt sich sofort mit readlink -f /etc/localtime gegenprüfen:

ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime

Jetzt der Punkt, der in vielen Anleitungen falsch steht: der cron von Debian und Ubuntu kennt kein CRON_TZ. Diese Variable stammt aus cronie, dem cron von Red Hat, Fedora und AlmaLinux. Auf Debian 13, Debian 12, Ubuntu 24.04 und Ubuntu 22.04 gibt es keine Zeitzone pro Benutzer oder pro Crontab. Setzt du dort TZ=Europe/Vienna in die Crontab, wirkt das ausschließlich auf die Umgebung der ausgeführten Befehle, ein date im Skript zeigt also Wiener Zeit. Der Auslösezeitpunkt selbst bleibt davon vollkommen unberührt und richtet sich weiter nach der Systemzeitzone. Wer das übersieht, hat einen Job, der scheinbar korrekt konfiguriert ist und trotzdem zwei Stunden daneben läuft.

Es bleiben drei saubere Wege: die Systemzeitzone passend setzen, die Zeiten selbst nach UTC umrechnen, oder auf einen systemd-Timer wechseln, der eine Zeitzone direkt im Zeitplan akzeptiert. Prüfen lässt sich das ohne Risiko:

systemd-analyze calendar "Mon..Fri 03:00 Europe/Vienna"

Die Ausgabe nennt die nächste Auslösung als konkretes Datum. Das ist die zuverlässigste Kontrolle, die du vor dem Aktivieren machen kannst.

Zur Sommerzeit noch ein praktischer Rat: plane keine Jobs zwischen 02:00 und 03:00 Uhr. Im März existiert diese Stunde nicht, im Oktober existiert sie zweimal. Je nach Job bedeutet das einen ausgefallenen oder einen doppelten Lauf, beides einmal im Jahr und beides schwer zu reproduzieren. 01:30 oder 03:30 sind unauffällige Alternativen. Bei systemd-Timern hilft zusätzlich Persistent=true, damit ein ausgefallener Lauf nachgeholt wird.

Schnelle Fehlersuche in sieben Schritten

Wenn ein Cronjob nicht läuft, arbeite diese Liste der Reihe nach ab. Sie deckt praktisch alle Fälle ab.

  1. Läuft der Dienst? systemctl status cron. Kein Dienst, kein Job.
  2. Ist der Eintrag angekommen? crontab -l für Benutzerjobs, sonst die Datei in /etc/cron.d/ ansehen. Prüfe Dateinamen ohne Punkt, Modus 0644, Eigentümer root und den abschließenden Zeilenumbruch.
  3. Hat cron ihn überhaupt gestartet? journalctl -t CRON --since today. Fehlt die CMD-Zeile, ist der Zeitplan oder die Datei das Problem, nicht das Skript.
  4. Stimmt die Feldzahl? Fünf Felder in der Benutzer-Crontab, sechs in /etc/cron.d und /etc/crontab.
  5. Absolute Pfade? Jeden Befehl und jedes Skript mit vollem Pfad eintragen, den Pfad vorher mit command -v ermitteln statt ihn zu tippen.
  6. Umgebung geprüft? Einmalig * * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1 eintragen, eine Minute warten, vergleichen.
  7. Im cron-Kontext testen. Führe das Skript nicht in deiner Shell aus, sondern mit leerer Umgebung: env -i PATH=/usr/bin:/bin /bin/sh -c '/usr/local/bin/backup.sh'. Den Suchpfad gibst du dabei bewusst mit an. env -i /bin/sh allein leert zwar die Umgebung, dash setzt sich danach aber seinen eigenen Vorgabepfad /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin, und der ist großzügiger als der von cron. Genau die Fehler, um die es hier geht, würden damit unentdeckt bleiben. Scheitert der Aufruf, hast du die Ursache gefunden, ganz ohne auf den nächsten Auslösezeitpunkt zu warten.

Der letzte Punkt ist der wertvollste. Fast jeder Cronjob, der "unerklärlich" nicht funktioniert, scheitert reproduzierbar, sobald man ihn mit leerer Umgebung startet. Damit wird aus einem Rätsel, das nur alle 24 Stunden auftritt, ein Fehler, den du in zehn Sekunden beheben kannst.

Wer regelmäßig Backups per Cronjob fährt, sollte zusätzlich die Zielseite absichern. Passende Hinweise dazu stehen in unserem Beitrag zur Absicherung eines Linux-Servers.

Häufige Fragen

Warum läuft mein Skript in der Shell, aber nicht als Cronjob?
In den allermeisten Fällen liegt es an der Umgebung. cron startet keine Login-Shell, liest also weder ~/.bashrc noch /etc/profile, und setzt für Benutzer-Crontabs nur PATH=/usr/bin:/bin. Damit fehlen /usr/local/bin und /usr/sbin. Zusätzlich ist SHELL auf /bin/sh gesetzt, was auf Debian und Ubuntu dash ist und keine Bash-Syntax versteht. Teste den Job in genau dieser Umgebung: env -i PATH=/usr/bin:/bin /bin/sh -c, gefolgt vom Skriptpfad in Hochkommas ('/pfad/zum/skript.sh'), dann scheitert er sofort reproduzierbar. Den Suchpfad gibst du dabei ausdrücklich mit an, denn ein bloßes env -i /bin/sh setzt den großzügigeren Vorgabepfad von dash und verdeckt genau diesen Fehler.
Was bedeutet die Meldung No MTA installed, discarding output?
Dein Job hat etwas auf stdout oder stderr ausgegeben, cron wollte das per Mail zustellen, es ist aber kein Mailsystem installiert. Der Job selbst ist davon nicht betroffen und kann trotzdem erfolgreich gewesen sein. Entweder du leitest die Ausgabe in eine Logdatei um, oder du setzt MAILTO="" am Anfang der Crontab. Achtung: die Meldung erscheint oft erst, wenn ein bisher stiller Job anfängt, Fehler auszugeben.
Unterstützt Debian oder Ubuntu die Variable CRON_TZ?
Nein. CRON_TZ stammt aus cronie, dem cron von Red Hat und Fedora. Debian 13, Debian 12, Ubuntu 24.04 und Ubuntu 22.04 nutzen die Debian-Variante von Vixie Cron und kennen keine Zeitzone pro Crontab. Ein TZ= in der Crontab wirkt nur auf die Umgebung der Befehle, nicht auf den Auslösezeitpunkt. Setze entweder die Systemzeitzone mit timedatectl, rechne die Zeiten nach UTC um oder nutze einen systemd-Timer, der eine Zeitzone im OnCalendar-Ausdruck akzeptiert.
Warum wird meine Datei in /etc/cron.d ignoriert?
Fast immer wegen des Dateinamens. Erlaubt sind nur Buchstaben, Ziffern, Unterstriche und Bindestriche, ein Punkt macht die Datei unsichtbar für cron. backup.cron wird also ignoriert, backup nicht. Weitere Gründe: falsche Rechte (nötig sind root:root und 0644), ein fehlendes Benutzerfeld zwischen Zeitfeldern und Befehl, oder eine letzte Zeile ohne abschließenden Zeilenumbruch.
Wo finde ich die Cron-Logs auf Debian 12 und 13?
Über journalctl, denn seit Debian 12 wird rsyslog nicht mehr standardmäßig installiert und eine Datei /var/log/syslog existiert dort meist nicht. Nutze journalctl -u cron oder journalctl -t CRON. Auf Ubuntu 22.04 und 24.04 funktioniert zusätzlich grep CRON /var/log/syslog. Eine eigene /var/log/cron.log gibt es auf keinem der vier Systeme ab Werk.
Beweist eine CMD-Zeile im Log, dass der Job erfolgreich war?
Nein. cron protokolliert nur den Start eines Jobs, weder das Ende noch den Exit-Code. Ein Skript kann eine Sekunde später abgestürzt sein und die Logzeile sieht identisch aus. Für einen echten Nachweis lässt du das Skript selbst einen Zeitstempel und den Exit-Code in eine Logdatei schreiben, etwa über einen trap auf EXIT, oder du wechselst auf einen systemd-Timer, bei dem systemctl status den Exit-Code anzeigt.
Sollte ich @reboot oder einen systemd-Service verwenden?
In fast allen Fällen einen systemd-Service. @reboot läuft nicht nach dem Boot, sondern beim Start des cron-Dienstes, ohne Garantie, dass Netzwerk oder Datenbank bereit sind. Außerdem wird es bei jedem systemctl restart cron erneut ausgelöst, etwa nach einem Paketupdate. Ein Service mit After=network-online.target und Wants=network-online.target löst beide Probleme und liefert zusätzlich Statusanzeige und Exit-Code.

Cron Crontab systemd Linux-Administration Debian Ubuntu Automatisierung Server-Verwaltung