Deploy Django With mod_wsgi-express

Run Django in production with mod_wsgi-express: install mod_wsgi with pip into the virtualenv, generate a self-contained Apache configuration, serve static files, run it under systemd, and put it behind Nginx or use it directly on port 80.

Problem statement

You like Apache's maturity and mod_wsgi's process model, or your organization standardizes on Apache, but configuring the system Apache, matching its compiled Python version, and editing virtual hosts is painful. mod_wsgi-express packages mod_wsgi and a generated Apache configuration into a command you install with pip. It runs Django with the Python of your virtualenv, serves static files, and can run under systemd like Gunicorn, without touching the system Apache configuration.

mod_wsgi-expresspip installmod_wsgi in venvsetup-servergenerates configsystemd unitapachectl startProxy or port 80static filesA pip-installed Apache instance dedicated to one Django app.
  1. Install Apache development headers (apache2-dev on Debian/Ubuntu), then pip install mod_wsgi in the app's virtualenv.
  2. Add mod_wsgi.server to INSTALLED_APPS to get a runmodwsgi management command, or call mod_wsgi-express directly.
  3. Generate a config: python manage.py runmodwsgi --setup-only --server-root=/srv/app/mod_wsgi --port=8000 --user=deploy --group=deploy.
  4. Run it under systemd with ExecStart=/srv/app/mod_wsgi/apachectl -D FOREGROUND.
  5. Put Nginx or a load balancer in front for TLS, or let it bind port 80 directly with --port 80 and start as root.

Step-by-step solution

1. Install

sudo apt-get install -y apache2-dev python3-dev
/srv/app/.venv/bin/pip install mod_wsgi

mod_wsgi compiles against the Apache installation found through apxs and the Python of the virtualenv, so the versions always match, which avoids the most common mod_wsgi error.

2. Generate a server configuration

INSTALLED_APPS += ["mod_wsgi.server"]
cd /srv/app/src
/srv/app/.venv/bin/python manage.py collectstatic --noinput
/srv/app/.venv/bin/python manage.py runmodwsgi --setup-only \
  --server-root=/srv/app/mod_wsgi \
  --port=8000 --user=deploy --group=deploy \
  --processes=3 --threads=5 \
  --url-alias /static /srv/app/src/staticfiles \
  --log-to-terminal --reload-on-changes

Drop --reload-on-changes in production. runmodwsgi reads WSGI_APPLICATION from settings, so you do not pass the WSGI file path. The command writes an apachectl script and an httpd.conf into the server root.

runmodwsgi --setup-onlyrunmodwsgi--setup-only--server-root=/srv/app/mod_wsgi--port=8000 --user=deploy --group=deploy--processes=3 --threads=5--url-alias/static /srv/app/src/staticfiles1231Writes config and apachectl; does not start.2Daemon processes and threads per process.3Apache serves static files directly.One command produces a complete, dedicated Apache configurationdjango-deployment.com
Rerun the command after changing options; it overwrites the generated files.

3. Run under systemd

# /etc/systemd/system/shop.service
[Unit]
Description=Shop (mod_wsgi-express)
After=network.target

[Service]
Type=simple
EnvironmentFile=/etc/shop/env
ExecStart=/srv/app/mod_wsgi/apachectl -D FOREGROUND
ExecReload=/srv/app/mod_wsgi/apachectl graceful
Restart=always

[Install]
WantedBy=multi-user.target

The generated Apache starts as root only if it needs a privileged port; with port 8000 it can run entirely as deploy if you add User=deploy to the unit. Environment variables from EnvironmentFile are passed to Django through Apache's environment.

4. TLS and the front proxy

Two common layouts:

  • Behind Nginx or a load balancer: mod_wsgi-express listens on 127.0.0.1:8000; the proxy terminates TLS. Set SECURE_PROXY_SSL_HEADER and pass --trust-proxy-header X-Forwarded-Proto.
  • Direct: run on ports 80 and 443 with --https-port 443 --ssl-certificate-file ... --ssl-certificate-key-file ..., starting as root so Apache can bind the ports and then drop to --user.

5. Deploy updates

After pulling new code and running migrations, reload gracefully:

sudo systemctl reload shop

apachectl graceful restarts the daemon processes when current requests finish.

operational differencesmod_wsgi-expressGunicornInstallpip + Apache headerspip onlyStatic filesurl-alias built inWhiteNoise or proxyConcurrencyprocesses x threadsworkers x threadsReloadapachectl gracefulHUP signalBoth are solid; pick the one your team can operatedjango-deployment.com
Apache’s request buffering protects workers from slow clients without Nginx.

Explanation

mod_wsgi-express builds a minimal Apache configuration around mod_wsgi in daemon mode: Apache's child processes accept connections and hand requests to dedicated mod_wsgi daemon processes running your Python code. Because mod_wsgi is installed with pip into the virtualenv, it is compiled for that exact Python, eliminating the version mismatch that troubles distribution packages. Apache's buffering of slow clients and its static file serving mean you can run it without a separate web server if you prefer.

Edge cases / notes

  • Build dependencies. Containers need apache2-dev and a compiler at build time; use a multi-stage build to keep the runtime image smaller.
  • Logs. --log-to-terminal sends Apache logs to stdout, which journald or Docker capture.
  • Authorization headers. Add --pass-authorization if your API uses the Authorization header.

Next steps

FAQ

Does mod_wsgi-express need the system Apache?

It needs Apache installed (binaries and development headers), but uses its own generated configuration, not the system site files.

Can it serve static files?

Yes, with --url-alias /static /path/to/staticfiles.

How do I pass the Authorization header to Django?

Use --pass-authorization, which enables WSGIPassAuthorization.

Is mod_wsgi-express suitable for production?

Yes. It is the same mod_wsgi in daemon mode, configured automatically.

Keep going