Django
Gunicorn
Celery
Systemd Service
Application Configuration

How to configure Celery to run as systemd service with a Django application served by Gunicorn?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Celery should run as its own supervised service, even when the Django web application is already running under Gunicorn. Gunicorn serves HTTP requests, while Celery workers consume background jobs from the broker, so systemd should manage them as separate processes with separate unit files.

Keep Gunicorn and Celery as Independent Services

One common misunderstanding is that Celery somehow runs "inside" Gunicorn because both processes import the same Django project. In production they should be treated as peers:

  • Gunicorn runs the WSGI or ASGI app.
  • Celery worker processes background tasks.
  • Celery Beat, if used, schedules recurring tasks.

That separation is useful operationally. You can restart a web service without interrupting background work, or scale workers without touching the HTTP stack.

Before writing any unit file, confirm the Django project exposes a proper Celery application.

python
1# myproject/celery.py
2import os
3from celery import Celery
4
5os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
6
7app = Celery("myproject")
8app.config_from_object("django.conf:settings", namespace="CELERY")
9app.autodiscover_tasks()
python
1# myproject/__init__.py
2from .celery import app as celery_app
3
4__all__ = ("celery_app",)

If celery -A myproject inspect ping fails in the virtual environment, fix that first. systemd should supervise a working command, not hide an import problem.

Create a Dedicated Worker Unit

For modern Celery deployments, Type=simple is usually the correct systemd setting. Let Celery stay in the foreground and allow systemd to monitor the main process directly.

ini
1[Unit]
2Description=Celery Worker for myproject
3After=network.target redis.service
4
5[Service]
6Type=simple
7User=www-data
8Group=www-data
9WorkingDirectory=/srv/myproject
10Environment="DJANGO_SETTINGS_MODULE=myproject.settings"
11Environment="PATH=/srv/myproject/.venv/bin"
12ExecStart=/srv/myproject/.venv/bin/celery -A myproject worker --loglevel=INFO --concurrency=4
13Restart=always
14RestartSec=5
15
16[Install]
17WantedBy=multi-user.target

Save that as /etc/systemd/system/celery.service. Then reload and start it:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now celery.service
sudo systemctl status celery.service

This gives you automatic startup on boot, restart on failure, and a clear process boundary.

Keep Gunicorn in Its Own Unit File

Gunicorn should have a separate service with the same project directory and virtual environment, but a different startup command.

ini
1[Unit]
2Description=Gunicorn for myproject
3After=network.target
4
5[Service]
6Type=simple
7User=www-data
8Group=www-data
9WorkingDirectory=/srv/myproject
10Environment="PATH=/srv/myproject/.venv/bin"
11ExecStart=/srv/myproject/.venv/bin/gunicorn myproject.wsgi:application --bind 127.0.0.1:8000
12Restart=always
13
14[Install]
15WantedBy=multi-user.target

The two services are related by codebase, not by process ownership. A deploy should typically restart both because the shared application code changed, but a crash in one does not mean the other should be bundled into the same service definition.

Put Environment Values in an Environment File

Once broker URLs, secrets, and Django settings become nontrivial, a separate environment file keeps the unit readable.

ini
[Service]
EnvironmentFile=/etc/myproject/celery.env
ExecStart=/srv/myproject/.venv/bin/celery -A myproject worker --loglevel=INFO

Example environment file:

bash
DJANGO_SETTINGS_MODULE=myproject.settings
CELERY_BROKER_URL=redis://127.0.0.1:6379/0
CELERY_RESULT_BACKEND=redis://127.0.0.1:6379/1

This also makes secret rotation and environment-specific overrides easier to manage than editing the unit file itself.

Run Beat Separately if You Need Scheduling

If the project uses scheduled jobs, Celery Beat should be a second service rather than a hidden child process of the worker.

ini
1[Unit]
2Description=Celery Beat for myproject
3After=network.target redis.service
4
5[Service]
6Type=simple
7User=www-data
8Group=www-data
9WorkingDirectory=/srv/myproject
10EnvironmentFile=/etc/myproject/celery.env
11Environment="PATH=/srv/myproject/.venv/bin"
12ExecStart=/srv/myproject/.venv/bin/celery -A myproject beat --loglevel=INFO
13Restart=always
14
15[Install]
16WantedBy=multi-user.target

Operationally this is cleaner. If Beat fails, worker throughput is unaffected. If workers fail, scheduling can still be diagnosed separately.

Logs from both services are available through the journal:

bash
journalctl -u celery.service -f
journalctl -u celery-beat.service -f

Common Pitfalls

  • Assuming Gunicorn starts Celery automatically because both use the same Django project.
  • Using Type=forking when Celery can run in the foreground and be supervised directly.
  • Skipping manual command-line verification before blaming systemd for startup failures.
  • Mixing worker and Beat into one hard-to-debug service definition.
  • Forgetting WorkingDirectory, PATH, or DJANGO_SETTINGS_MODULE, which causes most import failures.

Summary

  • Run Gunicorn, Celery worker, and Celery Beat as separate systemd services.
  • Verify the Django and Celery wiring before writing unit files.
  • Use Type=simple and keep Celery in the foreground.
  • Store environment-specific values in an EnvironmentFile.
  • Use journalctl and systemctl status for routine supervision and debugging.

Course illustration
Course illustration

All Rights Reserved.