Pourquoi votre tâche cron ne s’exécute pas
Cinq causes fréquentes : PATH minimal, signe % non échappé, mauvais fichier crontab, sortie envoyée par e-mail et script dépendant du shell.
Pourquoi votre tâche cron ne s’exécute pas
Une tâche cron qui « ne s’exécute jamais » s’est presque toujours exécutée. Elle a été lancée dans un environnement différent de votre shell, a échoué dès la première seconde et son message a été envoyé à un endroit que vous ne consultez pas. Cinq causes expliquent presque tous les signalements : le chemin de recherche, le signe pourcentage, le mauvais fichier crontab, la sortie envoyée par e-mail et un script qui attend une session de connexion.
cron est un daemon (un service en arrière-plan) qui lit les fichiers crontab et lance des commandes selon un calendrier. Il ne lit pas votre .bashrc, n’ouvre pas de terminal, ne lance pas de login shell et ne vous indique pas quand une commande échoue. Toutes les causes ci-dessous découlent de ces quatre faits.
Examinez-les dans l’ordre et commencez par la question qui les sous-tend toutes : cron a-t-il déclenché la tâche ? « cron n’a jamais lancé la tâche » et « la tâche a démarré puis s’est arrêtée » sont deux problèmes différents qui n’ont rien en commun. Répondez d’abord à cette question.
Le cron s’est-il exécuté ?
Le daemon utilise un nom d’unité différent selon la famille de distributions. Vérifiez les deux, puis consultez le journal.
systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"Debian et Ubuntu appellent cette unité cron. Fedora, Rocky et Alma l’appellent crond. Un seul de ces noms existe sur une machine donnée. Il est donc normal que l’une des deux commandes signale une unité inconnue. Ce n’est pas une erreur.
Consultez les entrées écrites par votre propre système. Ne recherchez pas une ligne copiée depuis un guide, car le libellé varie selon les implémentations de cron et la configuration de la journalisation. Vous devez vérifier deux éléments : une entrée existe-t-elle à la minute indiquée par votre planification, et cette entrée mentionne-t-elle votre commande ? Une entrée qui mentionne votre commande signifie que cron a effectué son travail et que l’échec se situe dans la commande. L’absence totale d’entrée signifie que cron n’a jamais chargé votre planification. C’est la cause 3 ci-dessous.
Certaines images transmettent les messages de cron à rsyslog, qui les écrit dans un fichier au lieu du journal. Recherchez dans /var/log un fichier dont le nom fait référence à cron ou à syslog, puis affichez-en la fin.
ls -l /var/log
sudo tail -n 50 /var/log/syslogSi ni l’unité ni le journal n’existent, cron n’est peut-être tout simplement pas installé. Les images cloud minimales et les conteneurs l’excluent souvent.
dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cronCause 1 : cron n’a pas votre PATH
Votre shell interactif construit PATH à partir de /etc/profile, ~/.profile, ~/.bashrc et de tout ce que ces fichiers chargent. Rien de tout cela ne s’exécute pour une tâche cron. cron lance la commande avec son propre environnement minimal. Un programme installé en dehors des répertoires système standard n’est donc pas trouvé. Cela peut concerner tout ce qui se trouve sous /usr/local/bin, /opt, un gestionnaire de versions de langage, un environnement virtuel Python ou un espace de travail Go. La tâche échoue dès sa première ligne. Le shell écrit une erreur de type « not found », dont la formulation exacte dépend du shell qui l’a exécutée.
Trouvez le chemin réel de chaque commande utilisée par votre tâche.
command -v docker
command -v node
readlink -f "$(command -v node)"Inscrivez ensuite ces chemins absolus dans la tâche, ou définissez PATH une seule fois au début de la crontab.
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool runRécupérez cette liste sur votre propre machine avec echo "$PATH", puis supprimez tout ce qui existe uniquement dans une session interactive. Une règle est importante : cron ne développe pas les variables dans ces lignes d’affectation. PATH=$PATH:/usr/local/bin conserve le texte littéral $PATH:/usr/local/bin. La tâche se retrouve donc avec un chemin de recherche qui ne contient aucun répertoire utilisable. Écrivez toute la liste explicitement.
Un gestionnaire de versions nécessite davantage qu’un chemin. nvm, pyenv, rbenv et asdf installent une fonction shell ou un répertoire de shims depuis votre .bashrc, et une tâche cron ne lit jamais ce fichier. Appelez le binaire de la version voulue avec son chemin absolu, ou chargez le script d’initialisation du gestionnaire comme première ligne de votre propre script.
Cause 2 : le signe pourcentage termine votre commande
Dans le champ de commande d’une crontab, % n’est pas un caractère ordinaire. Le premier % non échappé termine la commande. Tout ce qui suit est transmis à la commande sur son entrée standard, et chaque % supplémentaire devient un retour à la ligne. Il s’agit d’une fonctionnalité réelle de cron pour fournir une entrée courte à un programme. C’est aussi pourquoi un nom de fichier horodaté constitue l’exemple classique d’une entrée de crontab incorrecte.
Écrivez 0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site et tar ne voit jamais une date formatée. cron coupe la ligne au premier %. Le shell reçoit donc une substitution de commande incomplète, et le reste de votre ligne arrive sur l’entrée standard. Échappez chaque signe pourcentage avec une barre oblique inverse.
0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/siteDeux couches lisent cette ligne, dans cet ordre. \% est une règle cron, appliquée par cron avant de démarrer quoi que ce soit. $(date +\%F) est une substitution de commande, appliquée ensuite par le shell démarré par cron. Il faut déterminer quelle couche traite chaque caractère. C’est le point essentiel.
La méthode la plus sûre consiste à placer toute la logique en dehors de la crontab. Mettez-la dans un script, où le signe pourcentage n’a aucune signification particulière.
#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/siteLa ligne de crontab ne contient alors plus qu’un chemin et une redirection. Une crontab lisible en un coup d’œil est une crontab que vous pouvez déboguer.
Cause 3 : quelle crontab avez-vous modifiée ?
Il n’existe pas une seule crontab. Il y a plusieurs fichiers, avec des propriétaires et un nombre de champs différents. Une tâche écrite dans le mauvais fichier est invisible.
crontab -emodifie la crontab de l’utilisateur qui exécute la commande.sudo crontab -emodifie celle de root. Deux personnes qui déboguent le même serveur finissent souvent par lire deux fichiers différents.sudo crontab -l -u deployaffiche la crontab d’un autre utilisateur. C’est la commande à utiliser pour vérifier ce qui est réellement installé pour le compte qui doit exécuter la tâche./etc/crontabet tous les fichiers de/etc/cron.dcontiennent un champ supplémentaire entre la planification et la commande : l’utilisateur sous lequel la tâche doit s’exécuter. Si vous collez dans/etc/cron.dune ligne de crontab utilisateur à cinq champs, le premier mot de votre commande est interprété comme un nom d’utilisateur.- Les fichiers de
/etc/cron.ddoivent porter un nom composé de lettres, de chiffres, de tirets bas et de traits d’union. Un fichier nommébackup.shousite.confest ignoré à cause de son nom. Renommez-le enbackup, puis vérifiez de nouveau le journal. - Les fichiers de
/etc/cron.ddoivent appartenir à root et ne doivent pas être accessibles en écriture au groupe ou aux autres utilisateurs.ls -l /etc/cron.daffiche ces deux informations en une seule fois. - Les scripts placés dans
/etc/cron.dailyet dans ses répertoires associés doivent respecter la même règle de nommage et posséder le bit d’exécution. Un bit d’exécution manquant entraîne une omission silencieuse. /etc/cron.allowet/etc/cron.denydéterminent qui peut installer une crontab. Si l’un de ces fichiers existe sur votre serveur, consultez-le avant de supposer que votre utilisateur est autorisé à en avoir une.
Installez une crontab utilisateur avec la commande crontab au lieu de modifier directement le spool, car crontab analyse le fichier avant de l’installer. Après l’enregistrement, lisez ce que la commande affiche. Si elle refuse le fichier, la version précédente reste active et votre modification n’est jamais appliquée. Le résultat ressemble exactement à un cron qui vous ignore.
Le propriétaire détermine également les permissions. Une tâche dans la crontab de root crée des fichiers appartenant à root, que l’application qui les lit peut ne pas pouvoir modifier. Une tâche dans la crontab d’un utilisateur normal ne peut pas lire un répertoire réservé à root. Faites correspondre le propriétaire à la tâche : la maintenance d’une application doit être exécutée avec le compte de cette application. C’est le principe qui sous-tend le remplacement de wp-cron de WordPress par une tâche cron système. Le mode des fichiers créés par votre tâche provient de l’umask qu’elle hérite. Cette valeur n’est pas forcément celle de votre shell. Consultez donc le fonctionnement de umask pour définir les permissions des fichiers si la sortie d’une tâche est illisible.
Cause 4 : la sortie est envoyée vers une boîte que personne ne consulte
cron collecte tout ce qu’une tâche écrit sur la sortie standard et la sortie d’erreur standard. Si la tâche écrit quoi que ce soit, cron remet ce texte au système de messagerie local, à l’adresse du propriétaire de la crontab ou à celle indiquée par MAILTO. Sur un VPS minimal, aucun MTA (mail transfer agent) n’est généralement installé. Rien ne remet donc ce message. Votre erreur a existé un instant, puis a disparu. C’est la raison pour laquelle une tâche défaillante semble silencieuse.
Envoyez plutôt la sortie vers un fichier que vous contrôlez.
0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1>> ajoute la sortie standard à la fin du fichier. 2>&1 redirige la sortie d’erreur standard vers la destination actuelle de la sortie standard. Il doit donc apparaître après la redirection. Dans l’ordre inverse, avec 2>&1 >> file, la sortie d’erreur standard conserve sa destination initiale. L’erreur recherchée est alors précisément la partie qui n’atteint jamais le fichier.
Le journal constitue une autre bonne cible. logger écrit dans syslog avec le tag de votre choix.
0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-siteRelisez les messages avec journalctl -t backup-site. La sortie de la tâche reste ainsi à côté des entrées cron, ce qui facilite le suivi de la chronologie. Si vous devez également savoir quelle personne a exécuté quelle commande sur le serveur, il s’agit d’un système distinct. Consultez l’audit des commandes utilisateur sur votre serveur.
MAILTO="" en haut d’une crontab désactive l’envoi de messages pour les tâches qui suivent. Définir MAILTO avec une adresse réelle n’est utile que si un MTA fonctionnel existe. Vérifiez donc d’abord que les messages quittent bien le serveur avant de compter sur ce mécanisme.
Une règle pendant le dépannage : n’ajoutez jamais > /dev/null 2>&1. C’est la ligne la plus courante dans les crontab, et elle supprime le seul élément permettant de comprendre le problème. Vous pourrez la remettre plus tard, une fois la tâche fonctionnelle.
Cause 5 : le script suppose un environnement que cron ne lui fournit pas
Une fois la commande trouvée et sa sortie récupérée, il reste tout ce que votre session vous fournit automatiquement.
- Le shell n’est peut-être pas bash. Vérifiez avec
ls -l /bin/sh. Sur Debian et Ubuntu, il pointe vers dash ; le test à double crochet, les tableaux etsourceéchouent donc avec une erreur de syntaxe. Ajoutez une ligne#!/bin/bashau script et appelez le script, ou définissezSHELLau début de la crontab. - Le répertoire de travail n’est pas celui dans lequel vous vous trouviez. Utilisez des chemins absolus partout, ou utilisez
cdvers le répertoire sur la première ligne du script. Un chemin relatif est la raison la plus fréquente pour laquelle une tâche « fonctionne quand je la lance manuellement ». - La locale n’est pas celle de votre session. Toute commande qui formate une date ou un nombre, ou qui trie du texte, peut produire une sortie différente avec une autre
LANG. Si une étape ultérieure analyse cette sortie, définissez la locale dans le script au lieu de compter sur l’environnement. - Aucun TTY (terminal) n’est disponible. Une commande qui demande une confirmation, ouvre un éditeur ou affiche une barre de progression peut rester bloquée ou quitter. Ajoutez l’option non interactive proposée par l’outil.
- Aucun agent SSH n’est disponible.
SSH_AUTH_SOCKn’est pas présent dans l’environnement de cron ; une commandesshoursyncqui fonctionnait parce que votre agent était chargé échoue maintenant lors de l’authentification. Fournissez à la tâche sa propre clé, détenue par l’utilisateur qui l’exécute. - Aucun bus de session utilisateur n’est disponible ;
systemctl --userexécuté depuis cron échoue donc tant queXDG_RUNTIME_DIRn’est pas défini. Une unité systemd est préférable.
Sur Fedora, Rocky et Alma, il faut aussi vérifier un autre élément. SELinux restreint les tâches cron ; une tâche qui accède à un chemin portant un contexte inattendu est donc refusée, même si les permissions du fichier semblent correctes. Recherchez les refus avec sudo ausearch -m avc -ts recent, puis consultez les bases de SELinux pour un serveur avant de désactiver quoi que ce soit.
La sonde d’une minute qui vous montre l’environnement de cron
Ne devinez plus ce que contient l’environnement de cron : lisez-le. Écrivez un script qui enregistre tout, planifiez-le chaque minute, attendez, puis lisez le fichier.
cat > /home/deploy/cron-probe.sh <<'EOF'
#!/bin/bash
echo "=== probe ==="
date -Is
pwd
id
echo "SHELL=$SHELL"
echo "LANG=$LANG"
command -v node || echo "node is not on this PATH"
env | sort
EOF
chmod +x /home/deploy/cron-probe.shAjoutez une ligne à la crontab de l’utilisateur qui exécute réellement le job, avec des chemins absolus de part et d’autre.
* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&1Attendez une minute, puis lisez /home/deploy/cron-probe.log et comparez son contenu avec les mêmes commandes exécutées dans votre propre shell. La ligne PATH, le répertoire de travail et la locale expliquent généralement le problème à eux seuls. Notez deux détails de cette configuration : les signes pourcentage se trouvent dans le script, où la règle de cron ne s’applique pas, et le chemin du journal est accessible en écriture par l’utilisateur du job.
Supprimez cette ligne de la crontab dès que vous avez trouvé la cause. Un job exécuté chaque minute et ajouté à un fichier finira par remplir un petit disque, et il le fera silencieusement.
Le planning est-il bien celui que vous aviez prévu ?
Une ligne de crontab utilisateur commence par cinq champs : minute, heure, jour du mois, mois, jour de la semaine. Deux de ces champs interagissent d’une manière qui surprend souvent.
Lorsque le jour du mois et le jour de la semaine sont tous deux restreints, c’est-à-dire qu’aucun des deux ne vaut *, cron exécute la tâche lorsque l’un ou l’autre des champs correspond. 0 0 13 * 5 ne signifie pas « vendredi 13 ». La tâche s’exécute à minuit le 13 de chaque mois et à minuit chaque vendredi. Pour cibler un seul jour précis, laissez l’un des deux champs à * et testez l’autre dans le script.
cron utilise le fuseau horaire du système. De nombreuses images VPS sont configurées sur UTC (temps universel coordonné). Une tâche planifiée à 03:00 s’exécute donc à 03:00 UTC, ce qui peut correspondre au milieu de votre après-midi. timedatectl affiche le fuseau horaire réellement utilisé par votre serveur. Consultez cette valeur au lieu de supposer qu’elle correspond à celle de votre ordinateur portable.
Deux autres pièges liés au planning sont à connaître. @reboot s’exécute lorsque cron démarre lui-même. Ce n’est pas nécessairement le moment où le réseau est prêt. Une tâche qui dépend de DNS ou d’un hôte distant peut donc échouer au démarrage, puis réussir à chaque exécution manuelle. Rien n’empêche non plus une tâche lente de démarrer alors que l’exécution précédente est encore en cours. Utilisez un verrou.
*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1flock -n abandonne immédiatement lorsque le verrou est déjà détenu. L’exécution simultanée s’arrête donc au lieu de s’empiler sur la première.
Quand un timer systemd est le meilleur outil
cron est efficace pour une seule chose : exécuter cette commande à cette heure. Il est limité pour tout le reste. Un timer fournit le journal sans redirection, un code de sortie que vous pouvez consulter ultérieurement, un ordre d’exécution par rapport à network-online.target et un délai aléatoire afin que 100 serveurs ne démarrent pas tous à la même seconde. Lorsque votre tâche nécessite l’un de ces éléments, un service et un timer systemd sur un VPS demandent moins de travail que la maintenance d’une ligne crontab. Le comportement en cas de nouvelle tentative doit également être défini à cet endroit, car les politiques de redémarrage systemd déterminent ce qui se passe après un échec, alors que cron n’apporte aucune réponse à cette question.
Conservez cron pour les petites tâches. Déplacez vers un timer tout ce qui comporte des dépendances ou une politique de nouvelle tentative. Les deux peuvent fonctionner sur le même serveur : vous n’avez donc pas à effectuer cette migration en une seule fois.
FAQ
Pourquoi ma tâche cron fonctionne-t-elle manuellement, mais échoue-t-elle avec cron ?
Parce que votre shell et l’environnement de cron sont différents. Votre shell de connexion lit /etc/profile et ~/.bashrc, qui définissent PATH, la locale et les variables de votre agent. cron démarre la commande sans ces éléments, depuis un autre répertoire de travail et parfois avec un autre shell. Utilisez des chemins absolus pour chaque commande, définissez les variables nécessaires au début de la crontab ou dans le script, puis planifiez une tâche de test exécutée chaque minute. Celle-ci doit écrire env | sort, pwd et id dans un fichier journal afin que vous puissiez lire l’environnement réel de cron au lieu de le déduire.
Comment vérifier que cron a réellement exécuté ma tâche ?
Consultez le journal du daemon. Utilisez journalctl -u cron sur Debian et Ubuntu, ou journalctl -u crond sur Fedora, Rocky et Alma. Certaines images envoient plutôt ces messages via rsyslog dans un fichier sous /var/log. Recherchez une entrée à la minute indiquée par votre planification et vérifiez qu’elle mentionne votre commande. L’absence d’entrée signifie que cron n’a jamais chargé cette planification. Vérifiez donc que vous avez modifié la bonne crontab. Une entrée sans résultat signifie que la commande a démarré puis s’est arrêtée. Capturez sa sortie avec une redirection.
Pourquoi date +%Y provoque-t-il une erreur dans une crontab ?
cron traite % comme un caractère spécial dans le champ de commande. Le premier % non échappé termine la commande. Tout ce qui suit est transmis à cette commande sur son entrée standard, et chaque % supplémentaire devient un retour à la ligne. Un nom de fichier contenant une date formatée n’est donc jamais transmis au programme prévu. Échappez chaque signe de pourcentage avec \%, ou déplacez la commande dans un script et appelez ce script depuis cron. Dans un script, le signe de pourcentage n’a aucune signification particulière.
Où va la sortie de ma tâche cron ?
Elle est envoyée au système de messagerie local, à l’utilisateur propriétaire de la crontab ou à l’adresse indiquée par MAILTO. La plupart des images VPS n’ont aucun agent de transfert de courrier installé. Le message est donc supprimé et la tâche semble ne produire aucune sortie. Redirigez la sortie vers un fichier avec >> /path/to/log 2>&1, en conservant cet ordre pour que la sortie d’erreur standard suive la sortie standard, ou envoyez-la dans logger -t myjob et relisez-la avec journalctl -t myjob. N’utilisez pas > /dev/null 2>&1 pendant le diagnostic.
Dois-je utiliser cron ou un timer systemd ?
Utilisez cron pour exécuter une commande simple à une heure fixe, notamment si vous devez pouvoir la déplacer vers une machine qui n’exécute pas systemd. Utilisez un timer lorsque vous voulez écrire la sortie dans le journal sans redirection, interroger le code de sortie, attendre que le réseau soit opérationnel, ajouter un délai de démarrage aléatoire ou réessayer après un échec. Les deux mécanismes peuvent fonctionner sur le même serveur. Vous pouvez donc déplacer les tâches une par une lorsque cela devient nécessaire.