← Blog

Wie du einen Cron-Job ohne eigenen Server betreibst

Railyard

4. August 2026 · 6 Min. Lesezeit

Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig

Der Job selbst ist klein: eine Tabelle sichern, ein wöchentliches Digest versenden, ein paar Zeilen ablaufen lassen. Einen ganzen Server, oder eine ganze App aufzusetzen, um einen einzigen Befehl nach einem Timer auszuführen, fühlt sich unverhältnismäßig zur Aufgabe an, und das ist ein berechtigter Instinkt. Dieser Guide behandelt, was du wirklich brauchst für ein Skript, das nach einem Zeitplan laufen muss statt auf Requests zu antworten: zuerst die reine manuelle Version (eine billige VM, Crontab, und den Teil, den viele überspringen, zu bestätigen, dass es wirklich lief), dann Railyards echtes Cron-Feature, akkurat statt aspirativ beschrieben.

Was du brauchst

  • Das Skript selbst, ausführbar als einzelner Befehl (node send-digest.js, python cleanup.py, ein Shell-Einzeiler), mit allen Umgebungsvariablen, die es braucht, bereits identifiziert.
  • Für den manuellen Weg: SSH-Zugang zu irgendeiner kleinen Linux-VM. Ein Hetzner CX23 (2 vCPU, 4 GB RAM, 5,99 € im Monat exkl. MwSt.) reicht mehr als aus für ein Skript, das für Sekunden oder Minuten nach einem Zeitplan läuft, nicht durchgehend.

Die manuelle Version: eine VM, Crontab, und der Beweis, dass es lief

Den Job als einen Befehl schreiben

Wickle alles, was mehr als eine Zeile ist, in ein Skript, damit Cron nur eine Sache aufrufen muss:

#!/usr/bin/env bash
set -euo pipefail

cd /opt/myjob
set -a
source /opt/myjob/.env
set +a

node send-digest.js >> /var/log/myjob/digest.log 2>&1

set -euo pipefail ist hier speziell wichtig, weil Cron einen fehlgeschlagenen Lauf nicht für dich wiederholt; ohne das kann ein Schritt, der mittendrin fehlschlägt, den Rest des Skripts trotzdem weiterlaufen und Erfolg melden lassen. set -a vor source exportiert jede Variable, die die .env-Datei definiert, damit der Befehl in der letzten Zeile sie tatsächlich erbt; Cron selbst startet Jobs mit einer fast leeren Umgebung, nicht der deiner Shell.

Mach es ausführbar und führe es einmal von Hand aus, bevor du es planst:

chmod +x /opt/myjob/run.sh
/opt/myjob/run.sh
echo $?

Du solltest 0 sehen.

Den Crontab-Eintrag mit Locking hinzufügen

crontab -e
0 3 * * * /usr/bin/flock -n /tmp/myjob.lock /opt/myjob/run.sh

flock -n nimmt einen exklusiven Lock, bevor der Befehl läuft, und gibt sofort auf, statt einen zweiten Lauf einzureihen, falls der vorherige Lauf den Lock noch hält. Ohne das überlappt ein Job, der gelegentlich lange läuft, mit seinem eigenen nächsten geplanten Lauf, oft gegen dieselben Zeilen.

Bestätigen, dass es wirklich lief, nicht nur, dass es geplant war

Dass ein Crontab-Eintrag existiert, sagt nichts darüber aus, ob der Job letzte Nacht erfolgreich war. Die ehrliche Version von “Monitoring” hier ist ein Dead-Man’s-Switch: Der Job selbst pingt bei Erfolg eine URL an, und etwas alarmiert dich, wenn dieser Ping nicht pünktlich ankommt. Diese Prüfung selbst zu hosten ist selbst ein kleiner, immer laufender Service; siehe Uptime Kuma selbst hosten oder Healthchecks selbst hosten für genau dieses Stück. Der Crontab-Eintrag wird zu:

0 3 * * * /usr/bin/flock -n /tmp/myjob.lock /opt/myjob/run.sh && curl -fsS https://hc-ping.com/your-check-id

Das && bedeutet, dass der Ping nur ausgelöst wird, wenn run.sh tatsächlich erfolgreich beendet wurde; ein stiller Fehler zeigt sich jetzt als verpasster Check-in statt als gar nichts.

Für die Zeitzonen- und vollständigen Logging-Details, die diese verkürzte Version auslässt, deckt Heroku Scheduler durch Cron ersetzen denselben Boden ausführlicher ab, einschließlich warum das Standard-UTC-vs-lokale-Zeit-Verhalten von System-Cron fast jeden erwischt, der von einem Plattform-Scheduler wegzieht.

Was Railyards Cron tatsächlich ist

Railyard hat ein echtes Feature für geplante Aufgaben, im Dashboard Cron genannt, und es lohnt sich, es präzise statt vage zu beschreiben. Füge einen Eintrag mit Namen, Zeitplan und Befehl hinzu:

  • Der Zeitplan ist ein gewöhnlicher fünfteiliger Cron-Ausdruck (0 3 * * *), geparst und ausgewertet genau wie System-Cron ihn liest, in UTC.
  • Der Befehl läuft in einer frischen Instanz des eigenen Images deiner App, exakt dem Build, den deine Web- und Worker-Prozesse bereits ausführen, kein separater Container, den du von Hand synchron halten musst.
  • Jeder Lauf wird aufgezeichnet: Startzeit, Exit-Code und Ausgabe, als Run-History pro Job aufbewahrt, sodass “ist der Job letzte Nacht wirklich gelaufen” eine Seite ist, die du prüfst, statt eine Logdatei, deren Existenz du dir merken musst.

Dieser letzte Punkt ersetzt den Dead-Man’s-Switch-Workaround aus der manuellen Version oben durch etwas Eingebautes: Die Run-History selbst ist die Aufzeichnung darüber, ob der Job lief und was passiert ist, ohne dass ein zweiter Service einen dritten anpingt, um es dir zu sagen.

Eine ehrliche Einschränkung, dieselbe, auf die du stoßen würdest, wenn du das selbst betreibst: Railyards Cron überspringt keinen Lauf, während ein vorheriger Lauf desselben Jobs noch läuft. Der flock -n-Schutz aus der manuellen Version ist etwas, das du weiterhin selbst in den Job einbauen musst, falls er sein eigenes Intervall realistisch überlaufen kann, entweder indem du den Befehl sicher für gleichzeitiges zweifaches Ausführen machst, oder indem er prüft und frühzeitig abbricht, wenn er erkennt, dass ein vorheriger Lauf noch läuft.

Siehe Workers und geplante Jobs dafür, wie Cron neben dem separaten Workers-Tab steht, der lang laufende Hintergrundprozesse (zum Beispiel einen Queue-Consumer) statt geplanter Aufgaben behandelt; beide laufen im gleichen Image, sodass ein Deploy, der die App fixt, konstruktionsbedingt gleichzeitig ihre geplanten Jobs fixt.

Einrichten

Füge den Job über den Cron-Tab der App hinzu: einen Namen, den Zeitplan-Ausdruck und den Befehl, genau so, wie du ihn eingeben würdest, wenn du dich per SSH in den eigenen Container der App einloggen und ihn direkt ausführen würdest. Umgebungsvariablen, die der Job braucht, sind dieselben, die bereits unter dem Variables-Tab der App gesetzt sind, da der Befehl in diesem gleichen Image und derselben Umgebung läuft; es gibt keine separate .env-Datei zum Sourcen und keine Crontab zum Bearbeiten auf einem Server, weil es überhaupt keinen separaten Server gibt, der den Job ausführt. Siehe Variablen und Secrets.

Troubleshooting

Der Job läuft zur falschen Zeit. Railyards Cron wertet Zeitpläne in UTC aus; wenn du einen Zeitplan unter der Annahme lokaler Zeit geschrieben hast, konvertiere ihn nach UTC, statt anzunehmen, dass die Plattform ihn anders liest. Das ist derselbe Umrechnungsschritt, der Leute beim Wegzug von einem Plattform-Scheduler stolpern lässt, der ebenfalls UTC nutzte, wie Herokus.

Der Job scheint sich mit sich selbst zu überlappen. Wie oben erwähnt, wird das nicht automatisch verhindert; füge dieselbe Schutzmaßnahme hinzu, die du auch einem gewöhnlichen Crontab-Eintrag hinzufügen würdest, entweder Idempotenz im Befehl selbst oder eine explizite Lock-Prüfung am Anfang des Skripts, falls der Job sein eigenes Intervall realistisch überschreiten kann.

Es zeigt sich nichts, was bestätigt, dass der Job gelaufen ist. Prüfe zuerst direkt die Run-History im Cron-Tab statt Anwendungslogs; ein Lauf, der mit einem Exit-Code ungleich null abgeschlossen hat, zeigt sich dort trotzdem mit seinem Exit-Code und seiner Ausgabe, was meist schneller zu lesen ist, als dieselbe Information aus dem Logs-Tab zu rekonstruieren.

FAQ

Brauche ich eine ganze App nur um ein geplantes Skript auszuführen? Nicht mehr, als du einen ganzen Server für die manuelle Version brauchen würdest. Wenn das Skript tatsächlich das Einzige ist, was du deployst, ganz ohne Web-Prozess, ist das eine normale, unterstützte Form; ein geplanter Job und eine Worker-only-App bedeuten beide einfach, dass es keinen web-Prozesstyp gibt, nicht, dass etwas fehlt.

Was ist das minimale Intervall, in dem ein Zeitplan laufen kann? Standard-fünfteilige Cron-Syntax geht runter bis auf einmal pro Minute (* * * * *), dieselbe Untergrenze wie System-Cron; nichts unterhalb einer Minute lässt sich in diesem Format ausdrücken, unabhängig davon, wo es ausgewertet wird.

Wie unterscheidet sich das von einem Background-Worker? Ein Worker ist ein Prozess, der einmal startet und unbegrenzt weiterläuft, eine Queue konsumiert oder eine Verbindung offen hält. Ein Cron-Job startet, erledigt eine Sache und beendet sich, nach einem Zeitplan. Es sind unterschiedliche Prozessformen für unterschiedliche Aufgaben, abgedeckt durch zwei separate, verwandte Features, statt eines, das beides versucht.

Was passiert, wenn der zugrunde liegende Server mitten im Job neu startet? Dasselbe, was mit jedem Prozess passiert, der auf irgendeine andere Weise beendet wird: Er stoppt, unvollständig. Ein Job, der sicher zweimal laufen kann (idempotent), toleriert das ohne besondere Behandlung; einer, der das nicht ist, muss idempotent gemacht werden, unabhängig davon, was ihn plant, da derselbe Fehlerfall auch unter gewöhnlichem Cron auf einer VM existiert.

Kann ich den Job manuell auslösen, nicht nur nach seinem Zeitplan? Ja, ein geplanter Job unterstützt das Ausführen auf Abruf von derselben Stelle, an der du seine Run-History prüfen würdest, unabhängig vom Warten auf seine nächste geplante Zeit, was nützlich ist, um eine Änderung zu testen, bevor du sie dem eigentlichen 3-Uhr-Slot anvertraust.

Ob es eine Crontab-Zeile auf einer Fünf-Dollar-VM ist oder ein Eintrag in einem Dashboard, ein Cron-Job ist darunter dasselbe Dreigespann: ein Befehl, ein Zeitplan und ein Weg zu wissen, dass er wirklich lief. Den dritten Punkt richtig hinzubekommen ist meist der Teil, der gefehlt hat, nicht die ersten beiden.

Warum Leute sich dafür Railyard entscheiden

  • Dein eigenes Cloud-Konto. Deploye auf DigitalOcean, Hetzner, Vultr, AWS, OVHcloud oder jedem Linux-Server per SSH, direkt von deinem Anbieter abgerechnet, ohne Aufschlag.
  • Ein fester Preis pro Team. Free ist für immer kostenlos: ein Server plus ein Datenbankserver, drei Apps, keine Karte. Pro kostet 19 $ im Monat für bis zu fünf Server und unbegrenzte Apps. Team kostet 49 $ im Monat mit unbegrenzten Servern. Keine Kosten pro App, pro Deployment, pro Sitz oder pro Gigabyte.
  • Templates mit einem Klick. Deploye Open-Source-Apps, wie Datenbanken, Analytics und Dev-Tools, mit einem Klick auf deinem eigenen Server.
  • Verwaltete Datenbanken. Postgres, MySQL und Redis mit geplanten, wiederherstellungsgeprüften Backups und Point-in-Time-Recovery.
  • Push-to-Deploy. Deploye von GitHub, GitLab oder der CLI, mit automatischem HTTPS und Releases ohne Ausfallzeit.

Plane es direkt von der App-Seite aus

Railyards Cron führt einen Befehl im eigenen Image deiner App nach einem Zeitplan aus, mit Run-History für jede Ausführung, keine eigene Crontab oder VM.

Kostenloser Plan, keine Kreditkarte, und 14 Tage Pro für jedes neue Team. Ein fester Preis pro Team.