j4n
j4n.e7h.eu
e7h

2024-10-26 18:00:00

Django im Devcontainer mit PostgreSQL-Datenbank und Redis

Die Funktionsweise von Devcontainern habe ich bereits kurz im Artikel Typst im Devcontainer beschrieben. Während in diesem Beispiel die Entwicklungsumgebung aus nur einem Container bestand, zeige ich hier, wie sich komplexere Aufbauten realisieren lassen.

Was werden wir erreichen?

Wir erhalten eine containerisierte Entwicklungsumgebung für ein Django-Projekt, bestehend aus dem eigentlichen Entwicklungscontainer, einer PostgreSQL-Datenbank und einer Redis-Instanz. Dazu bekommen wir geeignete browserbasierte Werkzeuge für Datenbank und Key-Value-Store.

Die Entwicklungsumgebung wird so konstruiert sein, dass sie sowohl als Development Container, als auch in Github Codespaces und DevPod funktionieren wird.

So zeige ich, wie eine schon etwas komplexere Umgebung konstruiert werden kann.

Quellcode auf git.codebau.dev verfügbar

Dieser Projektaufbau hat es schon in sich. Daher stelle ich das komplette Grundgerüst auf git.codebau.dev zur Verfügung.

Was brauchen wir noch gleich?

Als Beispiel dient eine Django-Anwendung, die eine PostgreSQL-Datenbank und Redis als Cache bzw. Queue-System, z. B. für Celery, braucht. Alles in einem Container ist unpraktisch und benötigt einigen Aufwand bei der Erstellung und Konfiguration. Eine der Stärken von Containern liegt darin, dass ein Container konkret eine Aufgabe hat. So brauchen wir also mindestens diese drei verschiedenen Container:

  • Ein Container mit Redis für Cache1
  • Ein Container mit PostgreSQL2
  • Ein Container mit Python zur Django-Entwicklung3

Der zuletzt genannte Container ist der Hauptcontainer für die Anwendung. Alle Container müssen untereinander kommunizieren können. Um das Entwickler-Erlebnis zu verbessern, fügen wir noch zwei weitere Container hinzu:

  • Ein Container mit pgweb zum browserbasierten Zugriff auf die PostgreSQL-Datenbank
  • Ein Container mit redis-commander zum browserbasierten Zugriff auf Redis

Außerdem wollen wir auf verschiedene Ports zugreifen können: Neben der eigentlichen Django-Anwendung natürlich auf die beiden browserbasierten Werkzeuge, sowie auf die PostgreSQL-Datenbank und die Redis-Instanz direkt.

Keine Persistenz von Redis und PostgreSQL!

Es handelt sich hier um eine Entwicklungsumgebung. Außerhalb des Lebenszyklus der Container ist keine Persistenz von Daten in Redis oder der PostgreSQL-DB vorgesehen.

Die Grenzen

Es handelt sich um eine Entwicklungsumgebung zur lokalen Verwendung. Benutzernamen und Passwörter sind schwach. Das, was wir hier bauen, darf niemals ein Deployment für eine produktive Umgebung darstellen.

Nochmal: Es ist und bleibt eine Entwicklungsumgebung.

Der Grundaufbau

Natürlich folgt auch dieses Projekt den Konventionen für einen Development Container. Entsprechend sieht die Grundstruktur ähnlich der Struktur aus, die wir schon aus dem Typst-Artikel kennen.

1DjangoApp/
2├─ .devcontainer/
3│  ├─ devcontainer.json
4│  ├─ ... weitere Dev Container Files ...
5├─ .gitignore
6├─ ... Django Project Files ...
7├─ README.md

Da nun mehr als ein Container verwendet wird, nutzen wir die Mittel von Docker Compose. Entsprechend ist die devcontainer.json nun etwas anders aufgebaut:

 1{
 2    "name": "DjangoApp",
 3    "dockerComposeFile": "docker-compose.yml",
 4    "service": "devcontainer",
 5    "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
 6    "remoteUser": "root",  
 7    "mounts": [],
 8    "customizations": {
 9        "vscode": {
10            "extensions": [
11                "ms-python.python",
12                "ms-python.debugpy",
13                "ms-azuretools.vscode-docker"
14            ]
15        }
16    },
17    "forwardPorts": [
18        8000, 
19        "db:5432",
20        "cache:6379",
21        "pgweb:8081",
22        "commander:8080"
23    ],
24    "portsAttributes": {
25        "8000": {
26            "label": "Django Application"
27        },
28        "db:5432": {
29            "label": "PostgreSQL DB Instance"
30        },
31        "cache:6379": {
32            "label": "Redis Cache Instance"
33        },
34        "pgweb:8081": {
35            "label": "pgWeb"
36        },
37        "commander:8080": {
38            "label": "Redis Commander"
39        }
40    }
41}

Neben dem Projektnamen in Zeile 2 wird nun als erstes auf die docker-compose.yml (Zeile 3) verwiesen. Mit dem exakten Inhalt dieser Datei befassen wir uns gleich. Zunächst ist hier noch Zeile 4 wichtig. Sie gibt an, welcher der Container aus der docker-compose.yml nun der Container ist, der für die eigentliche Software- Entwicklung verwendet wird.

Die Angabe unter workspaceFolder in Zeile 5 gibt, wie im Typst-Beispiel, an, an welcher Stelle im Container wir unseren »Code« bearbeiten wollen. Ich verwende hier wieder die Standard-Angabe.

Die Zeilen 8-16 geben an, welche Extensions wir nutzen wollen, wenn Visual Studio Code als IDE zum Einsatz kommen soll. Für die meisten Python-Projekte genügen mir persönlich die ersten beiden genannten Erweiterungen. Mit ihnen kann ich Python-Code editieren, ausführen und debuggen. Die dritte Erweiterung unterstützt bei der Erstellung von Docker-Dateien (Dockerfile, docker-compose.yml).

Der nächste Teil der devcontainer.json beschreibt die aus den Containern weitergeleiteten Ports. Die Zeilen 17-23 benennen diese Ports zunächst. Hier fällt auf, dass ein Port wie gewohnt als einfache Nummer angegeben ist. Es ist das Port, unter dem die Django-Applikation im Browser erreichbar sein wird. Bei Django-Projekten ist das standardmäßig das Port 8000. Die anderen Ports sind als String mit einem Namen davor angegeben. Diese Namen begegnen uns im nächsten Abschnitt in der docker-compose.yml wieder. Im Ergebnis bedeutet das, dass diese Ports aus anderen Containern als dem eigentlichen Entwicklungscontainer zum Entwicklerrechner weitergeleitet werden.

Zur besseren Übersicht geben die Zeilen 24-41 noch jedem weitergeleiteten Port ein Label. So kann man in der »Ports-Übersicht« in Visual Studio Code nachher nachvollziehen, welches Port welchen Zweck hat.

Ins Detail: docker-compose.yml

Im Verzeichnis .devcontainer, direkt neben der Datei devcontainer.json, liegt die Datei docker-compose.yml. Sie hat den folgenden Inhalt:

 1version: '3.8'
 2
 3services:
 4  devcontainer:
 5    build:
 6      context: .
 7      dockerfile: Dockerfile
 8    command: sleep infinity
 9    volumes:
10      - ../..:/workspaces:cached
11  db:
12    image: postgres:alpine
13    restart: always
14    environment:
15      POSTGRES_PASSWORD: developer
16      POSTGRES_USER: developer
17  cache:
18    image: redis:alpine
19    restart: always
20  pgweb:
21    image: ghcr.io/sosedoff/pgweb:latest
22    restart: always
23    environment:
24      DATABASE_URL: postgres://developer:developer@db:5432/developer?sslmode=disable
25    depends_on:
26      - db
27  commander:
28    image: ghcr.io/joeferner/redis-commander:latest
29    restart: always
30    environment:
31      REDIS_HOSTS: Django-Cache:cache:6379:10
32      PORT: 8080
33    depends_on:
34      - cache
35    user: redis

Hier finden wir alle Container wieder, deren Namen wir zuvor bereits gesehen haben. Doch der Reihe nach:

Die Zeilen 4-10 beschreiben den eigentlichen Entwicklungscontainer für das Django-Projekt. Da wir hier noch Anpassungen am Standard-Image vornehmen wollen, verweisen wir auf einen Dockerfile, um den wir uns im nächsten Abschnitt kümmern.

Die Zeilen 11-16 definieren den Container für die PostgreSQL-Datenbank, basierend auf dem offiziellen Docker-Image2. Als Umgebungsvariablen werden ein Benutzername und ein Kennwort definiert. Beides brauchen wir für die Konfiguration im Django-Projekt, aber auch für den »pgweb«-Container, um den es gleich noch gehen wird.

Die Zeilen 17-19 definieren den Container für das Redis Key-Value-Store. Es wird auch hierfür das offizielle Docker-Image1 genutzt. In der Standard-Konfiguration benötigt Redis keine Zugangsdaten, daher müssen keine weiteren Informationen angegeben werden.

Die Zeilen 20-26 bringen pgweb als Tool für den browserbasierten Zugriff auf die PostgreSQL-Datenbank ins Spiel. Durch die Angabe der Verbindungs-URL als Umgebungsvariable lässt sich das Tool komplett vorkonfigurieren (Zeile 24). Hier finden sich die Zugangsdaten zur PostgreSQL-Datenbank.

In den Zeilen 27-35 wird schlussendlich das Tool für den browserbasierten Zugriff auf Redis bereitgestellt. Auch hier findet die Konfiguration über die Umgebungsvariablen statt.

Eine Ebene tiefer: Dockerfile

Für den eigentlichen Entwicklungscontainer brauchen wir das Standard-Python-Docker-Image3 und passen es entsprechend an. Das Dockerfile liegt ebenfalls im Verzeichnis .devcontainer, neben den Dateien devcontainer.json und docker-compose.yml. Der Inhalt:

 1FROM python:3-alpine
 2
 3RUN apk update && apk upgrade
 4RUN apk add git gpg wget curl bash bash-completion shadow nano starship openssh-client openssh-sftp-server sqlite sqlite-dev
 5
 6RUN chsh -s /bin/bash
 7RUN echo 'eval "$(starship init bash)"' >> ~/.bashrc
 8
 9RUN python3 -m pip install -U pip
10RUN python3 -m pip install -U pipx
11RUN python3 -m pipx ensurepath
12RUN pipx install poetry

Das kompakte Alpine-basierte Image ist eine gute Basis für Django-Projekte (Zeile 1). Die Zeilen 3-4 installieren eine Reihe an benötigten Standard-Paketen. Zeilen 6-7 ändern die Standard-Shell auf bash und aktivieren starship4. Beides kennen wir aus dem Typst-Projekt schon. Für mich erhöht starship einfach den Komfort auf dem Terminal des Entwicklungscontainers.

Ich bevorzuge für meine Python-Projektverwaltung das Tool poetry5. Dies muss zunächst installiert werden. Dazu wird in den Zeilen 9-12 pip aktualisiert, pipx installiert, pipx angewiesen, den PATH anzupassen (sofern erforderlich), und letztlich poetry zu installieren.

Fertig ist der Container.

Wie geht es weiter?

Nun kann man das Projekt z. B. mit Visual Studio Code nun »im Devcontainer« öffnen. Ist dies gelungen, kann man im VS Code Terminal das Projekt mit poetry init starten. Auf die weitere Projektkonfiguration gehe ich im Rahmen dieses Posts nicht weiter ein. Damit aber das frisch angelegte Django-Projekt die PostgreSQL-Datenbank nutzt, muss das Python-Package psycopg6 installiert werden. Dann kann man die DB-Konfiguration in der settings.py so anpassen:

 1DATABASES = {
 2    "default": {
 3        "ENGINE": "django.db.backends.postgresql",
 4        "OPTIONS": {
 5            "pool": True,
 6        },
 7        "NAME": "developer",
 8        "USER": "developer",
 9        "PASSWORD": "developer",
10        "HOST": "db",
11        "PORT": 5432,
12    },
13}

Außerdem soll Redis genutzt werden. Entsprechend wird die settings.py um die Konfiguration für das Cache und das Session-Handling erweitert, nachdem das Python-Package django-redis installiert wurde:

 1CACHES = {
 2    'default': {
 3        'BACKEND': 'django_redis.cache.RedisCache',
 4        'LOCATION': 'redis://cache:6379/10',
 5        'OPTIONS': {
 6            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
 7        }
 8    }
 9}
10
11SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
12SESSION_CACHE_ALIAS = 'default'

Im Beispiel hier wird die DB Nr. 10 der Redis-Datenbank in dem Container »cache« verwendet.

Jetzt ist alles bereit für die Entwicklung. Viel Spaß!

Codespaces-Kompatibilität

Obwohl hier mehrere Container verwendet werden: Die nun erstellte Konfiguration kann auch mit GitHub-Codespaces verwendet werden.


  1. Wir werden das offizielle Redis-Docker-Image verwenden. ↩︎ ↩︎

  2. Wir werden das offizielle PostgreSQL-Docker-Inage verwenden. ↩︎ ↩︎

  3. Wir werden das offizielle Python-Docker-Image für unsere Zwecke anpassen und etwas erweitern. ↩︎ ↩︎

  4. Siehe starship.rs↩︎

  5. Siehe python-poetry.org↩︎

  6. Bitte nicht mit psycopg2 verwechseln – psycopg ist der Nachfolger von psycopg2. Siehe dazu psycopg.org↩︎




Zuletzt geändert: 2024-10-29 21:34:11