Deploying a Django application to production requires a stable, web server architecture. While Nginx combined with Gunicorn or Uvicorn is a common modern stack, Apache HTTP Server paired with mod_wsgi remains a time tested choice for serving Python web applications.
In this post, we will walk through setting up Apache with mod_wsgi in daemon mode, configuring HTTP to HTTPS redirection, handling Django’s static and media assets, and troubleshooting common edge cases such as plain HTTP requests hitting SSL enabled ports.
When using mod_wsgi, Apache can run in two modes: mbedded Mode and Daemon Mode. Daemon mode is superior for security, memory usage, and performance because restarting your Python application does not require restarting the entire Apache web server.
Installing Apache and mod_wsgi
Start by updating package lists and installing Apache along with the Python 3 WSGI module:
apt update
apt install apache2 libapache2-mod-wsgi-py3 -y
Enable and start the Apache service:
systemctl start apache2
systemctl enable apache2
Place your Django application under /var/www/. For illustration, let’s assume the following directory layout:
/var/www/django_project/
├── django_app/
│ ├── __init__.py
│ ├── settings.py
│ └── wsgi.py
├── django_env/
├── static/
├── media/
├── manage.py
└── db.sqlite3
Ensure your production settings in settings.py reflect your domain and static file directories:
ALLOWED_HOSTS = ['your-domain.com', 'www.your-domain.com']
STATIC_ROOT = '/var/www/django_project/static/'
STATIC_URL = '/static/'
MEDIA_ROOT = '/var/www/django_project/media/'
MEDIA_URL = '/media/'
Collect all static assets into STATIC_ROOT:
cd /var/www/django_project
source django_env/bin/activate
python manage.py collectstatic --noinput
Apache runs under the user and group www-data on Debian/Ubuntu systems. Grant permissions so Apache can read project files and write to media or SQLite databases:
chown -R www-data:www-data /var/www/django_project
chmod -R 755 /var/www/django_project
To serve your site securely over HTTPS and redirect HTTP traffic, we will configure two virtual hosts:
- Port 80 (
HTTP): Redirects all incoming traffic to HTTPS. - Port 443 (
HTTPS): Serves the Django application viamod_wsgiand handles SSL/TLS termination.
Create the configuration for HTTP:
nano /etc/apache2/sites-available/django-http.conf
Paste the following configuration using Apache’s builtin Redirect permanent directive:
<VirtualHost *:80>
ServerName your-domain.com
ServerAlias www.your-domain.com
ServerAdmin [email protected]
ErrorLog ${APACHE_LOG_DIR}/your-domain.com_error.log
CustomLog ${APACHE_LOG_DIR}/your-domain.com_access.log combined
# Force HTTPS redirect
Redirect permanent / https://your-domain.com/
</VirtualHost>
Create the configuration file for HTTPS:
nano /etc/apache2/sites-available/django-ssl.conf
Paste the following SSL configuration:
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName your-domain.com
ServerAlias www.your-domain.com
ServerAdmin [email protected]
DocumentRoot /var/www/django_project
ErrorLog ${APACHE_LOG_DIR}/your-domain.com_ssl_error.log
CustomLog ${APACHE_LOG_DIR}/your-domain.com_ssl_access.log combined
# Static files directory
Alias /static /var/www/django_project/static
<Directory /var/www/django_project/static>
Require all granted
</Directory>
# Media files directory
Alias /media /var/www/django_project/media
<Directory /var/www/django_project/media>
Require all granted
</Directory>
# WSGI script permissions
<Directory /var/www/django_project/django_app>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
# mod_wsgi Daemon Mode Configuration
WSGIDaemonProcess django_app python-path=/var/www/django_project python-home=/var/www/django_project/django_env
WSGIProcessGroup django_app
WSGIScriptAlias / /var/www/django_project/django_app/wsgi.py
# SSL Certificate Configuration
SSLEngine on
SSLCertificateFile /etc/ssl/certs/your-domain.com.crt
SSLCertificateKeyFile /etc/ssl/private/your-domain.com.key
SSLCertificateChainFile /etc/ssl/certs/your-domain.com-ca-bundle.crt
</VirtualHost>
</IfModule>
Enable the required Apache modules (ssl, rewrite, and wsgi):
a2enmod ssl rewrite wsgi
Disable default Apache sites and enable your Django sites:
a2dissite 000-default.conf default-ssl.conf
a2ensite django-http.conf django-ssl.conf
Test your configuration syntax before restarting:
apache2ctl configtest
If it prints Syntax OK, restart Apache:
systemctl restart apache2
You can verify active virtual hosts anytime with:
apache2ctl -S
Automated SSL with Let’s Encrypt (Certbot)
If you prefer free, automated SSL certificates via Let’s Encrypt instead of custom SSL certificate files:
- Install Certbot and its Apache plugin:
apt install certbot python3-certbot-apache -y - Run Certbot to generate certificates and auto-configure Apache:
certbot --apache -d your-domain.com -d www.your-domain.com
Certbot will automatically edit your Apache configurations and set up automatic renewal via systemd timers.
Sometimes users or misconfigured network clients attempt to connect using
http://your-domain.com:443. When plain HTTP requests hit an SSL-enabled Apache port, Apache throws an HTTP 400 Bad Request error:
Bad Request
Your browser sent a request that this server could not understand.
Reason: You're speaking plain HTTP to an SSL-enabled server port.
Instead use the HTTPS scheme to access this URL, please.
Because the SSL handshake fails prior to HTTP header processing, standard rewrite rules inside <VirtualHost *:443> will not trigger. You can catch this specific error and redirect users back to the canonical HTTPS URL by adding an ErrorDocument directive inside your SSL VirtualHost block:
<VirtualHost *:443>
...
# Redirect plain HTTP requests on SSL port to HTTPS home page
ErrorDocument 400 "https://your-domain.com/"
</VirtualHost>
When a user hits port 443 over HTTP, Apache responds with an HTTP 400 status and sends them to https://your-domain.com/, seamlessly correcting the protocol.