Es hat mich genervt, das es für Django nur komplexe Compose-Files gibt, welche zusätzlich noch eine Docker-Datei mitbringen. Das ist eigentlich gar nicht nötig, und macht dann mit Dockge auch richtig Spaß.
Grundlagen
Zuallererst einmal ein paar kurze Grundlagen zum publishen von Django-Projekten:
Wenn man den Debug-Modus abschaltet, also das Projekt Live betreibt, dann müssen statische Dateien über einen Webserver laufen. Das Django-Projekt selber läuft entweder über ein Apache-Plugin, oder über Gunicorn, also ein Server der die WSGI-Schnittstelle versteht, welche Django (oder allgemeiner Python) erfordert.
Also man verwendet entweder den Apache Webserver mit WSGIPlugin (mod_wsgi), oder z.B Nginx Webserver und Gunicorn dahinter als WSGI-Server. Natürlich könnte man auch die statischen Dateien über WSGI laufen lassen (da gibts ein paar Patches für Django), aber das ist im höchsten Maße ineffizient, da für jede Datei ein Prozess geöffnet wird. Mit ein paar Besuchern kann man so den Server schnell auslasten. Ich werde mich hier auf die Kombi NginX und Gunicorn konzentrieren, da mir diese als die performanteste Lösung erscheint.
NginX und Compose
NginX lässt sich nicht besonders gut mit Compose verwenden, aber wenn man denn weiß das man auch innerhalb von Compose, weitere Dateien übersichtlich erstellen kann, dann stellt das keine große Hürde dar. Grob gesagt funktioniert das so:
nginx:
image: nginx:alpine
volumes:
- ./app:/data/app
- ./nginx:/etc/nginx/conf.d
ports:
- 8080:8080
environment:
NGINX_CONFIG: |
server {
listen 8080;
server_name ${SERVER_NAME};
location /static/ {
root /data/app;
}
location /media/ {
root /data/app;
}
}
NGINX_CONF_FILE: /etc/nginx/nginx.conf
command: |
sh -c "echo \"$$NGINX_CONFIG\" > /etc/nginx/conf.d/redir.conf
nginx -g \"daemon off;\""
Wir erstellen in yaml einen NginX-Bereich vom nginx-Image, und ein paar Volumes für die App und der Konfigdatei (für späteren Zugriff). Der Port ist ebenso wichtig. Nun kommt der interessante Teil:
Im Bereich Environment erstellen wir eine neue Variable "NGINX_CONFIG", welche dann die Datei enthält. Um Mehrzeilig in yaml zu schreiben verwendet man Pipe. Den Dateinamen speichern wir ebenfalls als Env-Variable ab. Nun fügen wir unter "command" einen mehrzeiligen Kommandobefehl hinzu, welcher die Datei aus der Env-Variable erstellt, und den NginX-Daemon startet.
Erwähnenswert hier ist noch das maskierte Dollar-Zeichen, da sonst der Composer die Variable einsetzen würde, und nicht die dann auszuführende Shell (sh)
Einstellungen separat verwalten
Man kann alle Einstellungen separat von Compose, und auch separat von Django verwalten. Bei Compose helfen Env-Variablen, welche man mit ${Variable} ansprechen kann (oben im Beispiel ist bereits ${SERVER_NAME} zu sehen. Die Variablen selber kann man in einer .env-Datei im gleichen Verzeichnis wie die compose.yaml unterbringen. Sie enthält zeilenweise "Schlüssel=Wert" Einträge. In Django, lässt sich sowas ebenfalls realisieren. Dazu einfach mal das Projekt: Python Decouple anschauen, welches eine solche Trennung einfach ermöglicht. Ich habe im folgenden Beispiel die Django-Einstellungen und die Compose-Einstellungen in eine .env-Datei geschrieben. Diese wird bei der Installation einfach über die originale Django-env-Datei von Decouple gelegt. und schon kann man Django für compose auch unabhängig von anderen Einsatzfällen konfigurieren.
Das vollständige Beispiel
docker-compose.yml
version: "3.8"
services:
django_gunicorn:
container_name: ${COMPOSE_PROJECT_NAME}-gunicorn
image: tiangolo/uvicorn-gunicorn-fastapi:python3.11
volumes:
- ./app:/data/app
- ./cache:/data/cache
- ./.env:/data/app/.env
ports:
- ${UNICORN_PORT}:8000
working_dir: /data/app
restart: unless-stopped
command: |
sh -c "pip install -r requirements.txt --cache-dir=/data/cache/pip
python manage.py migrate
gunicorn ${DJANGO_PROJECT}.wsgi:application --bind 0.0.0.0:8000"
nginx:
container_name: ${COMPOSE_PROJECT_NAME}-nginx
image: nginx:alpine
volumes:
- ./app:/data/app
- ./cache:/data/cache
- ./nginx:/etc/nginx/conf.d
ports:
- ${NGINX_PORT}:8080
environment:
NGINX_CONFIG: |
server {
listen 8080;
server_name ${SERVER_NAME};
location /static/ {
root /data/app;
}
location /media/ {
root /data/app;
}
location / {
proxy_set_header Host $$host;
proxy_set_header X-Real-IP $$remote_addr;
proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
proxy_pass http://${COMPOSE_PROJECT_NAME}-gunicorn:8000/;
}
}
NGINX_CONF_FILE: /etc/nginx/nginx.conf
NGINX_CONF_SRV_FILE: /etc/nginx/conf.d/redir.conf
command: |
sh -c "echo \"$$NGINX_CONFIG\" > /etc/nginx/conf.d/redir.conf
alias nrestart='nginx -s reload -c $$NGINX_CONF_FILE'
nginx -g \"daemon off;\""
networks: {}
.env:
## Composer
COMPOSE_PROJECT_NAME=djangoalbum
DJANGO_PROJECT=Djangoalbum
## Composer NginX
SERVER_NAME=localhost
RPROXY_SERVER_NAME=localhost
NGINX_PORT=8080
NGINX_PORT_S=8080
## Composer Unicorn
UNICORN_PORT=8000
## Django Project
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'xxxx'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = 'localhost'
CSRF_TRUSTED_ORIGINS = 'http://localhost:8080'
DEFAULT_FROM_EMAIL = 'x@y.z'
EMAIL_HOST = 'mail.x.net'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'x@y.z'
EMAIL_HOST_PASSWORD = 'geheim'
EMAIL_USE_TLS = True
EMAIL_USE_SSL = False
Das meiste habe ich ja bereits im Zuge des Artikels erläutert, heir aber noch ein paar Anmerkungen:
container_name
kann man die Namen der Compose-Bestandteile definieren, praktisch bei mehreren Projekten. Der Name darf hier aber nur aus Kleinbuchstaben + "-" bestehen.
cache und Libraries
ich habe python-pip mitgecached (mit --cache-dir), das beschleunigt den Bau des images, da nicht erneut aus dem Internet geladen werden muss, gilt es ein Paket zu installieren. mit "pip install -r requirements.txt" kann man wie in Python üblich zugehörige Libraries und Tools nachinstallieren.
NginX
Die NginX-Konfig erzeugt einen Webserver für "/static" und "/media", wie für Django benötigt. Unter "/" läuft ein reverse-Proxy, welcher alle Anfragen von Gunicorn beantworten lässt.
Env-Datei
Wie gesagt, diese liegt im gleichen Verzeichnis, und wird von compose automatisch bevorzugt angezogen (man kann auch "normale" Env-Variablen verwenden.)
Im Falle von Django wird die Datei in das Verzeichnis gespiegelt (siehe in der gunicorn-Konfig "- ./.env:/data/app/.env"), so kann Django problemlos auf die gleiche Datei zugreifen. Ähm fasst problemlos, ich musste zur initialisierung von Decouple folgendes in die Settings.py einbauen:
CONF_DIR = os.path.join(BASE_DIR, '.env')
config = Config(RepositoryEnv(CONF_DIR))
Das DjangoProjekt anbinden
Das Projekt selber wird einfach in das Verzeichnis "app" kopiert, dort wird es dann von compose verarbeitet.
Ich hoffe diese Gedanken haben euch weitergeholfen, und ja ich setze diese Lösung produktiv ein. Wie eingangs erwähnt, mit Dockge (Docker UI wie Portainer), kann man besonders einfach diese Art von Konfiguration verwalten.
cheers,
VoSs3o0