Der App Store ist ein Web-System, in dem Studierende und Dozierende vorgefertigte Cloud-Apps (Packer + Terraform in einem Git-Repo) per Klick auf OpenStack ausrollen. In Prod läuft alles auf einer einzelnen Ubuntu-VM in zehn Containern: nginx als TLS-Terminator, Vue-Frontend, FastAPI-Backend, Celery-Worker, Keycloak als Identity Provider sowie PostgreSQL (App + Terraform-State + Keycloak), RabbitMQ und Redis als Infrastruktur. Alle Service-Images werden zur Laufzeit aus GHCR gezogen — auf der VM wird nichts gebaut. Diese Anleitung führt von einer leeren Ubuntu-VM bis zum eingeloggten Browser unter https://<VM-IP>.
Tip
Im CI-Betrieb läuft der Staging-Stack automatisch über
infrastructure/ansible/staging.yml. Diese Anleitung beschreibt den
manuellen Weg für eine isolierte Prod-Instanz auf einer eigenen VM,
die du per IP erreichst.
| Werkzeug | Version | Anmerkung |
|---|---|---|
| Ubuntu Server | 22.04 LTS | Auch 24.04 funktioniert |
| Docker Engine | 24.x oder neuer | Compose v2 ist enthalten |
| Docker Compose | v2 (docker compose, nicht docker-compose) |
Über Docker Engine bereits dabei |
| Git | 2.x | |
| Python 3 | 3.11+ | Wird einmalig zum Generieren der Secrets gebraucht |
| OpenSSL | 3.x | Für das Self-signed-Zertifikat (Schritt 3) |
| GNU Make | Pflicht | Alle Schritte sind als make-Targets ausgelegt — wer kein Make hat, kann die zugrunde liegenden Befehle direkt aus dem Makefile ablesen |
| VM-IP | z. B. 203.0.113.42 |
Die öffentliche IP, unter der die VM erreichbar ist |
Ein GitHub Personal Access Token mit repo-Scope ist Pflicht — der Worker klont damit private App-Repos und das Backend verifiziert GitHub-Hooks.
Note
<VM-IP> ist in dieser Anleitung ein Platzhalter — überall durch die echte IP ersetzen (z. B. 141.72.12.185). In Bash würde <VM-IP> als Redirect interpretiert und mit syntax error near unexpected token brechen.
Ein frisches Ubuntu-Cloud-Image hat weder Docker noch Make installiert. Beides nachholen:
sudo apt update
sudo apt install -y git make ca-certificates curl gnupgDocker Engine + Compose v2 aus dem offiziellen Docker-Repo (Ubuntus docker.io-Paket ist meist mehrere Versionen hinter dem, was Compose v2 voraussetzt):
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginDen aktuellen User in die docker-Gruppe legen, damit Docker-Befehle ohne sudo laufen — danach neu einloggen (exit + ssh …), sonst greift die Gruppenmitgliedschaft noch nicht:
sudo usermod -aG docker $USER
exitNach dem Wiedereinloggen Smoke-Test:
docker --version
docker compose version
make --versionsudo mkdir -p /opt/app-store
sudo chown $USER:$USER /opt/app-store
cd /opt/app-store
git clone https://github.com/six7-click-n-deploy/deployment
cd deploymentAlle weiteren Befehle werden aus /opt/app-store/deployment ausgeführt — dort liegen Makefile, docker-compose.prod.yml, das nginx/-Verzeichnis und der Keycloak-Realm-Export. frontend/, backend/ und worker/ werden in Prod nicht geklont, weil die Images aus GHCR gezogen werden.
Important
Alle make-Targets in dieser Anleitung müssen aus /opt/app-store/deployment laufen — Make sucht das Makefile im aktuellen Verzeichnis. Wenn der Prompt ubuntu@prod-test:~$ zeigt (Home-Verzeichnis), kommt make: *** No rule to make target '…'. Stop.. Vorher cd /opt/app-store/deployment.
Tip
Auf Anfrage stellen wir eine fertig befüllte .env bereit. In dem Fall genügt es, die Datei direkt nach deployment/.env zu legen, alle Vorkommen von <VM-IP> durch die IP der VM zu ersetzen, auf der der App Store läuft — und mit Schritt 3 weiterzumachen. Die folgenden Unterpunkte 2a–2j sind dann nicht nötig.
docker-compose.prod.yml markiert sämtliche kritischen Werte als ${VAR:?... is required} — der Stack startet erst, wenn jeder Pflicht-Wert gesetzt ist. Anders als in Dev gibt es keine .env.example mit Defaults; jede Variable muss explizit gefüllt werden.
touch .env
chmod 600 .envDie folgenden Felder in .env eintragen. Reihenfolge ist egal, Kommentare mit # sind erlaubt.
Im Folgenden verwende ich <VM-IP> als Platzhalter — ersetze ihn überall durch deine tatsächliche IP-Adresse, z. B. 203.0.113.42. Wenn du auf derselben Maschine testest, kann auch localhost stehen.
Drei voneinander isolierte Postgres-Instanzen — Anwendung, Terraform-State (Worker), Keycloak. Jede bekommt eigene Credentials. Passwörter generieren mit openssl rand -base64 24.
DB_USER=appstore
DB_PASSWORD=<random>
DB_NAME=appstore
TFSTATE_DB_USER=tfstate
TFSTATE_DB_PASSWORD=<random>
TFSTATE_DB_NAME=tfstate
KEYCLOAK_DB_USER=keycloak
KEYCLOAK_DB_PASSWORD=<random>
KEYCLOAK_DB_NAME=keycloak
RABBITMQ_USER=appstore
RABBITMQ_PASSWORD=<random>
RABBITMQ_VHOST=/
KEYCLOAK_ADMIN_USER=admin
KEYCLOAK_ADMIN_PASSWORD=<random>
Der Admin-User wird beim ersten Keycloak-Boot im master-Realm angelegt — danach lässt sich das Passwort ohne DB-Reset nicht mehr ändern. Den Wert gut sichern.
Symmetrischer Schlüssel für die Backend-JWTs. In Dev hat dieser Wert einen Default — in Prod muss er explizit gesetzt werden.
python3 -c 'import secrets; print(secrets.token_hex(32))'SECRET_KEY=<output>
Symmetrischer Fernet-Key, den Backend und Worker teilen, um OpenStack-Credentials zu ver-/entschlüsseln. Beim Container-Start wird er zwingend geprüft — fehlt er, bricht docker compose up mit der Meldung CREDENTIAL_ENCRYPTION_KEY is required ab.
Generieren:
python3 -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'Die Ausgabe — ein url-safe-base64-String, der mit = endet — als CREDENTIAL_ENCRYPTION_KEY in die .env eintragen.
Falls cryptography lokal nicht installiert ist:
pip install cryptographyDas Secret kann erst nach dem ersten Keycloak-Start aus dem Admin-UI geholt werden. Für den Erst-Start in .env einfach einen Platzhalter eintragen:
KEYCLOAK_CLIENT_SECRET=changeme
In Schritt 6 wird der echte Wert nachgetragen.
GitHub Personal Access Token mit repo-Scope. Wird vom Worker beim Klonen privater App-Repos und vom Backend für Hook-Verifikation verwendet. Anlegen unter: GitHub → Settings → Developer settings → Personal access tokens (classic) → Generate new token.
GIT_ACCESS_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Alle URLs zeigen auf deine VM. nginx terminiert HTTPS auf 443 und routet /api an das Backend, /realms und /admin an Keycloak, alles andere an das Frontend — deshalb laufen alle drei VITE_*_URL-Werte über dieselbe Origin:
APP_BASE_URL=https://<VM-IP>
CORS_ORIGINS=https://<VM-IP>
VITE_APP_URL=https://<VM-IP>
VITE_API_URL=https://<VM-IP>/api
VITE_KEYCLOAK_URL=https://<VM-IP>
E-Mail-Benachrichtigungen (Approval-Workflow). Wenn nicht gebraucht, einfach SMTP_ENABLED=false lassen und die anderen Felder leer. Für Gmail ein App-Password verwenden, nicht das Account-Passwort.
SMTP_ENABLED=false
SMTP_HOST=smtp.gmail.com
SMTP_PORT=465
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=
SMTP_FROM_NAME=Click-n-Deploy
In Dev terminiert das Frontend HTTP direkt; in Prod sitzt nginx-prod davor und erwartet zwei TLS-Dateien unter nginx/certs/. Generiere beides mit einem Make-Target:
make prod-cert-self-signed PROD_HOST=<VM-IP>Das Target legt das Verzeichnis an, erzeugt ein 10 Jahre gültiges Zertifikat mit CN=<VM-IP> und subjectAltName=IP:<VM-IP>,DNS:<VM-IP>, setzt die richtigen Permissions und gibt das Ablaufdatum aus. Resultat:
nginx/certs/cert.pem # Public cert
nginx/certs/key.pem # Private key, 600
Browser zeigen beim ersten Aufruf eine Sicherheitswarnung („Verbindung nicht sicher") — Ausnahme einmal bestätigen und gut.
make prod-upAnders als in Dev wird hier nichts gebaut — das Target lädt zunächst alle :latest-Images aus GHCR (pull_policy: always) und startet danach die zehn Container: nginx, frontend, backend, worker, keycloak, keycloak-postgres, postgres, postgres-tfstate, redis, rabbitmq. Erstdurchlauf dauert je nach Bandbreite 2–5 Minuten.
Status prüfen:
make prod-psAlle zehn Container sollten running (healthy) melden, sobald die Healthchecks durchlaufen sind. Falls ein Container im Crash-Loop steckt, gezielt die Logs anschauen (in Prod gibt es keine dev-logs-<svc>-Aliasse, der Dienst wird per SVC= mitgegeben):
make prod-logs SVC=backend
make prod-logs SVC=keycloakBevor Schritt 5 läuft, sicherstellen dass Keycloak fertig hochgefahren ist:
make prod-logs SVC=keycloak | grep -i "listening on\|started in"Erwartete Zeilen:
Listening on: http://0.0.0.0:8080
... started in XX.XXXs
Ctrl-C beendet das Tail.
Anders als in Dev sind Migrationen in Prod kein Teilschritt des Seeds, sondern ein eigenes Target — eine Migrate-Init-Container-Variante würde Fehler im Compose-Output verstecken. Schema anlegen:
make prod-migrateSeed-Daten einspielen (Realm-Import, Test-User, Beispiel-Apps, Kurse):
make prod-seedDas Skript ist idempotent — wiederholtes Ausführen schadet nicht und ist bei Timing-Problemen die richtige Antwort.
Der mit make prod-seed importierte Realm-Export stammt aus einer Dev-Umgebung und enthält hartcodierte http://localhost:3000 / localhost:5173 / localhost:8000 als Redirect-URIs, Web-Origins und Post-Logout-URIs der beiden Clients. Ohne diesen Schritt lehnt Keycloak jeden Login/Logout mit Invalid redirect uri ab.
make prod-set-keycloak-urlsLiest APP_BASE_URL aus der .env, loggt sich als Admin bei Keycloak ein und patcht für die beiden Clients:
appstore-frontend:redirectUris,webOrigins,post.logout.redirect.urisappstore-backend:rootUrl,adminUrl,redirectUris,webOrigins
Idempotent — kann beliebig oft laufen. Erneut aufrufen, wenn sich APP_BASE_URL ändert (z. B. Umstieg von IP auf Domain).
Note
Bewusst kein Teil von prod-seed: wer in Keycloak manuell zusätzliche Redirect-URIs ergänzt hat (z. B. fürs lokale Testen vom Mac aus gegen die Prod-VM), würde die sonst bei jedem Seed-Lauf verlieren. Dieses Target überschreibt die Listen explizit — Aufruf ist eine bewusste Entscheidung.
Der mit Schritt 5 importierte Realm bringt den appstore-backend-Client mit dem maskierten Secret ********** aus dem Realm-Export mit. Das ist kein gültiger Wert — das echte Secret muss in Keycloak einmalig neu erzeugt und in die .env übernommen werden.
Anders als in Dev entfällt der Vorlauf mit make keycloak-disable-ssl — nginx terminiert HTTPS schon, der master-Realm akzeptiert die Admin-UI direkt.
-
https://<VM-IP>/adminim Browser öffnen (Cert-Warnung akzeptieren). -
Mit
KEYCLOAK_ADMIN_USER/KEYCLOAK_ADMIN_PASSWORDaus Schritt 2d einloggen. -
Oben links im Realm-Switcher von
masteraufdhbwumstellen. -
Links auf "Clients" →
appstore-backendöffnen. -
Reiter "Credentials" → Button "Regenerate" klicken (das Feld zeigt vorher buchstäblich
**********— das ist die Maskierung aus dem Realm-Export, kein nutzbarer Wert). -
Den neu generierten Wert kopieren und in
.enveintragen:KEYCLOAK_CLIENT_SECRET=<kopierter-wert> -
Backend neu starten (in Prod gibt es keinen
dev-restart-backend-Alias, der Dienst wird perSVC=mitgegeben):make prod-restart SVC=backend
Ab jetzt kann das Backend Tokens validieren.
In Dev gibt es make health, das drei Endpoints curlt — in Prod fehlt das Target, weil nginx HTTPS mit Self-signed-Cert terminiert. Stattdessen einzeln per curl -k (ignoriert die Cert-Warnung) oder im Browser prüfen:
curl -k https://<VM-IP>/health
curl -k https://<VM-IP>/api/health
curl -k https://<VM-IP>/realms/dhbw/.well-known/openid-configuration | headOder im Browser einzeln öffnen:
| Dienst | URL | Erwartet |
|---|---|---|
| Frontend | https://<VM-IP> |
Click-n-Deploy Login-Seite |
| Backend Swagger | https://<VM-IP>/api/docs |
OpenAPI-UI mit Endpoints /users, /apps, /deployments |
| Backend Health | https://<VM-IP>/api/health |
{"status":"ok"} |
| Keycloak Admin | https://<VM-IP>/admin |
Keycloak Welcome, Login mit KEYCLOAK_ADMIN_USER / KEYCLOAK_ADMIN_PASSWORD aus Schritt 2d |
| Keycloak OIDC Discovery | https://<VM-IP>/realms/dhbw/.well-known/openid-configuration |
JSON mit issuer: https://<VM-IP>/realms/dhbw |
RabbitMQ-UI und pgAdmin sind in Prod nicht über nginx exponiert (kein Port-Mapping nach außen); für Debugging per Container-Shell oder SSH-Tunnel zugreifen.
Im Browser https://<VM-IP> öffnen, "Login" klicken, mit einem der folgenden Test-User anmelden. Passwort ist für ALLE Seed-User 1234.
Administrator
| Passwort | Rolle | |
|---|---|---|
tobias.admin@dhbw.de |
1234 |
admin |
Dozierende
| Passwort | |
|---|---|
michael.eichberg@dhbw.de |
1234 |
henning.pagnia@dhbw.de |
1234 |
sarah.detzler@dhbw.de |
1234 |
frank.hubert@dhbw.de |
1234 |
andrea.bauer@dhbw.de |
1234 |
thomas.wagner@dhbw.de |
1234 |
Studierende
| Passwort | Kurs | |
|---|---|---|
luca.baeck@dhbw.de |
1234 |
WI SE B 23 |
felix.erhard@dhbw.de |
1234 |
WI SE B 23 |
okan.soenmez@dhbw.de |
1234 |
WI SE B 23 |
monika.piano@dhbw.de |
1234 |
WI SE B 23 |
anna.schulz@dhbw.de |
1234 |
WI SE B 24 |
jan.krueger@dhbw.de |
1234 |
INF TI 23 |
Hinweise zur E-Mail-Konvention: <vorname>.<nachname>@dhbw.de, alles klein, Umlaute werden ersetzt (ä → ae, ö → oe, ü → ue, ß → ss). Der Keycloak-Admin (KEYCLOAK_ADMIN_USER / KEYCLOAK_ADMIN_PASSWORD) ist NICHT identisch mit den Realm-Usern — er funktioniert nur unter https://<VM-IP>/admin, nicht im Frontend.
make prod-downContainer weg, Volumes (DBs, Keycloak-Daten, RabbitMQ) bleiben. Beim nächsten make prod-up startet alles im Zustand vor dem Stopp.
make prod-stop # stoppt, Container bleiben
make prod-up # läuft wiederIn Prod gibt es keine pro-Dienst-Aliasse wie in Dev — der Dienst wird per SVC= mitgegeben:
make prod-restart SVC=backend
make prod-restart SVC=frontend
make prod-restart SVC=workermake prod-pullZieht alle :latest-Images neu und rekreiert die Container, deren Image sich geändert hat. In Dev gibt es kein Pendant, weil dort lokal gebaut wird.
In Prod ebenfalls per SVC=…:
make prod-logs # alle
make prod-logs SVC=backend # nur Backend
make prod-logs SVC=worker # nur Worker
make prod-logs SVC=keycloak # nur Keycloak
make prod-logs SVC=nginx # nur nginx (TLS-Terminator)make prod-resetStoppt den Stack und löscht alle Volumes — irreversibel. Anders als make seed-reset in Dev wird nicht automatisch neu gemigriert und geseedet; danach von Schritt 4 an wieder durchlaufen (prod-up → prod-migrate → prod-seed). Den Wert für KEYCLOAK_CLIENT_SECRET muss man dabei erneut über die Admin-UI regenerieren (Schritt 6), weil der Realm aus dem Export wieder mit dem maskierten Platzhalter importiert wird.