Exécuter des tâches cron pour des applications Dockerisées
Suivez ça sur votre propre serveur. Commencer gratuitement, sans carte
Votre application tourne bien dans Docker, mais la partie qui ne survit pas au passage en conteneur, c’est cron. Il n’y a pas de démon cron dans une image d’application typique, et même si vous en installiez un, un redémarrage de conteneur efface tout l’état qu’il suivait. Vous avez besoin d’une tâche de nettoyage nocturne, d’un rapport qui part chaque lundi, ou d’un réchauffement de cache toutes les 15 minutes, et il faut que ça survive aux déploiements, aux redémarrages, et au fait que votre application tourne comme un conteneur à usage unique, pas comme une machine Linux complète où cron tourne déjà.
Ce guide met en place un conteneur planificateur dédié, ofelia, qui lit des labels Docker sur vos conteneurs existants et exécute des commandes contre eux selon un horaire. À la fin, vous aurez un docker-compose.yml fonctionnel qui exécute une commande dans votre conteneur d’application selon un horaire cron, avec des logs consultables et une façon sûre de gérer une tâche qui échoue.
Ce qu’il vous faut
- Un serveur avec Docker et le plugin Docker Compose installés. Un Hetzner CX23 (2 vCPU, 4 Go de RAM, 40 Go NVMe, 5,99 € par mois hors TVA) suffit pour faire tourner une petite application avec le planificateur.
- Votre application déjà en cours d’exécution comme conteneur, idéalement via
docker-compose.yml. - Aucun domaine n’est nécessaire pour le planificateur lui-même. Si votre application sert du HTTP, la configuration de proxy inverse et de HTTPS que vous avez déjà pour elle n’est affectée par rien dans ce guide.
- Un accès SSH au serveur.
Étape 1 : comprendre pourquoi cron ne fonctionne pas tout seul ici
Une image Docker exécute normalement un seul processus, votre application, comme PID 1. Rien ne surveille une crontab, parce qu’il n’y a pas de crontab. Il existe trois façons honnêtes de régler ça :
- Intégrer
cron(oucrondd’Alpine) directement dans l’image de votre application et l’exécuter aux côtés de votre processus avec quelque chose commesupervisord. Ça fonctionne, mais maintenant votre image d’application possède la planification, et chaque tâche sans lien nécessite un changement de code et une reconstruction pour être ajustée. - Exécuter un conteneur séparé qui, selon un horaire, exécute une commande à l’intérieur de votre conteneur d’application en cours d’exécution. Aucun changement à l’image de votre application.
- Exécuter un conteneur séparé qui, selon un horaire, démarre un nouveau conteneur éphémère à partir d’une image, exécute une commande ponctuelle, puis jette le conteneur.
Les options 2 et 3 sont toutes deux gérées par le même outil : ofelia, un petit planificateur en Go construit spécifiquement pour Docker. Il surveille le socket Docker, lit les définitions de tâches depuis des labels sur vos conteneurs (ou depuis un fichier de config), et les exécute selon un horaire cron. Ce guide utilise l’option 2, job-exec, puisque c’est le cas courant : vous avez déjà un conteneur d’application en cours d’exécution et vous voulez qu’une commande de gestion y tourne périodiquement.
Étape 2 : étiqueter le conteneur qui doit exécuter la tâche
Les tâches job-exec d’ofelia sont déclarées comme labels sur le conteneur contre lequel vous voulez exécuter des commandes. Voici un service d’application avec une tâche de nettoyage nocturne attachée :
services:
web:
image: ghcr.io/votrecompte/votreapp:latest
restart: unless-stopped
ports:
- "3000:3000"
labels:
ofelia.enabled: "true"
ofelia.job-exec.nightly-cleanup.schedule: "30 3 * * *"
ofelia.job-exec.nightly-cleanup.command: "bin/cleanup"
ofelia.enabled: "true" indique au planificateur que ce conteneur est autorisé à avoir des tâches exécutées contre lui, donc un conteneur que vous avez oublié d’étiqueter n’est jamais pris en compte silencieusement. Le format du label est ofelia.<type-de-tâche>.<nom-de-tâche>.<paramètre>, donc nightly-cleanup ici est juste un nom que vous avez choisi, il n’a aucune signification particulière pour ofelia. L’horaire 30 3 * * * est une expression cron standard à 5 champs (minute, heure, jour du mois, mois, jour de la semaine), lue chaque nuit à 3 h 30. command est ce que vous taperiez normalement à l’intérieur du conteneur, ici un script de gestion que votre image fournit déjà.
Étape 3 : ajouter le service planificateur
Ajoutez ofelia lui-même comme deuxième service dans le même docker-compose.yml :
services:
web:
image: ghcr.io/votrecompte/votreapp:latest
restart: unless-stopped
ports:
- "3000:3000"
labels:
ofelia.enabled: "true"
ofelia.job-exec.nightly-cleanup.schedule: "30 3 * * *"
ofelia.job-exec.nightly-cleanup.command: "bin/cleanup"
scheduler:
image: mcuadros/ofelia:a573727
restart: unless-stopped
depends_on:
- web
command: daemon --docker
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
mcuadros/ofelia ne publie actuellement pas de tags de version sémantique sur Docker Hub, seulement latest, development, et des tags nommés d’après de courts hash de commit git. a573727 est un de ces tags de hash et est confirmé présent sur Docker Hub aujourd’hui ; épinglez le tag de hash actuel que vous y trouvez plutôt que de flotter sur latest, pour qu’une mise à jour du planificateur soit quelque chose que vous choisissez, pas quelque chose qui arrive lors d’un redémarrage de routine. La commande daemon --docker dit à ofelia de surveiller l’API Docker pour des conteneurs étiquetés au lieu de lire un fichier INI statique.
Étape 4 : monter le socket Docker, et savoir ce que ça accorde
La ligne /var/run/docker.sock:/var/run/docker.sock:ro est ce qui permet à ofelia de voir vos conteneurs et d’y exécuter des commandes. C’est aussi, fonctionnellement, un accès root sur l’hôte : un processus avec accès au socket Docker peut démarrer un nouveau conteneur avec le système de fichiers de l’hôte monté dedans, ce qui est un accès root sous un autre nom. Le drapeau :ro empêche ofelia d’écrire sur le fichier socket lui-même, mais ne restreint pas les appels API Docker qu’il peut faire via ce socket, donc ce n’est pas un bac à sable.
Gardez ce conteneur loin de toute image que vous ne contrôlez pas, n’exposez le conteneur ofelia à rien d’autre sur le réseau, et traitez le serveur sur lequel il tourne comme une seule frontière de confiance, pas une machine multi-locataire. Si ce compromis ne vous convient pas, les tâches job-run (qui lancent un conteneur jetable à partir d’une image spécifiée plutôt que d’exécuter dans un conteneur de longue durée) réduisent un peu le rayon d’impact, puisque ofelia n’a besoin que de la permission d’exécuter cette seule image, pas d’atteindre des conteneurs arbitraires en cours d’exécution.
Étape 5 : déployer et vérifier la première exécution
Démarrez la pile :
docker compose up -d
Ofelia journalise chaque tâche qu’il exécute, y compris celles qui échouent, dans le stdout de son propre conteneur :
docker compose logs -f scheduler
Vous devriez voir une ligne comme job "nightly-cleanup" started la première fois que l’horaire se déclenche, suivie de job "nightly-cleanup" finished avec le code de sortie. Si vous ne voulez pas attendre 3 h 30, ajoutez temporairement un second label avec un horaire dans quelques minutes, confirmez qu’il s’exécute, puis retirez le label de test et redéployez.
Étape 6 : gérer les échecs sans perdre la sortie
Une tâche qui échoue ne redémarre pas votre conteneur et n’alerte personne de lui-même. Le code de sortie et la sortie de la commande se trouvent dans docker compose logs scheduler, et c’est tout à moins de brancher autre chose. Ofelia supporte une section de config [global] pour les notifications par mail, Slack, ou journal sauvegardé, mais ça s’applique seulement quand ofelia tourne contre un fichier de config plutôt que de purs labels Docker, puisque les labels n’ont nulle part où mettre des identifiants SMTP. Si vous avez besoin d’alertes, l’option pratique est de faire en sorte que la commande de votre tâche rapporte elle-même l’échec (un appel webhook Slack, une erreur vers votre monitoring existant) plutôt que de compter sur le planificateur pour le remarquer.
Pour les tâches où une exécution manquée compte, gardez la commande elle-même idempotente, pour que si une exécution échoue à mi-chemin, l’exécution planifiée suivante puisse simplement réessayer plutôt que de nécessiter une intervention humaine.
Étape 7 : mettre à jour le planificateur et sauvegarder sa configuration
Ofelia lui-même ne garde aucun état propre au-delà de ce qui est dans vos labels ou votre fichier de config, donc il n’y a rien à sauvegarder côté planificateur au-delà de ce que vous gardez déjà dans le contrôle de version pour docker-compose.yml. La mise à jour consiste à récupérer un tag de hash plus récent et à recréer ce seul service :
docker compose pull scheduler
docker compose up -d scheduler
Votre conteneur d’application continue de tourner sans être affecté, puisque docker compose up -d ne touche que les services dont l’image ou la config a vraiment changé.
Le raccourci : faites-le avec Railyard
Si vous déployez l’application elle-même sur Railyard plutôt que de bricoler Compose à la main, le problème de planification disparaît comme élément d’infrastructure séparé. La fonctionnalité Cron de Railyard exécute une commande dans l’image de votre propre application selon un horaire que vous définissez, avec un historique d’exécution conservé par tâche, donc 30 3 * * * devient une ligne dans le tableau de bord au lieu d’un label et d’un second conteneur. Il n’y a aucun socket Docker à monter et aucune seconde image à garder à jour, parce que la commande s’exécute avec la même image que votre application déploie déjà.
Le même onglet Workers récupère aussi tout processus en arrière-plan que votre dépôt déclare déjà, une ligne worker ou clock dans un Procfile, ou un exécuteur comme Sidekiq, Solid Queue, GoodJob, ou un worker de queue Laravel trouvé dans vos dépendances, et vous permet de l’ajouter comme son propre processus mis à l’échelle indépendamment en un clic. Voir Workers et tâches planifiées pour la vue d’ensemble des deux, et Déployer depuis un dépôt pour le fonctionnement du déploiement initial lui-même.
Dépannage
ofelia.enabled est défini mais la tâche ne s’exécute jamais. Vérifiez que le label est sur le conteneur cible (celui dans lequel vous voulez que la commande s’exécute), pas seulement sur le planificateur. Pour job-exec, la paire de labels vit sur le conteneur de l’application ; seuls job-local et les entrées de config globale vivent sur le planificateur lui-même.
Le conteneur planificateur n’arrive pas à atteindre le socket Docker. Confirmez que le chemin du montage de volume correspond à l’emplacement réel du socket de votre hôte, /var/run/docker.sock est standard sur la plupart des distributions Linux, mais une configuration Docker Desktop ou Docker en mode rootless le place parfois ailleurs. docker compose logs scheduler affichera une erreur de permission ou de connexion au démarrage si c’est le cas.
La syntaxe cron semble correcte mais la tâche se déclenche au mauvais moment. Ofelia tourne sur l’horloge propre du conteneur en UTC par défaut à moins que vous ayez défini TZ sur le conteneur planificateur ; 30 3 * * * signifie 3 h 30 UTC, pas 3 h 30 dans votre fuseau horaire local, à moins d’avoir explicitement configuré autrement.
Une tâche qui touche des fichiers échoue avec une erreur de permissions. Ça signifie généralement que la commande essaie d’écrire quelque part où l’utilisateur du conteneur de l’application lui-même ne peut pas, indépendamment d’ofelia. Testez la même commande avec docker compose exec web <commande> directement pour isoler les problèmes de planificateur des problèmes de permissions.
Le serveur manque de mémoire après l’ajout du planificateur. Ofelia lui-même est petit (environ 12 Mo compressé), donc c’est presque toujours la commande de la tâche elle-même, pas le planificateur, qui consomme de la mémoire. Vérifiez l’utilisation des ressources de la tâche de la même manière que vous vérifieriez n’importe quel autre processus sur la machine.
FAQ
Ai-je besoin de cron installé quelque part sur l’hôte ? Non. Ofelia remplace entièrement la crontab de l’hôte ; rien dans cette configuration ne touche /etc/crontab ou les timers systemd.
Puis-je planifier une tâche qui n’a pas besoin d’un conteneur en cours d’exécution existant ? Oui, c’est job-run : il démarre un nouveau conteneur à partir d’une image que vous spécifiez, exécute la commande, et retire le conteneur après, ce qui se rapproche davantage d’une tâche batch traditionnelle « démarrer, faire le travail, arrêter ».
Que se passe-t-il si deux exécutions planifiées se chevauchent, parce que l’une finit encore quand la suivante démarre ? Ofelia ne sérialise pas les exécutions job-exec contre le même conteneur par défaut, donc une tâche longue et un intervalle court peuvent se chevaucher. Gardez la commande soit rapide par rapport à son intervalle, soit sûre à exécuter en même temps qu’elle-même.
Est-ce que ça fonctionne de la même façon sur Docker Swarm ou Kubernetes ? Ofelia supporte aussi job-service-run pour les services Swarm. Sur Kubernetes, une ressource CronJob est l’équivalent natif et n’a pas besoin d’ofelia du tout.
Y a-t-il une interface web pour voir l’historique des tâches ? Pas dans l’image open source seule ; l’historique des tâches vit dans la sortie de log et la rétention que vous configurez pour le conteneur scheduler.
Un conteneur planificateur et quelques labels, c’est peu d’infrastructure pour ce qu’une tâche cron demande, et c’est de l’infrastructure que vous devez maintenant corriger, surveiller, et expliquer à la prochaine personne qui ouvre ce dépôt. Ce compromis en vaut la peine quand vous faites déjà tourner votre propre pile Compose à la main. Quand ce n’est pas le cas, la tâche elle-même reste exactement aussi portable, puisque c’est juste une commande que votre image peut déjà exécuter.
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.