Remplacer Heroku Scheduler par des tâches cron sur votre serveur
Suivez ça sur votre propre serveur. Commencer gratuitement, sans carte
La documentation officielle de Heroku pour Scheduler dit les choses sans détour : « l’exécution des tâches de Scheduler est prévue, mais non garantie ». La même page précise que, dans de très rares cas, une tâche peut être sautée ou s’exécuter deux fois, et que les seules fréquences proposées sont toutes les 10 minutes, toutes les heures et une fois par jour. Pour tout ce que vous remarqueriez si ça manquait, un rapport nocturne, une vérification de renouvellement d’abonnement, une tâche de nettoyage qui empêche une table de grossir indéfiniment, ce n’est pas une base solide. La documentation de Heroku recommande elle-même un processus clock personnalisé pour tout ce qui est critique.
Ce guide remplace Scheduler par cron ordinaire sur un serveur que vous contrôlez : comment écrire la tâche, comment empêcher deux exécutions de se chevaucher, comment régler le fuseau horaire correctement, et où vont réellement les journaux.
Ce dont vous aurez besoin
- Un serveur sur lequel votre application tourne déjà, ou un serveur neuf si vous consolidez. Un Hetzner CX23 (2 vCPU, 4 Go de RAM, 40 Go NVMe, 5,99 €/mois hors taxes) suffit pour une application web et ses tâches planifiées.
- Un accès SSH avec un utilisateur pouvant modifier sa propre crontab, ou root si la tâche doit s’exécuter sous un compte de service.
- La liste de ce que Scheduler exécute actuellement. Ouvrez le tableau de bord de Scheduler avec
heroku addons:open scheduler -a your-app-nameet notez la commande, la fréquence et la prochaine heure d’exécution de chaque tâche avant de commencer.
Étape 1 : écrire la tâche sous forme de commande ou de script simple
Quoi que Scheduler exécute actuellement, que ce soit rails runner SomeJob.perform, python manage.py cleanup, ou un script SQL brut, il faut que ce soit quelque chose que vous puissiez invoquer comme une seule commande shell, sans que l’environnement du dyno Heroku fasse quoi que ce soit d’implicite pour vous. Enveloppez-le dans un petit script si ça dépasse une ligne, pour que cron n’ait qu’une seule chose à appeler et que vous ayez un seul endroit où ajouter la journalisation :
#!/usr/bin/env bash
set -euo pipefail
cd /opt/myapp
set -a
source /opt/myapp/.env
set +a
bundle exec rails runner "NightlyReportJob.perform_now" >> /var/log/myapp/nightly-report.log 2>&1
Rendez-le exécutable :
chmod +x /opt/myapp/bin/nightly-report.sh
set -euo pipefail compte ici plus que dans la plupart des scripts : cron ne relancera pas une tâche échouée à votre place, et sans cette ligne, une étape qui échoue en cours de route peut laisser le reste du script continuer silencieusement et signaler un succès. set -a avant source exporte chaque variable définie par le fichier, pour que la commande que vous exécutez les voie réellement ; un simple source ne les définirait que dans le shell propre au script.
Exécutez-le une fois à la main avant de le planifier :
/opt/myapp/bin/nightly-report.sh
tail -n 20 /var/log/myapp/nightly-report.log
Vous devriez voir la sortie normale de la tâche dans le journal, et echo $? juste après le script devrait afficher 0. Créez d’abord /var/log/myapp (sudo mkdir -p /var/log/myapp && sudo chown $USER /var/log/myapp) s’il n’existe pas.
Étape 2 : charger les variables d’environnement explicitement
C’est l’étape qui piège presque tout le monde en quittant Heroku. Les tâches de Scheduler s’exécutent dans un dyno ponctuel qui hérite automatiquement de l’ensemble complet de vos config vars. Cron s’exécute avec un environnement minimal, généralement seulement PATH, HOME, SHELL et ce que la crontab elle-même définit, rien du .env de votre application ni du profil de connexion de votre shell.
Chargez votre fichier d’environnement explicitement dans le script, comme montré plus haut, ou définissez les variables en haut de la crontab elle-même, une paire NAME=value par ligne :
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DATABASE_URL=postgres://user:pass@localhost:5432/myapp_production
Les deux fonctionnent. L’approche par script est plus facile à garder sous contrôle de version avec votre application ; l’approche par crontab garde les secrets hors d’un fichier qui est déployé avec votre code. Choisissez-en une et ne comptez pas sur cron pour récupérer quoi que ce soit d’un shell de connexion, parce qu’il ne le fera pas.
Étape 3 : régler le fuseau horaire correctement
La documentation de Heroku précise que la prochaine heure d’exécution des tâches quotidiennes de Scheduler est en UTC, et vous demande d’ajouter vous-même le décalage UTC si vous voulez une tâche à une heure locale précise. Si vous l’avez fait, votre horaire actuel est déjà écrit en UTC. Le cron système, lui, s’exécute dans le fuseau horaire configuré sur le serveur, qui est généralement UTC sur un serveur cloud neuf, mais ça vaut la peine de vérifier.
Vérifiez quelle heure le serveur pense qu’il est :
timedatectl
Vous devriez voir une ligne Time zone:. Si elle indique Etc/UTC (UTC, +0000), vous pouvez reprendre vos horaires de Scheduler sans les changer : une tâche quotidienne à 03:00 sur Heroku devient 0 3 * * *.
Si vous préférez écrire les horaires en heure locale, changez le fuseau horaire du serveur et redémarrez cron pour qu’il prenne le changement en compte :
sudo timedatectl set-timezone America/Toronto
sudo systemctl restart cron
Gardez en tête que ça change l’horloge pour tout sur le serveur, y compris les horodatages des journaux, et que l’heure locale subit les changements d’heure saisonniers : une tâche planifiée entre 02:00 et 03:00 peut être sautée ou s’exécuter deux fois les nuits de changement d’heure. Rester en UTC évite les deux problèmes, ce qui explique pourquoi plusieurs équipes gardent leurs serveurs en UTC et font la conversion de tête.
Étape 4 : ajouter l’entrée crontab, avec verrouillage
Installez la crontab pour l’utilisateur qui doit posséder la tâche :
crontab -e
*/10 * * * * /usr/bin/flock -n /tmp/myapp-sync.lock /opt/myapp/bin/sync-job.sh
0 3 * * * /usr/bin/flock -n /tmp/myapp-nightly-report.lock /opt/myapp/bin/nightly-report.sh
flock -n est la réponse directe au problème de Scheduler où « les exécutions peuvent se chevaucher », sauf qu’ici c’est vous qui le contrôlez au lieu d’espérer que ça n’arrive pas. Il prend un verrou exclusif sur le fichier donné avant d’exécuter la commande, et -n signifie qu’il abandonne immédiatement plutôt que de mettre en file une seconde exécution, si une exécution précédente de la même tâche détient encore le verrou. Une tâche de synchronisation de dix minutes qui prend parfois douze minutes sautera simplement cette exécution chevauchante au lieu d’empiler deux copies d’elle-même contre les mêmes lignes de la base de données.
Confirmez que la crontab est installée :
crontab -l
Vous devriez voir les deux lignes affichées exactement comme vous les avez saisies.
Étape 5 : envoyer les journaux quelque part où vous irez vraiment les consulter
Le comportement par défaut de cron, envoyer la sortie de la tâche par courriel à l’utilisateur local via sendmail, ne sert à rien sur un serveur sans agent de transport de courriel configuré, ce qui est le cas de la plupart. Sans redirection, la sortie disparaît tout simplement.
Le script de l’étape 1 redirige déjà vers un fichier journal, ce qui est la solution la plus simple. Pour tout ce que vous voulez regrouper plutôt que disperser dans des fichiers journaux par tâche, passez par logger pour l’envoyer au journal système :
0 3 * * * /usr/bin/flock -n /tmp/myapp-nightly-report.lock /opt/myapp/bin/nightly-report.sh 2>&1 | logger -t myapp-nightly-report
Suivez-le ensuite avec :
journalctl -t myapp-nightly-report -n 50
Vous devriez voir la sortie de la tâche horodatée selon l’horaire attendu, sans rien d’autre mélangé provenant d’autres tâches.
Étape 6 : si votre application tourne sous Docker Compose
Si votre application est déployée avec Docker Compose plutôt que directement sur l’hôte, gardez cron sur l’hôte et faites démarrer, par chaque entrée, un conteneur de courte durée à partir du service propre de l’application :
0 3 * * * /usr/bin/flock -n /tmp/myapp-nightly-report.lock /usr/bin/docker compose -f /opt/myapp/compose.yaml run --rm -T web bin/rails runner "NightlyReportJob.perform_now" >> /var/log/myapp/nightly-report.log 2>&1
docker compose run démarre un nouveau conteneur à partir de l’image du service web, avec le même env_file et le même environnement que ceux définis par le service, exécute la commande unique, et --rm supprime le conteneur ensuite. -T désactive le pseudo-terminal, que cron n’a pas. Comme la tâche s’exécute dans la même image que le processus web, elle utilise toujours le code de votre dernier déploiement.
Vérifiez d’abord le chemin avec which docker ; /usr/bin/docker est l’endroit où les paquets de Docker l’installent sur Ubuntu et Debian.
Faire tourner crond dans son propre conteneur est plus difficile que ça n’en a l’air : le démon cron démarre les tâches avec un environnement propre, donc les variables que Compose transmet au conteneur n’atteignent pas les tâches, et le binaire crond ainsi que l’emplacement de sa crontab diffèrent entre les images basées sur Alpine et celles basées sur Debian. Le cron de l’hôte combiné à docker compose run évite ces deux problèmes.
Le raccourci : le faire avec Railyard
La fonctionnalité Cron de Railyard s’occupe du script, de l’environnement et des journaux à votre place. Ajoutez une entrée cron avec une expression d’horaire comme 0 3 * * * et une commande, et Railyard l’exécute dans un conteneur ponctuel à partir de l’image propre à votre application, avec les variables d’environnement de l’application déjà attachées, donc il n’y a pas de crontab à modifier sur le serveur ni de .env à charger. Les horaires sont évalués en UTC, la même convention que les tâches quotidiennes de Heroku Scheduler, donc les heures que vous aviez écrites pour Heroku restent valides telles quelles. Chaque exécution apparaît dans l’historique d’exécution avec son code de sortie et sa sortie, donc « est-ce que la tâche d’hier soir s’est vraiment exécutée » devient une page que vous consultez plutôt qu’un fichier journal que vous devez penser à aller vérifier.
Une chose que Railyard ne fait pas, c’est sauter une exécution pendant que la précédente est encore en cours. Si une tâche peut durer plus longtemps que son intervalle, rendez-la sûre à exécuter deux fois, ou donnez-lui un intervalle plus long.
Ça se trouve à côté de l’onglet Workers, qui joue le même rôle pour les processus d’arrière-plan de longue durée comme Sidekiq ou Celery plutôt que pour les tâches planifiées ; voir Workers et tâches planifiées. Les deux utilisent la même image que celle de votre processus web, donc il n’y a rien de plus à construire ni à synchroniser.
Dépannage
La tâche s’exécute au mauvais moment. C’est presque toujours un décalage de fuseau horaire entre ce que vous pensiez configuré sur le serveur et ce que timedatectl rapporte réellement, surtout juste après avoir quitté le Scheduler de Heroku, qui fonctionne uniquement en UTC. Vérifiez d’abord le fuseau horaire du serveur, puis convertissez l’horaire vers le fuseau du serveur ou changez le fuseau du serveur comme montré à l’étape 3.
La tâche semble s’exécuter deux fois, ou interfère avec elle-même. C’est le problème de chevauchement « rare, mais ça arrive » de Scheduler qui réapparaît sur votre propre infrastructure, généralement parce qu’une tâche prend parfois plus de temps que son intervalle. Enveloppez la commande dans flock -n comme montré à l’étape 4 ; une exécution chevauchante sautée est presque toujours plus sûre que deux copies s’exécutant en même temps sur les mêmes données.
Rien n’apparaît dans aucun journal, et vous ne pouvez pas savoir si la tâche s’est exécutée. La sortie par courriel par défaut de cron ne va nulle part sur un serveur sans MTA configuré. Redirigez la sortie explicitement vers un fichier ou vers logger, et vérifiez que votre redirection fonctionne réellement en exécutant à la main, une fois, la commande exacte que cron exécuterait.
Le script fonctionne quand vous l’exécutez à la main, mais échoue sous cron. C’est presque toujours l’environnement manquant de l’étape 2. Exécutez-le avec un environnement réduit pour reproduire le problème localement : env -i /bin/bash -c '/opt/myapp/bin/nightly-report.sh' échouera de la même façon que l’environnement minimal de cron, ce qui est bien plus rapide pour trouver la variable manquante que de déboguer directement sur le serveur.
Permission refusée à l’exécution du script. Confirmez qu’il est exécutable (chmod +x) et que l’utilisateur dans la crontab duquel il se trouve possède réellement, ou peut lire, chaque fichier et répertoire touché par le script, y compris le répertoire parent du fichier journal si ce répertoire n’existe pas déjà.
FAQ
Est-ce que cron peut exécuter des tâches plus souvent que le minimum de dix minutes de Heroku Scheduler ? Oui. La syntaxe cron standard ne descend qu’à une fois par minute, mais vous pouvez aller plus loin en faisant lancer, par une entrée exécutée chaque minute, un court script qui boucle avec un sleep, si vous avez vraiment besoin d’une planification à l’infra-minute, ce qui est rare pour tout ce qui tournait auparavant sur Scheduler.
Quel est l’équivalent de heroku run pour une tâche ponctuelle ? Connectez-vous en SSH et exécutez la commande directement ; il n’y a pas de dyno à démarrer au préalable. Si la tâche a besoin exactement du même environnement que votre application en cours d’exécution, exécutez-la à l’intérieur du conteneur propre à l’application avec docker compose exec web <command> plutôt que de recourir à des outils au niveau de l’hôte qui pourraient voir un environnement différent.
Est-ce que cron survit à un redémarrage du serveur ? Oui, cron ou crond est un service système qui démarre au boot sur toutes les distributions Linux courantes, et les crontabs sont stockées sur disque, pas en mémoire. Confirmez qu’il est activé avec systemctl is-enabled cron (ou crond, selon la distribution) si vous voulez en être certain après une installation de serveur neuve.
Comment gérer les secrets dont une tâche planifiée a besoin, sans les mettre en clair dans le fichier crontab ? Gardez-les dans le fichier .env que le script charge, avec des permissions de fichier restrictives (chmod 600). La crontab de chaque utilisateur est déjà privée à cet utilisateur, mais garder les secrets dans un seul fichier .env signifie que l’application et chaque tâche planifiée lisent les mêmes valeurs, et faire tourner un secret devient une seule modification au lieu de plusieurs.
Que se passe-t-il si le serveur redémarre en plein milieu d’une tâche ? La même chose que si le processus était tué de toute autre manière : ce que la tâche était en train de faire s’arrête, inachevé. Les tâches idempotentes (sûres à exécuter deux fois, ou capables de reprendre où elles se sont arrêtées) tolèrent bien ça ; les tâches qui ne le sont pas doivent être rendues idempotentes peu importe ce qui les planifie, puisque le même mode de défaillance existe aussi sous Scheduler.
Quitter Scheduler consiste surtout à rendre explicite ce que Heroku faisait implicitement pour vous : l’environnement, le fuseau horaire, et ce qui se passe si deux exécutions se chevauchent. Une fois ces trois points réglés, cron devient plus simple à comprendre qu’un planificateur hébergé ne l’a jamais été, parce que vous pouvez lire exactement ce qu’il fera dans un fichier texte plutôt que de faire confiance à un tableau de bord.
Pourquoi les gens choisissent Railyard pour ça
- Votre propre compte infonuagique. Déployez sur DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou n'importe quel serveur Linux par SSH, facturé directement par votre fournisseur, sans majoration.
- Un prix fixe par équipe. L'offre gratuite est gratuite pour toujours : un serveur plus un serveur de base de données, trois applications, sans carte. Pro coûte 19 $ par mois pour jusqu'à cinq serveurs et un nombre illimité d'applications. Team coûte 49 $ par mois avec des serveurs illimités. Aucuns frais par application, par déploiement, par siège ou par gigaoctet.
- Des gabarits en un clic. Déployez des applications open source, comme des bases de données, des outils d'analyse et des outils de développement, sur votre propre serveur en un clic.
- Des bases de données gérées. Postgres, MySQL et Redis, avec des sauvegardes planifiées, vérifiées par restauration, et la récupération à un point dans le temps.
- Déploiement par push. Déployez depuis GitHub, GitLab ou la CLI, avec HTTPS automatique et des mises en production sans interruption.