Skip to content

Django + Celery Background Tasks

A Django application with Celery background task processing. This app runs two services from the same Docker image: a web server and a Celery worker. It demonstrates portless services, command overrides, and the combination of database and cache.

Source: git@gitlab.com:appmecca-sample-apps/django-celery-tasks.git

What This App Demonstrates

  • Two services from one container image -- web and worker use the same Docker image
  • Command override -- the worker runs celery instead of the default gunicorn
  • Portless services -- the worker has no port, no URL, and no platform health check
  • Database + Cache together -- PostgreSQL for data, Redis as Celery's message broker
  • APPMECCA_SERVICE guard -- migrations run only on the web service, not the worker

appmecca.yaml

containers:
  - name: web
    dockerfile: Dockerfile
    context: .

deploy:
  - name: web
    container: web
    port: 8000
    health_check:
      path: /health/

  - name: worker
    container: web
    command: "celery -A config worker -l info"

database:
  enabled: true

redis:
  enabled: true

There are four things to understand here:

1. One container, two services

Both web and worker reference container: web -- the same Docker image. AppMecca builds one image and starts two containers from it, each running a different command.

2. Command override

The web service runs the Dockerfile's default CMD (the entrypoint script, which starts gunicorn). The worker service overrides this with command: "celery -A config worker -l info", which starts the Celery worker process instead.

How command override works

When a service has a command field, AppMecca passes it as the Docker CMD, replacing whatever is in the Dockerfile. The ENTRYPOINT is not affected -- if your Dockerfile has an ENTRYPOINT, the command is passed as arguments to it. If you want the command to replace everything (including the entrypoint), use a shell-form command.

3. Portless service

The worker service has no port field and no health_check:

Service Has port? Gets a URL? Has health check?
web Yes (8000) Yes Yes (/health/)
worker No No No

The worker runs as a standalone process. AppMecca starts it and monitors that the container stays alive, but does not route any web traffic to it.

4. Database + Cache

Both database and cache are enabled. Every container (both web and worker) receives the same connection variables:

Variable Source Used by
DATABASE_URL Database Django ORM
DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD Database Django ORM
REDIS_URL Cache Celery broker
CELERY_BROKER_URL Cache Celery broker
CACHE_PREFIX Cache Key namespacing

The worker needs both: it reads tasks from Redis and writes results to PostgreSQL.

URL Pattern

Only the web service has a port, so only web gets a URL. The service name is always part of the hostname — there is no bare {app}.{env}.{domain} form, even when the app has a single reachable service:

https://django-celery-tasks-web.dev.appmecca.net

If the worker also had a port, it would get django-celery-tasks-worker.dev.appmecca.net alongside it. Because it is portless, it is reachable only from inside the app.

Entrypoint Script

The entrypoint script handles database readiness and migration guarding:

#!/bin/bash
set -e

echo "Waiting for database..."

RETRIES=10
DELAY=3

for i in $(seq 1 $RETRIES); do
    python -c "
import psycopg2, os
try:
    conn = psycopg2.connect(
        host=os.environ.get('DB_HOST', 'localhost'),
        port=int(os.environ.get('DB_PORT', '5432')),
        user=os.environ.get('DB_USER', 'postgres'),
        password=os.environ.get('DB_PASSWORD', ''),
        dbname=os.environ.get('DB_NAME', 'postgres'),
    )
    conn.close()
    print('Database is ready!')
except Exception as e:
    print(f'Database not ready: {e}')
    exit(1)
" && break

    echo "Attempt $i/$RETRIES failed. Retrying in ${DELAY}s..."
    sleep $DELAY
done

# Only the web service runs migrations
if [ "$APPMECCA_SERVICE" = "web" ]; then
    echo "Running migrations..."
    python manage.py migrate --noinput
fi

echo "Starting application..."
exec "$@"

The worker skips this script

Because the worker service specifies a command override, it runs celery -A config worker -l info directly. The entrypoint script only runs for the web service (which uses the Dockerfile's default CMD). This means the worker does not wait for the database or run migrations -- only the web service does.

Django settings for Celery

Configure Celery to use the APPMECCA-injected Redis URL:

import os

# Celery configuration
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0")
CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"

Deploy It

# Register the app
mecca register git@gitlab.com:appmecca-sample-apps/django-celery-tasks.git -e dev

# Build (one image used by both services)
mecca build trigger -e dev -g git@gitlab.com:appmecca-sample-apps/django-celery-tasks.git

# Deploy (starts both web and worker)
mecca app deploy -e dev django-celery-tasks
Expected output
Deploying django-celery-tasks to dev...
Deployment queued: deploy-xxxxxxxx
Waiting for agents...
Container started: django-celery-tasks-web
Container started: django-celery-tasks-worker
Health check passed: /health/ -> 200 OK
Deployment complete.

Services:
  web:    https://django-celery-tasks-web.dev.appmecca.net (healthy)
  worker: running (no port)

Managing Services

List services

mecca app show django-celery-tasks -e dev
Expected output
Services for django-celery-tasks (dev):

SERVICE    STATUS     PORT    URL
web        running    8000    https://django-celery-tasks-web.dev.appmecca.net
worker     running    -       (no port)

View logs

You can view logs for both services independently. The service is selected with --service, not as a positional argument:

mecca logs django-celery-tasks --type container --service web -e dev
mecca logs django-celery-tasks --type container --service worker -e dev
mecca logs django-celery-tasks --service worker --level error --since 24h -e dev

mecca logs is a search, not a live tail

There is no --follow flag. mecca logs queries a time window (--since, default 1h) and prints the matches. Re-run it to see newer lines.

Worker log output
[2025-06-15 10:23:01,234: INFO/MainProcess] celery@abc123 ready.
[2025-06-15 10:23:01,235: INFO/MainProcess] Connected to redis://10.0.10.50:6379/0
[2025-06-15 10:23:15,890: INFO/ForkPoolWorker-1] Task myapp.tasks.send_email[abc-123] succeeded in 0.234s

Restart the worker

If the worker gets stuck, restart it without affecting the web server. The service name is the second positional argument, after the app name:

mecca app restart django-celery-tasks worker --force -e dev

Adding More Workers

You can add additional worker processes by extending the deploy section. For example, adding a Celery Beat scheduler:

deploy:
  - name: web
    container: web
    port: 8000
    health_check:
      path: /health/

  - name: worker
    container: web
    command: "celery -A config worker -l info"

  - name: scheduler
    container: web
    command: "celery -A config beat -l info"

All three services use the same Docker image. Only web gets a URL. The worker and scheduler run their respective commands as background processes.

Key Takeaways

  • Background workers use the same container image as your web server -- define them with a command override
  • Services without a port are internal only -- no URL, no inbound traffic, no health check
  • Use APPMECCA_SERVICE in your entrypoint to guard migrations so only the web service runs them
  • Enable cache: enabled: true for Redis when your workers need a message broker
  • Use mecca logs to view worker output and mecca app restart to restart individual services

What's Next?