Part 5: Background Workers¶
Add a Celery worker to process tasks asynchronously using Redis as a broker.
What you'll learn¶
- Adding Celery to a Django project
- Defining a portless worker service in
appmecca.yaml - Enabling Redis with
cache: enabled: true - Managing worker services (start, stop, restart, logs)
- The
commandoverride for non-HTTP services
What you'll need¶
- A full-stack app with SPA runtime config from Part 4: SPA Runtime Config
Step 1: Add Celery to the backend¶
Add to backend/requirements.txt:
Create backend/config/celery.py:
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
app = Celery("todo")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
Edit backend/config/__init__.py:
Add Celery settings to backend/config/settings.py:
import os
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0")
CELERY_RESULT_BACKEND = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0")
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
Step 2: Create an async task¶
Create backend/core/tasks.py:
import logging
from celery import shared_task
logger = logging.getLogger(__name__)
@shared_task
def send_todo_notification(todo_id, action):
"""
Simulate sending a notification when a todo is created or completed.
Replace this with real email/webhook logic in production.
"""
logger.info(f"Notification: todo {todo_id} was {action}")
# In a real app:
# send_email(user.email, f"Todo {action}", ...)
return {"todo_id": todo_id, "action": action, "notified": True}
Trigger the task from the viewset. Update backend/core/views.py:
from django.http import JsonResponse
from rest_framework import viewsets
from .models import Todo
from .serializers import TodoSerializer
from .tasks import send_todo_notification
def health(request):
return JsonResponse({"status": "healthy"})
class TodoViewSet(viewsets.ModelViewSet):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
def perform_create(self, serializer):
todo = serializer.save()
send_todo_notification.delay(todo.id, "created")
def perform_update(self, serializer):
todo = serializer.save()
if todo.done:
send_todo_notification.delay(todo.id, "completed")
Step 3: Update appmecca.yaml¶
Add the worker service and enable Redis:
database:
enabled: true
redis:
enabled: true
containers:
- name: backend
dockerfile: backend/Dockerfile
context: backend/
- name: frontend
dockerfile: frontend/Dockerfile
context: frontend/
deploy:
- name: backend
container: backend
port: 8000
path: /api
health_check:
path: /health
environment:
CELERY_BROKER_URL: "${redis_url}"
- name: frontend
container: frontend
type: spa
command: 'nginx -g "daemon off;"' # serves the built bundle from a container
spa_root: /usr/share/nginx/html
port: 3000
health_check:
path: /
environment:
PUBLIC_API_URL: "${service:backend}"
PUBLIC_APP_TITLE: "Todo App"
- name: worker
container: backend
command: "celery -A config worker --loglevel=info"
environment:
CELERY_BROKER_URL: "${redis_url}"
Portless services
The worker service has no port field. AppMecca treats it as a background process — it gets no public hostname, no route on the platform HTTPS router, and no health check. The container runs the command instead of the Dockerfile's default CMD.
Top-level cache: is the managed Redis
The cache: enabled: true block at the top level of appmecca.yaml turns on the managed Redis cache and injects REDIS_URL into every container. It defaults to off.
Do not confuse it with the per-service deploy[].cache: block, which configures edge caching for a web service and is a completely different feature. They share a name and nothing else.
Shared container image
The worker uses the same backend container image but with a different entrypoint command. This avoids building a separate image for the worker.
Step 4: Build and deploy¶
mecca app show todo-app now lists three services: backend, frontend, and worker. Only backend and frontend have URLs.
Step 5: Manage the worker service¶
Read worker logs to see task execution. Select the service with --service:
Create a todo via the frontend or curl, then check the worker logs for the notification task output.
Restart the worker after a code change. Here the service name is a positional argument, following the app name:
Stop and start the worker independently:
Worker restarts
Restarting a worker sends SIGTERM to the Celery process. Tasks that are currently executing will attempt to finish within the shutdown timeout. Design tasks to be idempotent in case they are interrupted.
Step 6: Verify the full flow¶
- Open the frontend and create a new todo
- Check worker logs for the notification:
You should see output like:
- Mark the todo as done in the frontend
- Check logs again for the "completed" notification
What's next¶
You have a complete three-service application. In Part 6: Branch Deployments, you will deploy a feature branch alongside your main deployment for testing.