Technique · Mis à jour le

Docker, PHP, Symfony et les Crons sont dans un bateau

Comment lancer des tâches planifiées (crons) quand votre application Symfony tourne dans Docker ? Il existe plusieurs façons de gérer des crons avec Docker. Voici celle que nous utilisons : lancer deux fois la même image, avec deux rôles différents.

Le principe

La méthode la plus courante consiste à utiliser la crontab du serveur hôte. Elle déclenche alors des commandes à l’intérieur du container. Ça fonctionne, mais la planification vit en dehors de votre projet.

Nous préférons une autre approche :

  • on construit une seule image, qui contient l’application et le programme cron ;
  • on la lance deux fois : un container sert le site web, l’autre exécute uniquement les crons.

Ainsi, la planification est versionnée avec le code, et les deux containers ont exactement le même environnement.

Étape 1 : écrire le fichier cron

On crée un fichier crontab à la racine du projet, avec les commandes à lancer :

* * * * * cd /app && /usr/local/bin/php bin/console app:ma-commande > /proc/1/fd/1 2>&1

Deux détails importants :

  • La redirection vers /proc/1/fd/1 envoie la sortie de la commande dans les logs du container. Vous la retrouvez avec docker compose logs. Une redirection vers /dev/null, elle, ferait disparaître toutes les erreurs.
  • Le fichier doit se terminer par une ligne vide, sinon cron ignore la dernière ligne.

Étape 2 : installer cron dans l’image

Dans le Dockerfile, on installe le démon cron et on enregistre notre fichier :

RUN apt-get update \
    && apt-get install -y --no-install-recommends cron \
    && rm -rf /var/lib/apt/lists/*

COPY crontab /etc/app-crontab
RUN chmod 0644 /etc/app-crontab && crontab /etc/app-crontab

On obtient une image Symfony avec tout l’environnement d’exécution (PHP, serveur web), plus un cron prêt à démarrer.

Étape 3 : déclarer deux services dans Docker Compose

Dans le fichier compose.yaml, on déclare deux services basés sur la même image : php pour le HTTP, crond pour les tâches planifiées.

volumes:
  shared-tmpfs:
    driver: local
    driver_opts:
      type: "tmpfs"
      device: "tmpfs"
      o: "size=256m"

services:
  php:
    image: XXX
    pull_policy: always
    restart: always
    ports:
        - target: 80
          published: 80
          protocol: tcp
    environment:
      SERVER_NAME: :80

    volumes:
      - shared-tmpfs:/app/var
      - "/etc/timezone:/etc/timezone:ro"
      - "/etc/localtime:/etc/localtime:ro"

  crond:
    image: XXX
    pull_policy: always
    restart: always
    command: cron -f
    healthcheck:
       test: ['CMD','true']
    volumes:
      - shared-tmpfs:/app/var
      - "/etc/timezone:/etc/timezone:ro"
      - "/etc/localtime:/etc/localtime:ro"

Ce qu’il faut regarder :

  • command: cron -f : le service crond remplace la commande de démarrage par cron, lancé au premier plan. Le service php, lui, garde son point d’entrée habituel.
  • Le healthcheck neutralisé : l’image vérifie normalement que le serveur web répond. Le container cron n’a pas de serveur web, on désactive donc ce contrôle.
  • Les volumes timezone et localtime : ils alignent le fuseau horaire sur celui du serveur. Sans eux, vos crons tournent à l’heure UTC.

Partager des données entre les deux containers

Notre application utilise une base SQLite comme cache un peu évolué. Les deux containers doivent donc voir la même base.

Nous avons choisi un système de fichiers en mémoire vive (tmpfs), partagé entre les deux services via le volume shared-tmpfs. La base est créée automatiquement au démarrage du container php.

Attention aux variables d’environnement

C’est le piège classique. Cron lance les tâches avec un environnement quasi vide. Les variables définies dans compose.yaml (comme DATABASE_URL) ne sont donc pas transmises à vos commandes.

Si votre configuration est dans les fichiers .env de Symfony embarqués dans l’image, pas de souci : Symfony les lit lui-même. Mais si elle passe par Docker, deux solutions :

  • exporter les variables au démarrage du container, par exemple avec printenv > /etc/environment avant de lancer cron ;
  • remplacer cron par Supercronic, un cron pensé pour les containers. Il transmet les variables d’environnement, écrit ses logs sur la sortie standard et lit directement un fichier crontab.

Une alternative 100 % Symfony : le composant Scheduler

Depuis Symfony 6.3, le composant Scheduler permet de déclarer les tâches planifiées directement dans le code PHP. Plus besoin de cron : le second container lance simplement un worker Messenger.

  scheduler:
    image: XXX
    restart: always
    command: php bin/console messenger:consume scheduler_default

C’est une bonne option si vous utilisez déjà Messenger. La planification reste alors au même endroit que votre code métier.

À lire aussi

Articles sur le même sujet