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 --
webandworkeruse the same Docker image - Command override -- the worker runs
celeryinstead of the defaultgunicorn - 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_SERVICEguard -- migrations run only on thewebservice, 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:
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¶
Expected output
View logs¶
You can view logs for both services independently. The service is selected with
--service, not as a positional argument:
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
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:
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
commandoverride - Services without a port are internal only -- no URL, no inbound traffic, no health check
- Use
APPMECCA_SERVICEin your entrypoint to guard migrations so only the web service runs them - Enable
cache: enabled: truefor Redis when your workers need a message broker - Use
mecca logsto view worker output andmecca app restartto restart individual services
What's Next?¶
- Flask + Vue Fullstack -- Multi-container app with separate frontend and backend
- Cache Configuration -- Full reference for Redis/cache settings
- Services & Logs -- Complete service management reference