🔀Alternativen zu cron

cron ist überall – aber nicht immer die beste Wahl. systemd-Timer protokollieren sauber und holen verpasste Läufe nach, Kubernetes plant Jobs im Cluster, und in Anwendungen erledigen Bibliotheken das Scheduling.

⚙️systemd-Timer: cron → OnCalendar

Ein Timer (.timer) startet einen Dienst (.service). Der Zeitplan steht in OnCalendar= – mit eigener Syntax: Wochentag Jahr-Monat-Tag Stunde:Minute:Sekunde.
Jeden Montag um 04:30
OnCalendar=Mon *-*-* 04:30:00

Prüfen: systemd-analyze calendar --iterations=5 'Mon *-*-* 04:30:00'

/etc/systemd/system/backup.timer
[Unit]
Description=backup (umgerechnet aus cron: 30 4 * * 1)

[Timer]
OnCalendar=Mon *-*-* 04:30:00
Persistent=true

[Install]
WantedBy=timers.target
/etc/systemd/system/backup.service
[Unit]
Description=backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
systemctl daemon-reload
systemctl enable --now backup.timer
systemctl list-timers backup.timer
journalctl -u backup.service      # Ausgabe landet im Journal
$ systemd-analyze calendar weekly Original form: weekly Normalized form: Mon *-*-* 00:00:00 $ systemd-analyze calendar '*:0/15' Normalized form: *-*-* *:00/15:00
📖 systemd-analyze calendar, systemd 257 (Debian 13)
💡 Persistent=true
Speichert den Zeitpunkt des letzten Laufs. War der Rechner zur geplanten Zeit aus, startet der Dienst beim nächsten Hochfahren sofort – das, wofür man bei cron anacron braucht.
💡 RandomizedDelaySec= und AccuracySec=
RandomizedDelaySec verzögert jeden Start um eine zufällige Zeit bis zum angegebenen Wert – gut gegen „alle Server um 00:00“. AccuracySec (Standard 1min) erlaubt systemd, Starts zu bündeln; für sekundengenaue Timer AccuracySec=1s setzen.
⚠️ Unterschiede zur cron-Syntax
Bereiche mit .. statt -; Wiederholung 00/15 statt */15; Wochentage als Mon..Fri. Wochentag und Datum sind immer UND-verknüpft – für die cron-ODER-Regel braucht es zwei OnCalendar=-Zeilen. Sekunden sind möglich.
# ad hoc, ohne Unit-Datei:
systemd-run --on-calendar='Mon *-*-* 04:30:00' /usr/local/bin/backup.sh

# alle Timer mit nächstem Lauf:
systemctl list-timers --all

☸️Kubernetes CronJob

Der CronJob-Controller erzeugt zu jedem Termin ein Job-Objekt, das Pods startet. Syntax: klassische 5 Felder.
apiVersion: batch/v1
kind: CronJob
metadata:
  name: backup                  # max. 52 Zeichen
spec:
  schedule: "30 4 * * 1"
  timeZone: "Europe/Berlin"     # stabil seit v1.27
  concurrencyPolicy: Forbid     # Allow | Forbid | Replace
  startingDeadlineSeconds: 600
  successfulJobsHistoryLimit: 3 # Standard 3
  failedJobsHistoryLimit: 1     # Standard 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: registry.example.org/backup:1.4
              args: ["/app/backup.sh"]
concurrencyPolicy: Allow – Jobs dürfen parallel laufen.
concurrencyPolicy: Forbid – Läuft der vorige Job noch, wird der neue Termin übersprungen (zählt als verpasst).
✗✗✗✗✗✗
concurrencyPolicy: Replace – Der laufende Job wird durch den neuen ersetzt (abgebrochen).

Violett: läuft allein · Rot: parallel · Orange: durch Replace abgebrochen · ✗: übersprungen. Zeitraum 90 Minuten.

⚠️ Zeitzone und verpasste Termine
Ohne timeZone gilt die Zeitzone des kube-controller-managers. CRON_TZ= oder TZ= im schedule lehnt die API ab. Hat der Controller mehr als 100 Termine verpasst, startet er den Job nicht mehr („too many missed start times“) – startingDeadlineSeconds begrenzt das Zeitfenster. Laut Doku können in Ausnahmefällen zwei oder kein Job entstehen: Jobs sollten idempotent sein.

🐳Docker: Cron im Container oder auf dem Host?

🏠 Host-cron ruft Container
15 3 * * *  /usr/bin/docker compose -f /srv/app/compose.yml run --rm worker python cleanup.py
0 * * * *   /usr/bin/docker exec app-db pg_dump -U app app > /srv/backup/app.sql

Einfach und transparent. Nachteil: Zeitplan liegt außerhalb des Projekts; docker braucht vollen Pfad (PATH!) und Rechte.

📦 cron im Container
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y cron
COPY crontab /etc/cron.d/app
RUN chmod 0644 /etc/cron.d/app
CMD ["cron", "-f"]

Fallen: Jobs sehen die Container-Umgebung (-e DB_URL=…) NICHT – cron baut seine minimale Umgebung. Ausgaben erscheinen nur in docker logs, wenn man nach /proc/1/fd/1 umleitet. Dateiname in cron.d ohne Punkt!

🧰 Container-taugliche Runner

Werkzeuge wie supercronic lesen eine crontab, laufen im Vordergrund, reichen die Umgebung an Jobs weiter und schreiben die Ausgabe auf stdout – gemacht für Container. Alternativ ein eigener Scheduler-Dienst im Compose-Stack oder – im Cluster – ein Kubernetes CronJob.

🐙GitHub Actions: on.schedule

on:
  schedule:
    - cron: "30 3 * * 1-5"          # Standard: UTC
    - cron: "30 5 * * 1-5"
      timezone: "Europe/Berlin"     # optional, IANA-Name
  workflow_dispatch: {}             # manuell auslösbar zum Testen
By default, scheduled workflows run in UTC. You can optionally specify a timezone using an IANA timezone string […]. The shortest interval you can run scheduled workflows is once every 5 minutes. The schedule event can be delayed during periods of high loads […]. High load times include the start of every hour. In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days. GitHub Actions does not support the non-standard syntax @yearly, @monthly, @weekly, @daily, @hourly, and @reboot.
📖 GitHub Docs – Events that trigger workflows, schedule

📚Scheduler in der Anwendung

node-cron (Node.js)
import cron from "node-cron";
cron.schedule("30 4 * * 1", backup, {
  timezone: "Europe/Berlin",
  noOverlap: true,
});
// 6 Felder: "0 30 4 * * 1" (Sekunden vorne)
APScheduler (Python)
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
s = BlockingScheduler(timezone="Europe/Berlin")
s.add_job(backup, CronTrigger.from_crontab("30 4 * * 1"))
s.add_job(report, "cron", day_of_week="mon-fri", hour=5, minute=30)
s.start()
BullMQ (Node.js + Redis)
await queue.upsertJobScheduler(
  "backup-weekly",
  { pattern: "0 30 4 * * 1", tz: "Europe/Berlin" },
  { name: "backup", data: {} },
);
// Job Scheduler gibt es ab BullMQ 5.16
💡 Wanduhr-Semantik
APScheduler dokumentiert ausdrücklich: Der Cron-Trigger arbeitet mit Wanduhrzeit – in der übersprungenen Stunde läuft nichts, in der doppelten Stunde kann ein Job zweimal laufen. node-cron lässt die doppelte Stunde laut README nur einmal laufen. Wer lückenlose Abstände braucht, plant in UTC.

📊Vergleich

Merkmalcronsystemd-TimerKubernetes CronJobGitHub Actionsnode-cron / APScheduler / BullMQ
Syntax5 Felder, @-StringsOnCalendar=, Sekunden möglich5 Felder, @-Strings5 Felder, keine @-Strings5 oder 6 Felder (Sekunden)
ZeitzoneSystemzeit (cronie: CRON_TZ)Systemzeit oder Zone im Ausdruck.spec.timeZoneUTC, optional timezoneOption timezone / tz
Rechner war ausverpasst (→ anacron)Persistent=true holt nachstartingDeadlineSecondsläuft in GitHubs Cloudnur solange der Prozess läuft
Überlappungja (flock nötig)nein – läuft der Dienst noch, startet er nicht erneutconcurrencyPolicyWorkflow-concurrencyje nach Bibliothek (noOverlap, max_instances …)
LoggingMail/UmleitungJournal (journalctl -u)Pod-Logs, Job-HistorieActions-LogAnwendungs-Log
Kleinstes Intervall1 MinuteSekunden (AccuracySec)1 Minute5 Minuten1 Sekunde
AbhängigkeitenkeinesystemdClusterGitHub-Repolaufender Prozess, BullMQ: Redis