← Blog

Sidekiq- oder Celery-Worker neben deiner Web-App betreiben

Railyard

7. August 2026 · 9 Min. Lesezeit

Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig

In dem Moment, in dem eine App eine E-Mail senden, ein Bild verkleinern oder als Nebeneffekt eines Requests eine langsame Drittanbieter-API aufrufen muss, muss diese Arbeit irgendwo passieren, das nicht der Request selbst ist. Sidekiq für Ruby und Celery für Python lösen dasselbe Problem: Eine Queue hält den Job, und ein separater Worker-Prozess holt Jobs daraus und führt sie aus, sodass ein langsamer E-Mail-Anbieter nie dafür sorgt, dass sich ein Seitenaufruf langsam anfühlt.

Diese Anleitung richtet einen Worker-Prozess neben einer bestehenden Web-App ein: Sidekiq mit Redis als Queue-Backend für eine Ruby-App, oder Celery mit Redis als Broker für eine Python-App, beide lauffähig mit Docker Compose oder als einfacher systemd-Dienst.

Was du brauchst

  • Einen Server, auf dem bereits deine Web-App läuft, oder einen frischen. Ein Hetzner CX23 (2 vCPU, 4 GB RAM, 40 GB NVMe, 5,99 €/Monat exkl. MwSt.) hat genug Spielraum, um einen Web-Prozess, einen Worker-Prozess und Redis zusammen für eine kleine bis mittlere App zu betreiben.
  • Redis. Falls du noch keine Instanz hast, ist das offizielle redis:8-alpine-Image in Compose der schnellste Weg, eine zu bekommen; Schritt 1 zeigt wie.
  • Den Code deiner App bereits so strukturiert, dass Jobs irgendwo definiert sind, wo der Worker-Prozess sie importieren kann: ein app/jobs- oder app/sidekiq-Verzeichnis in Rails/Sidekiq-Begriffen, eine tasks.py mit @app.task-Dekoratoren in Celery-Begriffen.

Schritt 1: Redis als gemeinsame Queue hinzufügen

Sowohl Sidekiq als auch Celery brauchen einen Broker, der Jobs zwischen Einreihen und Ausführung hält. Redis ist für beide die gängige Wahl (Celery unterstützt auch RabbitMQ, aber Redis deckt die große Mehrheit der Setups ab und hält diese Anleitung bei einem beweglichen Teil):

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

volumes:
  redis-data:

Wenn diese Redis-Instanz reine eine Job-Queue ist und sonst niemand davon liest oder darauf schreibt, brauchst du die Cache-Tuning-Flags eines allgemeinen Redis-Setups nicht, die Standardeinstellungen sind in Ordnung; stelle nur sicher, dass das Volume vorhanden ist, damit eingereihte, aber noch nicht gelaufene Jobs einen Neustart überleben, statt zu verschwinden.

Schritt 2: Sidekiq einrichten (Ruby)

Füge das Gem hinzu. Diese Anleitung verwendet Sidekiq 8.1 (8.1.7 ist die aktuelle Version auf RubyGems):

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

Definiere einen Job:

class WelcomeEmailJob
  include Sidekiq::Job

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

Richte Sidekiq auf die Redis-Instanz aus Schritt 1 und setze eine sinnvolle Concurrency für die Servergröße. Jeder Sidekiq-Thread hält seine eigene Datenbankverbindung, daher muss die Concurrency zu deinem Datenbank-Connection-Pool passen, nicht nur zu der freien CPU, die du hast:

# 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

Reihe den Job von irgendwo in deiner App ein:

WelcomeEmailJob.perform_async(user.id)

Schritt 3: Celery einrichten (Python)

Füge die Abhängigkeit hinzu. Diese Anleitung verwendet Celery 5.6 (5.6.3 ist die aktuelle Version auf PyPI); das redis-Extra installiert den Redis-Client, den Celery braucht:

pip install "celery[redis]==5.6.3"

Definiere die Celery-App und einen Task in tasks.py, das Layout, das Celerys eigene Erste-Schritte-Anleitung verwendet:

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}")

Ersetze den Rumpf mit deiner echten Arbeit. Rufe den Task aus deinem Web-Code mit .delay() auf, was ihn in die Queue legt und sofort zurückkehrt:

from tasks import send_welcome_email

send_welcome_email.delay(user.id)

In einem Django-Projekt legt Celerys “First steps with Django”-Anleitung die App in proj/celery.py ab und lädt zuerst deine Django-Settings, damit Tasks das ORM nutzen können; der Worker-Befehl wird dann zu celery -A proj worker statt celery -A tasks worker.

Schritt 4: Den Worker als eigenen Prozess betreiben

Egal welche Bibliothek du verwendest, der Worker ist ein separater, langlaufender Prozess, getrennt vom Web-Server, kein Thread darin. Mit Docker Compose bedeutet das einen zweiten Service, aus demselben Image gebaut wie deine Web-App, der sich nur im Befehl unterscheidet:

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:

Das Celery-Äquivalent tauscht die web- und worker-Befehle:

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 am Sidekiq-Worker ist bei jedem Deploy wichtig. Docker sendet TERM beim Stoppen und tötet den Container standardmäßig 10 Sekunden später, während Sidekiq standardmäßig bis zu 25 Sekunden auf laufende Jobs wartet, bevor es unfertige zurück nach Redis schiebt. Ohne die längere Gnadenfrist tötet Docker Sidekiq, bevor es diese Jobs zurücklegen kann.

In beiden Fällen setzt die .env-Datei neben der Compose-Datei REDIS_URL=redis://redis:6379/0, sodass der Web-Prozess und der Worker auf denselben Redis-Service über dessen Compose-Namen zeigen.

Alles hochfahren:

docker compose up -d
docker compose ps

Du solltest die Services web, worker und redis alle mit dem Status Up sehen.

Löse einen Job aus deiner App aus und bestätige, dass er gelaufen ist, indem du die Logs des Workers prüfst:

docker compose logs -f worker

Du solltest den Job-Namen und eine Abschlusszeile sehen (Sidekiq loggt done: mit einer Zeitangabe; Celery loggt Task ... succeeded).

Schritt 5: Die systemd-Alternative, ohne Docker

Wenn deine App direkt auf dem Host läuft statt in Containern, gibt dir eine systemd-Unit dasselbe “läuft neben dem Web-Prozess, startet bei Absturz neu”-Verhalten ohne 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 mit WatchdogSec folgt der Beispiel-Unit-Datei in Sidekiqs eigenem Repository, die ab Sidekiq 6.0.6 funktioniert: Sidekiq meldet systemd, wann es bereit ist, und pingt es weiter, und systemd startet den Prozess innerhalb von Sekunden neu, wenn diese Pings aufhören.

Celery sendet diese Benachrichtigungen nicht, daher braucht es seine eigene Unit mit Type=simple und ohne Watchdog. Eine notify-Unit würde auf ein Bereitschaftssignal warten, das nie kommt, und systemd würde sie irgendwann töten:

# /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

Aktiviere und starte, welche auch immer du verwendest:

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

Für Celery verwende myapp-celery in denselben drei Befehlen.

Du solltest active (running) sehen, und Restart=always bedeutet, dass ein abgestürzter Worker von selbst zurückkommt, statt lautlos unten zu bleiben, bis jemand merkt, dass Jobs nicht mehr verarbeitet werden.

Die Abkürzung: mit Railyard

Alles in den Schritten 4 und 5 (ein zweiter Prozess aus demselben Image, seine eigene Restart-Policy, seine eigene Skalierung) ist genau das, was Railyards Workers-Tab direkt einrichtet. Es liest dein Repository nach Job-Runnern, die es erkennt – Sidekiq, Solid Queue, GoodJob, Resque, Celery, RQ, Laravels Queue-Worker oder Horizon, ein npm run worker-Skript, oder alles, was als Prozess in deinem Procfile, Procfile.dev oder app.json deklariert ist – und bietet jeden als Worker an, den du mit einem Klick hinzufügst. Jeder Worker läuft mit dem eigenen Image deiner App und skaliert unabhängig von deinem Web-Prozess, sodass ein Traffic-Spike auf der Website nicht automatisch bedeutet, auch für mehr Worker-Kapazität zu zahlen, oder umgekehrt.

Eine reine Worker-App, eine ganz ohne Web-Prozess, kann unter Settings → General auch so eingestellt werden, dass sie im Leerlauf schläft, und Railyard weckt sie nach eigenem Zeitplan, um alles Eingereihte zu verarbeiten, bevor es sie wieder schlafen lässt – das zählt, wenn die Queue die meiste Zeit wirklich ruhig ist und du lieber nicht für einen rund um die Uhr untätigen Prozess bezahlst.

Problembehandlung

Der Worker startet, nimmt aber nie Jobs an. Fast immer ein REDIS_URL-Mismatch zwischen dem Prozess, der Jobs einreiht, und dem, der den Worker ausführt, meistens weil einer sie aus einer .env-Datei liest und der andere aus einer anderen, oder ein veralteter Wert, der zur Build-Zeit statt zur Laufzeit in ein Image gebacken wurde. Bestätige, dass beide Prozesse auf dieselbe Redis-Instanz und dieselbe logische Datenbanknummer zeigen, und prüfe dann, ob überhaupt etwas in der Queue ist (redis-cli -u $REDIS_URL llen queue:default für Sidekiqs Standard-Queue-Benennung, oder das Äquivalent dazu, wie deine Queue-Namen eingerichtet sind).

Jobs bleiben in der Queue liegen und laufen nie, obwohl der Worker gesund ist. Prüfe, ob die Queue-Namen tatsächlich übereinstimmen. Sidekiq-Worker holen nur aus den Queues, die in ihrer eigenen :queues-Konfiguration aufgelistet sind; ein Job, der in eine Queue eingereiht wurde, auf die der Worker nie gehört wurde, bleibt unbegrenzt dort liegen, ohne Fehler auf beiden Seiten. Celery hat denselben Fehlerfall mit seinen Routing-Keys, wenn du eigene Queues konfiguriert hast statt die Standard-Queue zu verwenden.

Der Speicherverbrauch steigt, bis der Server den Worker tötet. Ein Worker mit einer höher eingestellten Concurrency, als der Server tatsächlich tragen kann, ist die häufigste Ursache; jeder Thread oder Prozess hält seinen eigenen Speicher (und bei Sidekiq seine eigene Datenbankverbindung), sodass eine Concurrency, die auf einen größeren Server zugeschnitten ist als den, auf dem du tatsächlich läufst, den Prozess irgendwann vom Out-of-Memory-Handling des Kernels getötet bekommt. Senke zuerst die Concurrency und beobachte, ob das Problem wiederkehrt, bevor du von einem Leck in deinem Job-Code ausgehst.

Der Worker-Prozess beendet sich sofort ohne klaren Fehler. Meist eine fehlende Umgebungsvariable, die der Worker braucht, der Web-Prozess aber nicht, da der Codepfad eines Workers durch Job-Klassen oft andere Teile der Konfiguration deiner App berührt. Führe den exakten Worker-Befehl von Hand in einer Shell mit derselben Umgebungsdatei aus, um die echte Exception zu sehen, statt das, was dein Prozessmanager daraus zusammenfasst.

Zwei Kopien eines Jobs sind irgendwie beide gelaufen. Keines der beiden Tools verspricht genau-einmalige Ausführung. Ein Job, der einen Fehler auslöst, wird wiederholt, und mit Celerys acks_late-Einstellung aktiviert wird ein mitten im Lauf unterbrochener Task erneut zugestellt, sodass ein Job, der schon einen Teil seiner Arbeit erledigt hatte, diese wiederholen kann. Schreibe Jobs so, dass ein zweimaliges Ausführen harmlos ist (prüfe, ob die E-Mail schon gesendet wurde, verwende Upserts statt Inserts), und das hört auf, ein Problem zu sein, egal wie viele Worker du betreibst.

FAQ

Sollte ich Sidekiq oder Celery verwenden? Was zur Sprache deines Web-Frameworks passt. Es gibt keinen bedeutsamen Zuverlässigkeitsunterschied, der eine Ruby-App zu Celery oder eine Python-App zu Sidekiq treiben sollte; beide sind ausgereift, beide nutzen standardmäßig Redis, und beide unterstützen Retries und geplante Jobs.

Braucht Celery RabbitMQ statt Redis? Nein. Celery unterstützt beides als Broker, und Redis ist einfacher zu betreiben, wenn du nicht schon aus anderen Gründen RabbitMQ laufen hast. RabbitMQ hat in manchen Grenzfällen stärkere Zustellgarantien, was mehr bei einer Größenordnung zählt, die die meisten kleinen Apps nicht erreichen.

Wie viele Worker-Prozesse brauche ich? Starte mit einem Prozess bei moderater Concurrency (5 Threads für Sidekiq, 4 für Celery, wie oben gezeigt) und beobachte, ob sich Jobs unter echter Last in der Queue stauen, bevor du weitere hinzufügst. Ein zweiter Worker-Prozess ist einfacher nachzuvollziehen, als die Concurrency eines einzelnen über das hinaus zu erhöhen, was die CPU des Servers und der Connection-Pool deiner Datenbank tatsächlich tragen können.

Was passiert mit einem Job, wenn der Worker mitten im Lauf abstürzt? Das hängt davon ab, wie der Worker gestoppt wurde. Bei einem normalen Shutdown (systemctl stop, docker compose stop, ein Deploy) wartet Sidekiq auf laufende Jobs bis zu seinem Shutdown-Timeout und schiebt unfertige zurück nach Redis. Wenn der Prozess hart getötet wird (ein Out-of-Memory-Kill, kill -9, ein Stromausfall), verliert Open-Source-Sidekiq die Jobs, die es gerade ausführte, weil es einen Job aus Redis entfernt, sobald es anfängt, daran zu arbeiten; Sidekiq Pros super_fetch existiert genau, um diese Lücke zu schließen. Celery bestätigt einen Task standardmäßig, bevor er läuft, sodass ein harter Absturz auch diesen Task verliert; task_acks_late = True zu setzen lässt Celery erst nach Abschluss des Tasks bestätigen, sodass ein unterbrochener Task erneut zugestellt wird statt verloren zu gehen. Wenn der Verlust eines gelegentlichen Jobs wichtig ist, halte Jobs idempotent und aktiviere entweder acks_late (Celery), oder zeichne wichtige Arbeit irgendwo dauerhaft auf, bevor du sie einreihst.

Brauche ich eine von meinem Cache getrennte Redis-Instanz? Nicht unbedingt, aber wenn du dir eine teilst, halte die Queue auf ihrer eigenen logischen Datenbanknummer oder ihrem eigenen Key-Prefix, und sei vorsichtig mit jeder maxmemory-policy, die unter Druck Keys verwirft, denn eine auf eine Queue angewandte Cache-Eviction-Policy kann lautlos einen Job fallen lassen, der nie tatsächlich verarbeitet wurde.

Sobald die Queue und der Worker mit derselben Redis-Instanz mit übereinstimmenden Queue-Namen sprechen, geht es im Rest hauptsächlich darum, die Concurrency auf das zuzuschneiden, was der Server und die Datenbank tatsächlich tragen können, statt eine größere Zahl zu raten, weil sie schneller klingt.

Warum Leute sich dafür Railyard entscheiden

  • Dein eigenes Cloud-Konto. Deploye auf DigitalOcean, Hetzner, Vultr, AWS, OVHcloud oder jedem Linux-Server per SSH, direkt von deinem Anbieter abgerechnet, ohne Aufschlag.
  • Ein fester Preis pro Team. Free ist für immer kostenlos: ein Server plus ein Datenbankserver, drei Apps, keine Karte. Pro kostet 19 $ im Monat für bis zu fünf Server und unbegrenzte Apps. Team kostet 49 $ im Monat mit unbegrenzten Servern. Keine Kosten pro App, pro Deployment, pro Sitz oder pro Gigabyte.
  • Templates mit einem Klick. Deploye Open-Source-Apps, wie Datenbanken, Analytics und Dev-Tools, mit einem Klick auf deinem eigenen Server.
  • Verwaltete Datenbanken. Postgres, MySQL und Redis mit geplanten, wiederherstellungsgeprüften Backups und Point-in-Time-Recovery.
  • Push-to-Deploy. Deploye von GitHub, GitLab oder der CLI, mit automatischem HTTPS und Releases ohne Ausfallzeit.

Füge einen Worker in einem Klick hinzu

Railyard liest die Abhängigkeiten deines Repos und bietet Sidekiq oder Celery als Worker an, den es neben deinem Web-Prozess betreiben kann, unabhängig skaliert.

Kostenloser Plan, keine Kreditkarte, und 14 Tage Pro für jedes neue Team. Ein fester Preis pro Team.