Django direkt mit Apache ausliefern

In meinem Umfeld kam die Frage auf, wie die Menschen ihre Django-Applikationen ausliefern. Eine gute Gelegenheit mal meinen Workflow "zu Papier" zu bringen. Hier erfährst du, wie du Apache konfigurierst um deine Django-App erreichbar zu machen.

Dauer: ~30 Minuten
Inhalte: Apache2 config, WSGI, static files
Voraussetzungen: Linux mit Apache installiert, Django App (egal wie groß oder klein), Texteditor eigener Wahl, Superuser-Rechte

Voraussetzungen und Pfade in diesem Beispiel

In dieser Anleitung gehe ich davon aus, dass du mit einer Video-Hosting-Plattform in Django berühmt werden möchtest. Deine App heißt MePipe und befindet sich auf einem Linuxserver im Verzeichnis /opt/djangoapps/MePipe innerhalb des App-Verzeichnisses hast du ein config Verzeichnis, dass die wsgi.py Datei enthält. In /opt/venvs/MePipe liegt das Virtual Environment, in das du die benötigten Python Pakete für MePipe installiert hast. Zu guter letzt hast du für static und media files je einen Ordner unter /var/www/MePipe/static bzw. .../media angelegt. Das Apache2-Modul mod_wsgi hast du installiert.

Als Skript formuliert (inklusive Installationen und git clone):

# benötigte Python-Pakete installieren
apt install python3 python3-venv
apt install libapache2-mod-wsgi

# Modul aktivieren falls nicht geschehen
a2enmod wsgi

# Verzeichnisse für Django app anlegen
mkdir -p /opt/djangoapps
mkdir -p /opt/venvs

# Projekt klonen
cd /opt/djangoapps
git clone <hier dein repo>

# venv erstellen
cd /opt/venvs
python3 -mvenv MePipe

source /opt/venvs/MePipe/bin/activate
pip install -r /opt/djangoapps/MePipe/requirements.txt

# Verzeichnisse für static und media files anlegen
mkdir -p /var/www/MePipe/static
mkdir -p /var/www/MePipe/media

Zuletzt solltest du noch deine Django Settings überprüfen und STATIC_ROOT = "/var/www/MePipe/static" und MEDIA_ROOT = "/var/www/MePipe/media" setzen.

Apache2-Config

Lege unter /etc/apache2/sites-available eine neue Datei mit Endung .conf an z.B. 020-mepipe.conf

<VirtualHost *:80>
        ServerName www.mepipe.com
        ServerAlias mepipe.com
        
        RewriteEngine on
        RewriteCond %{SERVER_NAME} =mepipe.com [OR]
        RewriteCond %{SERVER_NAME} =www.mepipe.com
        RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

<VirtualHost *:443>
        ServerName www.mepipe.com
        ServerAlias mepipe.com

        ServerAdmin info@example.com
        
        Alias /favicon.ico /var/www/MePipe/favicon.ico
        Alias /media/ /var/www/MePipe/media/
        Alias /static/ /var/www/MePipe/static/
        
        <Directory /var/www/MePipe/>
                Options -Indexes
                Require all granted
        </Directory>

        WSGIDaemonProcess mepipe python-home=/opt/venvs/MePipe python-path=/opt/djangoapps/MePipe/
        WSGIProcessGroup mepipe
        WSGIScriptAlias / /opt/djangoapps/MePipe/config/wsgi.py
        WSGIApplicationGroup %{RESOURCE}
        <Directory /opt/djangoapps/MePipe/config>
                <Files wsgi.py>
                Require all granted
                </Files>
        </Directory>


        ErrorLog ${APACHE_LOG_DIR}/MePipe_error.log
        CustomLog ${APACHE_LOG_DIR}/MePipe_access.log combined

        SSLEngine on
        SSLCertificateFile      /etc/ssl/certs/ssl-cert-snakeoil.pem
        SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key

</VirtualHost>

Zeilen 1-9 definieren den normalen HTTP Host unter dem die Anwendung erreichbar ist und da es 2022 ist und wir kein HTTP mehr machen (zumindest nicht übers Internet) besteht dieser Block lediglich aus einer Umleitung von http auf https.
Ab Zeile 11 beginnt die Konfiguration für den https-Teil. Die ersten paar Zeilen sind so aus der Beispielkonfiguration geklaut und angepasst und definieren den Hostnamen und eventuelle Zweitnamen untern denen deine Seite erreichbar sein soll. Die Admin E-Mail wird iirc bei Standard-Fehlerseiten angezeigt, die in Django eh überschrieben werden. Vielleicht übersehe ich hier auch etwas.
Spannend wird es ab Zeile 17-19 hier wird definiert, welche Verzeichnisse bzw. Dateien direkt vom Apache ausgeliefert werden und nicht von Django (weil das so viel schneller ist, Korrektur siehe *). Hier siehst du die vorher angelegten Pfade für static und media files. Und als Serviervorschlag noch einen Direktlink zum Favicon.
Zeilen 21 - 24 erlauben dem Apache erst die entsprechenden Ordner zuzugreifen, verbieten aber gleichzeitig ein Directory-Listing (also das jemand mepipe.com/static aufruft und in deinen Dateien stöbern kann).
Jetzt kommt der Teil für den dieser Blogpost überhaupt erst enstanden ist.
In Zeile 26 wird die Python-Prozess-Umgebung definiert. Der erste Parameter ist der Prozess-Name mepipe, python-home ist der Pfad zu deinen Python-Bibliotheken (und gibst du hier ein virtual environment an, wird das benutzt), python-path ist das Working-Directory also der Pfad, auf den sich alle relativen Links innerhalb deiner Anwendung beziehen werden, und zeigt dementsprechend auf das Projektverzeichnis.
Zeile 27 definiert die Gruppe des Prozesses. Ich glaube hier ist die cgroup gemeint, 100% sicher bin ich mir aber nicht. Es schadet jedenfalls nicht, die zu setzen und auch mepipe zu nennen :D
Der Kern dieser Config ist dann Zeile 28. Denn hier wird angegeben welche Route (in diesem Fall /) auf welches WSGI file zeigt, hier /opt/djangoapps/MePipe/config/wsgi.py. Das bedeutet auch, dass du im Prinzip auf mehrere Djangoprojekte verweisen kannst, eins unter /blog, eins unter /shop, eins unter /supersecretsociety und eben eins unter /. Meine Vermutung ist, dass du spezifischere Routen vor allgemeineren in die Config schreiben musst, also /blog vor /
Was Zeile 28 macht entzieht sich meiner Erinnerung, aber ich schleife sie seit meinem ersten Django-Apache-Projekt mit.
Zu guter Letzt erlaubst du dem Apache in Zeilen 29 - 34 noch auf die wsgi.py Datei zuzugreifen.
Der Rest der Datei ist aus der Apacheconf-Beispieldatei und definiert, wo die Logs liegen (Achtung der Ordner muss existieren, die Datei nicht), und welche Zertifikate für SSL verwendet werden sollen. Hier sind die selbstsignierten Standardzertifikate des Servers verlinkt, die definitiv nicht als sicher gelten.

* Korrektur/Anmerkung: Hier bin ich scheinbar einem Mythos aufgesessen, der sich breiter Beliebtheit in der Django-Community erfreut. Ein Twitter-Nutzer machte mich mit einem Link auf eine andere Seite darauf aufmerksam, dass der Performance-Gewinn in den meisten Fällen nicht real ist, weil dafür viele andere Konfigurationen vorgenommen werden müssten. Der entsprechende (englischsprachige) Post befindet sich hier: https://whitenoise.evans.io/en/stable/#isn-t-serving-static-files-from-python-horribly-inefficient Dieser Post ist Teil der Whitenoise Dokumentation und empfiehlt whitenoise, was mich erstmal skeptisch gemacht hat, die Argumente in dem übernächsten Abschnitt haben mich aber abgeholt.
Dementsprechend ist die Begründung warum die Zeilen in der Apache-Config notwendig sind jetzt schlichtweg, weil es irgendwo konfiguriert werden muss, entweder in Django oder in Apache und wir haben gerade eine Apache-Config offen

Zertifikate

Um also fix vernünftige Zertifikate zu bekommen empfehle ich LetsEncrypt zu verwenden. Installiere certbot und führe es mit der entsprechenden Domain aus (Achtung der DNS-Eintrag für IPv4 UND IPv6 sollte derweil schon auf deinen Server zeigen)

pip3 install certbot

certbot -d mepipe.com

Certbot leitet dich interaktiv durch den Prozess und passt deine Apache-Conf an. Den Redirect nach dem Certbot fragt, hast du bereits oben in der Config gesetzt.

Logrotation gefällig?

Dieser Abschnitt ist nur relevant, wenn du die Logs in der Config NICHT ins APACHE_LOG_DIR direkt schreibst, also in ein anderes Verzeichnis oder ein Unterverzeichnis. In dem Fall kannst du so eine Config unter /etc/logrotate.d/apache2_mepipe  ablegen:

/dein/log/pfad/*.log {
        weekly
        missingok
        rotate 8
        compress
        delaycompress
        notifempty
        create 640 root adm
        sharedscripts
        postrotate
                if invoke-rc.d apache2 status > /dev/null 2>&1; then \
                    invoke-rc.d apache2 reload > /dev/null 2>&1; \
                fi;
        endscript
        prerotate
                if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
                        run-parts /etc/logrotate.d/httpd-prerotate; \
                fi; \
        endscript
}

weekly und rotate 8 bedeutet, dass insgesamt 8 Wochen archiviert werden und dann die Archive überschrieben werden. Hier sind alle Kombinationen möglich auch sowas wie weekly, rotate 52 um ein Jahr in Wochen abzudecken oder monthly, rotate 6 für ein halbes Jahr monatsweise, usw.

Deployment

Diesen Abschnitt habe ich komplett vergessen in der initialen Veröffentlichung.

Neben Datenbankmigrationen musst du in Django jedes Mal, wenn sich die statischen Dateien ändern oder neue hinzukommen python3 manage.py collectstatic ausführen. Wenn du das in ein Skript einbettest, füge noch --noinput als Parameter hinzu. Außerdem solltest du die Apache config aktivieren und den Apache neuladen.

Alles in einem Skript:

# Statische Files kopieren
python3 /opt/djangoapps/MePipe/manage.py collectstatic --noinput

# Apache Config aktivieren
a2ensite 020-mepipe.conf

# Apache neuladen
systemctl reload apache2

Zusammenfassung

Wenn das Modul mod_wsgi in Apache installiert und aktiviert ist, kannst du eine Django-Anwendung darüber hosten, indem du die statischen Dateien direkt auslieferst und alles was Django betrifft per WSGIScriptAlias mit einer Route versiehst, die von deiner wsgi.py-Datei bedient wird.

Wenn du Fragen, Anmerkungen oder Wünsche hast, schreib mir gerne eine Mail an info@ oder eine DM auf Twitter. Wenn du mich beim Schreiben dieser Anleitungen unterstützen möchtest, findest du im Menü Links zu ko-fi und Patreon und in anderen Posts auch schonmal Amazon-Affiliate-Links. Wenn dir meine Anleitungen gefallen, kannst du auch unten rechts (vermutlich?!) auf die Figur klicken und deine E-Mail-Adresse da lassen, dann bekommst du die Posts bei Veröffentlichung direkt zugeschickt.