← Blogue

Exécuter des workers Sidekiq ou Celery à côté de votre application web

Railyard

7 août 2026 · 11 min de lecture

Suivez ça sur votre propre serveur. Commencer gratuitement, sans carte

Dès qu’une application doit envoyer un courriel, redimensionner une image ou appeler une API tierce lente en effet secondaire d’une requête, ce travail doit se produire ailleurs que dans la requête elle-même. Sidekiq pour Ruby et Celery pour Python résolvent tous deux le même problème : une file d’attente contient la tâche, et un processus worker séparé en retire les tâches et les exécute, de sorte qu’un fournisseur de courriel lent ne ralentit jamais le chargement d’une page.

Ce guide met en place un processus worker à côté d’une application web existante : Sidekiq avec Redis comme backend de file d’attente pour une application Ruby, ou Celery avec Redis comme broker pour une application Python, tous deux exécutables avec Docker Compose ou comme simple service systemd.

Ce dont vous aurez besoin

  • Un serveur qui exécute déjà votre application web, ou un nouveau serveur. Un Hetzner CX23 (2 vCPU, 4 Go de RAM, 40 Go NVMe, 5,99 €/mois hors TVA) offre assez de marge pour exécuter ensemble un processus web, un processus worker et Redis pour une application petite à moyenne.
  • Redis. Si vous n’avez pas encore d’instance, l’image officielle redis:8-alpine dans Compose est le moyen le plus rapide d’en obtenir une ; l’étape 1 montre comment faire.
  • Le code de votre application déjà structuré pour que les tâches soient définies quelque part où le processus worker peut les importer : un répertoire app/jobs ou app/sidekiq en termes Rails/Sidekiq, un fichier tasks.py avec des décorateurs @app.task en termes Celery.

Étape 1 : ajouter Redis comme file d’attente partagée

Sidekiq et Celery ont tous deux besoin d’un broker pour contenir les tâches entre leur mise en file et leur exécution. Redis est le choix courant pour les deux (Celery prend aussi en charge RabbitMQ, mais Redis couvre la grande majorité des configurations et permet de garder ce guide à une seule pièce mobile) :

services:
  redis:
    image: redis:8-alpine
    volumes:
      - redis-data:/data
    restart: unless-stopped

volumes:
  redis-data:

Si cette instance Redis sert uniquement de file d’attente de tâches et que rien d’autre n’y lit ni n’y écrit, vous n’avez pas besoin des options de réglage de cache d’une configuration Redis polyvalente, les réglages par défaut conviennent ; assurez-vous simplement que le volume est présent pour que les tâches mises en file mais pas encore exécutées survivent à un redémarrage au lieu de disparaître.

Étape 2 : configurer Sidekiq (Ruby)

Ajoutez la gem. Ce guide utilise Sidekiq 8.1 (8.1.7 est la version actuelle sur RubyGems) :

gem "sidekiq", "~> 8.1"
bundle install

Définissez une tâche :

class WelcomeEmailJob
  include Sidekiq::Job

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

Pointez Sidekiq vers l’instance Redis de l’étape 1 et définissez une concurrence raisonnable pour la taille du serveur. Chaque thread Sidekiq détient sa propre connexion à la base de données, donc la concurrence doit correspondre au pool de connexions de votre base de données, pas seulement au CPU disponible :

# config/sidekiq.yml
:concurrency: 5
:queues:
  - default
  - mailers
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: ENV.fetch("REDIS_URL") }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV.fetch("REDIS_URL") }
end

Mettez la tâche en file depuis n’importe où dans votre application :

WelcomeEmailJob.perform_async(user.id)

Étape 3 : configurer Celery (Python)

Ajoutez la dépendance. Ce guide utilise Celery 5.6 (5.6.3 est la version actuelle sur PyPI) ; l’extra redis installe le client Redis dont Celery a besoin :

pip install "celery[redis]==5.6.3"

Définissez l’application Celery et une tâche dans tasks.py, la structure utilisée par le propre guide de premiers pas de Celery :

import os
from celery import Celery

app = Celery("tasks", broker=os.environ["REDIS_URL"])

@app.task
def send_welcome_email(user_id):
    print(f"sending welcome email to user {user_id}")

Remplacez le corps par votre vrai traitement. Appelez la tâche depuis le code de votre application web avec .delay(), qui la met en file et retourne immédiatement :

from tasks import send_welcome_email

send_welcome_email.delay(user.id)

Dans un projet Django, le guide « First steps with Django » de Celery place l’application dans proj/celery.py et charge d’abord vos paramètres Django, afin que les tâches puissent utiliser l’ORM ; la commande du worker devient alors celery -A proj worker au lieu de celery -A tasks worker.

Étape 4 : exécuter le worker comme processus séparé

Quelle que soit la bibliothèque utilisée, le worker est un processus de longue durée distinct de votre serveur web, pas un thread à l’intérieur de celui-ci. Avec Docker Compose, cela signifie un second service construit à partir de la même image que votre application web, qui ne diffère que par sa commande :

services:
  web:
    build: .
    command: bundle exec puma -C config/puma.rb
    env_file: .env
    ports:
      - "3000:3000"
    depends_on:
      - redis
    restart: unless-stopped

  worker:
    build: .
    command: bundle exec sidekiq -C config/sidekiq.yml -c 5
    env_file: .env
    stop_grace_period: 30s
    depends_on:
      - redis
    restart: unless-stopped

  redis:
    image: redis:8-alpine
    volumes:
      - redis-data:/data
    restart: unless-stopped

volumes:
  redis-data:

L’équivalent pour Celery change simplement les commandes web et worker :

services:
  web:
    build: .
    command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
    env_file: .env
    ports:
      - "8000:8000"
    depends_on:
      - redis
    restart: unless-stopped

  worker:
    build: .
    command: celery -A tasks worker --loglevel=INFO --concurrency=4
    env_file: .env
    depends_on:
      - redis
    restart: unless-stopped

  redis:
    image: redis:8-alpine
    volumes:
      - redis-data:/data
    restart: unless-stopped

volumes:
  redis-data:

stop_grace_period: 30s sur le worker Sidekiq compte à chaque déploiement. Docker envoie TERM à l’arrêt et tue le conteneur 10 secondes plus tard par défaut, alors que Sidekiq attend jusqu’à 25 secondes par défaut pour les tâches en cours avant de remettre les tâches inachevées dans Redis. Sans cette période de grâce plus longue, Docker tue Sidekiq avant qu’il ne puisse remettre ces tâches en file.

Dans les deux cas, le fichier .env à côté du fichier Compose définit REDIS_URL=redis://redis:6379/0, de sorte que le processus web et le worker pointent vers le même service Redis par son nom Compose.

Démarrez tout :

docker compose up -d
docker compose ps

Vous devriez voir les services web, worker et redis tous avec un statut Up.

Déclenchez une tâche depuis votre application et confirmez qu’elle s’est exécutée en vérifiant les journaux du worker :

docker compose logs -f worker

Vous devriez voir le nom de la tâche et une ligne de complétion (Sidekiq journalise done: avec un temps d’exécution ; Celery journalise Task ... succeeded).

Étape 5 : l’alternative systemd, sans Docker

Si votre application s’exécute directement sur l’hôte plutôt que dans des conteneurs, une unité systemd vous donne le même comportement « s’exécute à côté du processus web, redémarre en cas de plantage » sans Compose.

# /etc/systemd/system/myapp-sidekiq.service
[Unit]
Description=Sidekiq worker for myapp
After=network.target redis-server.service

[Service]
Type=notify
WatchdogSec=10
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env
ExecStart=/usr/local/bin/bundle exec sidekiq -C config/sidekiq.yml
User=deploy
Restart=always
RestartSec=1

[Install]
WantedBy=multi-user.target

Type=notify avec WatchdogSec suit l’exemple de fichier d’unité du propre dépôt de Sidekiq, qui fonctionne à partir de Sidekiq 6.0.6 : Sidekiq indique à systemd quand il est prêt et continue de lui envoyer des pings, et systemd redémarre le processus en quelques secondes si ces pings s’arrêtent.

Celery n’envoie pas ces notifications, il lui faut donc sa propre unité avec Type=simple et sans watchdog. Une unité notify resterait à attendre un signal de disponibilité qui ne vient jamais, et systemd finirait par la tuer :

# /etc/systemd/system/myapp-celery.service
[Unit]
Description=Celery worker for myapp
After=network.target redis-server.service

[Service]
Type=simple
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env
ExecStart=/opt/myapp/.venv/bin/celery -A tasks worker --loglevel=INFO --concurrency=4
User=deploy
Restart=always
RestartSec=1

[Install]
WantedBy=multi-user.target

Activez et démarrez celle que vous utilisez :

sudo systemctl daemon-reload
sudo systemctl enable --now myapp-sidekiq
sudo systemctl status myapp-sidekiq

Pour Celery, utilisez myapp-celery dans les trois mêmes commandes.

Vous devriez voir active (running), et Restart=always signifie qu’un worker planté revient de lui-même au lieu de rester silencieusement à l’arrêt jusqu’à ce que quelqu’un remarque que les tâches ont cessé d’être traitées.

Le raccourci : faites-le avec Railyard

Tout ce qui se trouve dans les étapes 4 et 5 (un second processus à partir de la même image, sa propre politique de redémarrage, sa propre mise à l’échelle) est ce que met en place directement l’onglet Workers de Railyard. Il lit votre dépôt à la recherche des exécuteurs de tâches qu’il reconnaît, Sidekiq, Solid Queue, GoodJob, Resque, Celery, RQ, le worker de file d’attente ou Horizon de Laravel, un script npm run worker, ou tout ce qui est déclaré comme processus dans votre Procfile, Procfile.dev ou app.json, et propose chacun comme worker que vous ajoutez en un clic. Chaque worker exécute l’image propre à votre application et se met à l’échelle indépendamment de votre processus web, de sorte qu’un pic de trafic sur le site web ne signifie pas forcément payer plus de capacité worker aussi, ou l’inverse.

Une application uniquement worker, sans aucun processus web, peut aussi être réglée pour se mettre en veille quand elle est inactive sous Settings → General, et Railyard la réveille selon son propre horaire pour vider tout ce qui est en file avant de la laisser repasser en veille, ce qui compte si la file est vraiment calme la plupart du temps et que vous préférez ne pas payer pour un processus qui reste inactif en permanence.

Dépannage

Le worker démarre mais ne récupère jamais aucune tâche. Presque toujours un décalage de REDIS_URL entre le processus qui met les tâches en file et le processus qui exécute le worker, souvent parce que l’un le lit depuis un fichier .env et l’autre depuis un fichier différent, ou une valeur périmée figée dans une image au moment du build plutôt que lue au moment de l’exécution. Confirmez que les deux processus pointent vers la même instance Redis et le même numéro de base de données logique, puis vérifiez que la file contient bien quelque chose (redis-cli -u $REDIS_URL llen queue:default pour le nommage de file par défaut de Sidekiq, ou l’équivalent selon la façon dont vos files sont nommées).

Les tâches restent dans la file et ne s’exécutent jamais, même si le worker est en bonne santé. Vérifiez que les noms de file correspondent réellement. Les workers Sidekiq ne tirent que des files listées dans leur propre configuration :queues ; une tâche mise en file dans une file que le worker n’a jamais été configuré pour écouter y reste indéfiniment sans aucune erreur d’un côté ni de l’autre. Celery a le même mode de défaillance avec ses clés de routage si vous avez configuré des files personnalisées plutôt que d’utiliser celle par défaut.

La mémoire grimpe jusqu’à ce que le serveur tue le worker. Une concurrence réglée plus haut que ce que le serveur peut réellement supporter est la cause la plus courante ; chaque thread ou processus détient sa propre mémoire (et, pour Sidekiq, sa propre connexion à la base de données), donc une concurrence réglée pour un serveur plus gros que celui que vous utilisez réellement finira par faire tuer le processus par la gestion du manque de mémoire du noyau. Baissez d’abord la concurrence et observez si le problème revient avant de supposer qu’il s’agit d’une fuite dans le code de vos tâches.

Le processus worker se termine immédiatement sans erreur claire. Généralement une variable d’environnement manquante dont le worker a besoin mais pas le processus web, car le chemin de code d’un worker à travers les classes de tâches touche souvent des parties différentes de la configuration de votre application. Exécutez la commande exacte du worker à la main dans un shell avec le même fichier d’environnement pour voir la véritable exception plutôt que ce que votre gestionnaire de processus en a résumé.

Deux copies d’une même tâche se sont exécutées d’une manière ou d’une autre. Aucun des deux outils ne garantit une exécution exactement une fois. Une tâche qui lève une erreur est retentée, et avec le réglage acks_late de Celery activé, une tâche interrompue en cours d’exécution est livrée à nouveau, donc une tâche ayant déjà effectué une partie de son travail peut la répéter. Écrivez les tâches de façon à ce que les exécuter deux fois soit sans danger (vérifiez si le courriel a déjà été envoyé, utilisez des upserts plutôt que des insertions), et cela cesse d’être un problème peu importe le nombre de workers que vous exécutez.

FAQ

Dois-je utiliser Sidekiq ou Celery ? Celui qui correspond au langage de votre framework web. Il n’y a pas de différence de fiabilité significative qui devrait pousser une application Ruby vers Celery ou une application Python vers Sidekiq ; les deux sont matures, les deux utilisent Redis par défaut, et les deux prennent en charge les réessais et les tâches planifiées.

Celery a-t-il besoin de RabbitMQ au lieu de Redis ? Non. Celery prend en charge les deux comme broker, et Redis est plus simple à exploiter si vous n’exécutez pas déjà RabbitMQ pour autre chose. RabbitMQ offre des garanties de livraison plus fortes dans certains cas limites, ce qui compte davantage à une échelle que la plupart des petites applications n’atteignent pas.

De combien de processus worker ai-je besoin ? Commencez avec un seul processus à une concurrence modeste (5 threads pour Sidekiq, 4 pour Celery, comme montré plus haut) et observez si les tâches s’accumulent dans la file sous une charge réelle avant d’en ajouter d’autres. Un second processus worker est plus simple à raisonner qu’augmenter la concurrence d’un seul processus au-delà de ce que le CPU du serveur et le pool de connexions de votre base de données peuvent réellement supporter.

Qu’arrive-t-il à une tâche si le worker plante en cours d’exécution ? Cela dépend de la façon dont le worker s’est arrêté. Lors d’un arrêt normal (systemctl stop, docker compose stop, un déploiement), Sidekiq attend les tâches en cours jusqu’à son délai d’arrêt et remet les tâches inachevées dans Redis. Si le processus est tué sans préavis (un kill pour manque de mémoire, kill -9, une coupure de courant), Sidekiq open source perd les tâches qu’il était en train d’exécuter, car il retire une tâche de Redis dès qu’il commence à la traiter ; le super_fetch de Sidekiq Pro existe pour combler cet écart. Celery accuse réception d’une tâche avant de l’exécuter par défaut, donc un plantage brutal perd aussi cette tâche ; régler task_acks_late = True fait que Celery accuse réception après que la tâche soit terminée, de sorte qu’une tâche interrompue est livrée à nouveau à la place. Si perdre une tâche occasionnelle compte pour vous, gardez les tâches idempotentes et activez soit acks_late (Celery), soit enregistrez le travail important quelque part de durable avant de le mettre en file.

Ai-je besoin d’une instance Redis séparée de mon cache ? Pas nécessairement, mais si vous en partagez une, gardez la file sur son propre numéro de base de données logique ou préfixe de clé, et soyez prudent avec toute maxmemory-policy qui évince des clés sous pression, car une politique d’éviction de cache appliquée à une file peut faire disparaître silencieusement une tâche qui n’a en réalité jamais été traitée.

Une fois que la file et le worker communiquent avec la même instance Redis et des noms de file qui correspondent, le reste consiste surtout à dimensionner la concurrence selon ce que le serveur et la base de données peuvent réellement supporter, pas à deviner un chiffre plus élevé parce qu’il semble plus rapide.

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.

Ajoutez un worker en un clic

Railyard lit les dépendances de votre dépôt et propose Sidekiq ou Celery comme worker qu'il peut exécuter à côté de votre processus web, mis à l'échelle indépendamment.

Offre gratuite, sans carte, et 14 jours de Pro pour chaque nouvelle équipe. Prix fixe par équipe.