Serving a Django Application with Apache over HTTPS

Tadios Abebe | Mar 20, 2025 min read

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:

  1. Port 80 (HTTP): Redirects all incoming traffic to HTTPS.
  2. Port 443 (HTTPS): Serves the Django application via mod_wsgi and 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:

  1. Install Certbot and its Apache plugin:
    apt install certbot python3-certbot-apache -y
    
  2. 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.

comments powered by Disqus