Ejecuta workers de Sidekiq o Celery junto a tu aplicación web
Sigue esto en tu propio servidor. Empieza gratis, sin tarjeta
En el momento en que una aplicación necesita enviar un correo electrónico, redimensionar una imagen o llamar a una API de terceros lenta como efecto secundario de una solicitud, ese trabajo debe ocurrir en algún lugar que no sea la solicitud misma. Sidekiq para Ruby y Celery para Python resuelven el mismo problema: una cola retiene la tarea, y un proceso worker separado extrae las tareas de ella y las ejecuta, de modo que un proveedor de correo lento nunca hace que la carga de una página se sienta lenta.
Esta guía configura un proceso worker junto a una aplicación web existente: Sidekiq con Redis como backend de cola para una aplicación Ruby, o Celery con Redis como broker para una aplicación Python, ambos ejecutables con Docker Compose o como un simple servicio systemd.
Lo que necesitarás
- Un servidor que ya ejecute tu aplicación web, o uno nuevo. Un Hetzner CX23 (2 vCPU, 4 GB de RAM, 40 GB NVMe, 5,99 €/mes sin IVA) tiene suficiente margen para ejecutar juntos un proceso web, un proceso worker y Redis para una aplicación pequeña a mediana.
- Redis. Si todavía no tienes una instancia, la imagen oficial
redis:8-alpineen Compose es la forma más rápida de conseguir una; el paso 1 muestra cómo. - El código de tu aplicación ya estructurado para que las tareas se definan en algún lugar que el proceso worker pueda importar: un directorio
app/jobsoapp/sidekiqen términos de Rails/Sidekiq, untasks.pycon decoradores@app.tasken términos de Celery.
Paso 1: agrega Redis como cola compartida
Tanto Sidekiq como Celery necesitan un broker para retener las tareas entre su encolado y su ejecución. Redis es la opción común para ambos (Celery también admite RabbitMQ, pero Redis cubre la gran mayoría de configuraciones y mantiene esta guía con una sola pieza móvil):
services:
redis:
image: redis:8-alpine
volumes:
- redis-data:/data
restart: unless-stopped
volumes:
redis-data:
Si esta instancia de Redis es únicamente una cola de tareas y nada más lee ni escribe en ella, no necesitas los indicadores de ajuste de caché de una configuración Redis de propósito general, los valores por defecto están bien; solo asegúrate de que el volumen esté presente para que las tareas encoladas pero aún no ejecutadas sobrevivan a un reinicio en lugar de desaparecer.
Paso 2: configura Sidekiq (Ruby)
Agrega la gem. Esta guía usa Sidekiq 8.1 (8.1.7 es la versión actual en RubyGems):
gem "sidekiq", "~> 8.1"
bundle install
Define una tarea:
class WelcomeEmailJob
include Sidekiq::Job
def perform(user_id)
user = User.find(user_id)
UserMailer.welcome(user).deliver_now
end
end
Apunta Sidekiq a la instancia de Redis del paso 1 y establece una concurrencia razonable para el tamaño del servidor. Cada hilo de Sidekiq mantiene su propia conexión a la base de datos, así que la concurrencia debe coincidir con el pool de conexiones de tu base de datos, no solo con el CPU que te sobra:
# 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
Encola la tarea desde cualquier parte de tu aplicación:
WelcomeEmailJob.perform_async(user.id)
Paso 3: configura Celery (Python)
Agrega la dependencia. Esta guía usa Celery 5.6 (5.6.3 es la versión actual en PyPI); el extra redis instala el cliente Redis que Celery necesita:
pip install "celery[redis]==5.6.3"
Define la aplicación Celery y una tarea en tasks.py, la estructura que usa la propia guía de primeros pasos 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}")
Reemplaza el cuerpo con tu trabajo real. Llama a la tarea desde el código de tu aplicación web con .delay(), que la coloca en la cola y retorna de inmediato:
from tasks import send_welcome_email
send_welcome_email.delay(user.id)
En un proyecto Django, la guía “First steps with Django” de Celery coloca la aplicación en proj/celery.py y carga primero tus configuraciones de Django, para que las tareas puedan usar el ORM; el comando del worker se convierte entonces en celery -A proj worker en lugar de celery -A tasks worker.
Paso 4: ejecuta el worker como su propio proceso
Sin importar qué biblioteca uses, el worker es un proceso de larga duración separado de tu servidor web, no un hilo dentro de él. Con Docker Compose, eso significa un segundo servicio construido a partir de la misma imagen que tu aplicación web, que difiere solo en su comando:
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:
El equivalente para Celery simplemente cambia los comandos de web y 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 en el worker de Sidekiq importa en cada despliegue. Docker envía TERM al detenerse y mata el contenedor 10 segundos después por defecto, mientras que Sidekiq espera hasta 25 segundos por defecto por las tareas en ejecución antes de devolver las inacabadas a Redis. Sin ese período de gracia más largo, Docker mata a Sidekiq antes de que pueda devolver esas tareas.
En ambos casos, el archivo .env junto al archivo Compose define REDIS_URL=redis://redis:6379/0, de modo que el proceso web y el worker apuntan al mismo servicio Redis por su nombre en Compose.
Levanta todo:
docker compose up -d
docker compose ps
Deberías ver los servicios web, worker y redis, todos con estado Up.
Dispara una tarea desde tu aplicación y confirma que se ejecutó revisando los registros del worker:
docker compose logs -f worker
Deberías ver el nombre de la tarea y una línea de finalización (Sidekiq registra done: con un tiempo; Celery registra Task ... succeeded).
Paso 5: la alternativa con systemd, sin Docker
Si tu aplicación se ejecuta directamente en el host en lugar de en contenedores, una unidad systemd te da el mismo comportamiento de “se ejecuta junto al proceso web, se reinicia ante un fallo” sin 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 con WatchdogSec sigue el archivo de unidad de ejemplo del propio repositorio de Sidekiq, que funciona desde Sidekiq 6.0.6 en adelante: Sidekiq le indica a systemd cuándo está listo y sigue enviándole señales, y systemd reinicia el proceso en segundos si esas señales se detienen.
Celery no envía esas notificaciones, así que necesita su propia unidad con Type=simple y sin watchdog. Una unidad notify se quedaría esperando una señal de disponibilidad que nunca llega, y systemd terminaría por matarla:
# /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
Activa e inicia la que uses:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp-sidekiq
sudo systemctl status myapp-sidekiq
Para Celery, usa myapp-celery en los mismos tres comandos.
Deberías ver active (running), y Restart=always significa que un worker que falla vuelve por sí solo en lugar de quedarse caído silenciosamente hasta que alguien note que las tareas dejaron de procesarse.
El atajo: hazlo con Railyard
Todo lo que hay en los pasos 4 y 5 (un segundo proceso a partir de la misma imagen, su propia política de reinicio, su propia escala) es lo que la pestaña Workers de Railyard configura directamente. Lee tu repositorio en busca de ejecutores de tareas que reconoce, Sidekiq, Solid Queue, GoodJob, Resque, Celery, RQ, el worker de colas o Horizon de Laravel, un script npm run worker, o cualquier cosa declarada como proceso en tu Procfile, Procfile.dev o app.json, y ofrece cada uno como un worker que agregas con un clic. Cada worker ejecuta la propia imagen de tu aplicación y escala de forma independiente de tu proceso web, de modo que un pico de tráfico en el sitio web no tiene por qué significar pagar también por más capacidad de worker, ni al revés.
Una aplicación solo de workers, sin ningún proceso web, también se puede configurar para dormir cuando está inactiva en Settings → General, y Railyard la despierta según su propio horario para vaciar lo que esté encolado antes de dejarla volver a dormir, lo cual importa si la cola está genuinamente tranquila la mayor parte del tiempo y prefieres no pagar por un proceso inactivo las 24 horas.
Solución de problemas
El worker arranca pero nunca recoge ninguna tarea. Casi siempre es un desajuste de REDIS_URL entre el proceso que encola las tareas y el proceso que ejecuta el worker, comúnmente porque uno lo lee de un archivo .env y el otro de uno distinto, o un valor obsoleto fijado en una imagen en el momento de la compilación en lugar de leerse en tiempo de ejecución. Confirma que ambos procesos apuntan a la misma instancia de Redis y al mismo número de base de datos lógica, luego revisa que la cola tenga algo (redis-cli -u $REDIS_URL llen queue:default para el nombrado de cola por defecto de Sidekiq, o el equivalente según cómo estén configurados tus nombres de cola).
Las tareas se quedan en la cola y nunca se ejecutan, aunque el worker esté sano. Revisa que los nombres de cola realmente coincidan. Los workers de Sidekiq solo toman de las colas listadas en su propia configuración :queues; una tarea encolada en una cola que nunca se le dijo al worker que escuchara se queda ahí indefinidamente sin ningún error de ningún lado. Celery tiene el mismo modo de falla con sus claves de enrutamiento si has configurado colas personalizadas en lugar de usar la predeterminada.
La memoria sube hasta que el servidor mata al worker. Un worker con una concurrencia configurada más alta de lo que el servidor realmente puede soportar es la causa más común; cada hilo o proceso mantiene su propia memoria (y, en el caso de Sidekiq, su propia conexión a la base de datos), así que una concurrencia ajustada para un servidor más grande que el que realmente usas terminará por hacer que el kernel mate el proceso por falta de memoria. Baja primero la concurrencia y observa si el problema se repite antes de asumir que es una fuga en el código de tus tareas.
El proceso worker termina de inmediato sin un error claro. Generalmente es una variable de entorno que falta y que el worker necesita pero el proceso web no, ya que la ruta de código de un worker a través de las clases de tareas suele tocar partes distintas de la configuración de tu aplicación. Ejecuta el comando exacto del worker a mano en una shell con el mismo archivo de entorno para ver la excepción real en lugar de lo que tu gestor de procesos haya resumido.
De alguna manera se ejecutaron dos copias de una misma tarea. Ninguna de las dos herramientas garantiza una ejecución exactamente una vez. Una tarea que lanza un error se reintenta, y con el ajuste acks_late de Celery activado, una tarea interrumpida a mitad de ejecución se entrega de nuevo, así que una tarea que ya había hecho parte de su trabajo puede repetirlo. Escribe las tareas de modo que ejecutarlas dos veces sea inofensivo (verifica si el correo ya se envió, usa upserts en lugar de inserciones), y eso deja de ser un problema sin importar cuántos workers ejecutes.
FAQ
¿Debería usar Sidekiq o Celery? El que coincida con el lenguaje de tu framework web. No hay una diferencia de confiabilidad significativa que deba inclinar una aplicación Ruby hacia Celery o una aplicación Python hacia Sidekiq; ambos son maduros, ambos usan Redis por defecto, y ambos admiten reintentos y tareas programadas.
¿Celery necesita RabbitMQ en lugar de Redis? No. Celery admite ambos como broker, y Redis es más simple de operar si no estás ya ejecutando RabbitMQ para otra cosa. RabbitMQ ofrece garantías de entrega más fuertes en algunos casos límite, lo cual importa más a una escala que la mayoría de las aplicaciones pequeñas no alcanzan.
¿Cuántos procesos worker necesito? Comienza con un solo proceso a una concurrencia modesta (5 hilos para Sidekiq, 4 para Celery, como se mostró arriba) y observa si las tareas se acumulan en la cola bajo carga real antes de agregar más. Un segundo proceso worker es más simple de razonar que aumentar la concurrencia de un solo proceso más allá de lo que el CPU del servidor y el pool de conexiones de tu base de datos pueden realmente soportar.
¿Qué le pasa a una tarea si el worker falla a mitad de ejecución? Depende de cómo se detuvo el worker. En un apagado normal (systemctl stop, docker compose stop, un despliegue), Sidekiq espera por las tareas en ejecución hasta su tiempo de espera de apagado y devuelve a Redis las que quedaron inacabadas. Si el proceso se mata de forma abrupta (un kill por falta de memoria, kill -9, un corte de energía), Sidekiq de código abierto pierde las tareas que estaba ejecutando, porque retira una tarea de Redis en cuanto empieza a trabajar en ella; el super_fetch de Sidekiq Pro existe para cerrar esa brecha. Celery confirma una tarea antes de ejecutarla por defecto, así que un fallo abrupto también pierde esa tarea; configurar task_acks_late = True hace que Celery confirme después de que la tarea termina, de modo que una tarea interrumpida se entrega de nuevo en su lugar. Si perder una tarea ocasional te importa, mantén las tareas idempotentes y activa acks_late (Celery) o registra el trabajo importante en algún lugar duradero antes de encolarlo.
¿Necesito una instancia de Redis separada de mi caché? No necesariamente, pero si compartes una, mantén la cola en su propio número de base de datos lógica o prefijo de clave, y ten cuidado con cualquier maxmemory-policy que desaloje claves bajo presión, ya que una política de desalojo de caché aplicada a una cola puede eliminar silenciosamente una tarea que en realidad nunca se procesó.
Una vez que la cola y el worker se comunican con la misma instancia de Redis y nombres de cola que coinciden, el resto se trata sobre todo de dimensionar la concurrencia según lo que el servidor y la base de datos realmente pueden soportar, no de adivinar un número más grande porque suena más rápido.
Por qué eligen Railyard para esto
- Tu propia cuenta en la nube. Despliega en DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, o cualquier servidor Linux por SSH, facturado directamente por tu proveedor, sin márgenes adicionales.
- Un precio fijo por equipo. El plan Free es gratis para siempre: un servidor más un servidor de base de datos, tres apps, sin tarjeta. Pro cuesta $19 al mes por hasta cinco servidores y apps ilimitadas. Team cuesta $49 al mes con servidores ilimitados. Sin cargos por app, por despliegue, por asiento ni por gigabyte.
- Plantillas de un clic. Despliega apps de código abierto, como bases de datos, analítica y herramientas de desarrollo, en tu propio servidor con un clic.
- Bases de datos administradas. Postgres, MySQL y Redis, con copias de seguridad programadas y verificadas por restauración, y recuperación a un punto en el tiempo.
- Despliegue con git push. Despliega desde GitHub, GitLab o la CLI, con HTTPS automático y lanzamientos sin tiempo de inactividad.