Zum Hauptinhalt springen
Sachbearbeiter-Cockpit

Deployment

Ein gebautes Sachbearbeiter-Cockpit ist eine statische SPA plus zwei Dinge, die es zur Laufzeit braucht: die authConfig.json und einen Proxy auf die ActaNova-API. Beides muss pro Umgebung anders sein können, ohne neu zu bauen — sonst brauchst du für Test und Produktion zwei Images.

Diese Seite beschreibt den Weg, den das Demo-Projekt geht: ein nginx-Container, dessen Proxy-Ziel aus einer Umgebungsvariable kommt.

Die Idee in einem Bild

docker run -e ACTANOVA_BASE_URL=https://actanova.example.com …


Container-Start: envsubst rendert die NGINX-Vorlage
/etc/nginx/templates/default.conf.template
│ ${ACTANOVA_BASE_URL} wird eingesetzt

/etc/nginx/conf.d/default.conf → nginx startet

Damit ist ein Image für alle Umgebungen gültig. Was sich unterscheidet, sind nur die Umgebungsvariable und die gemountete authConfig.json.

Das Dockerfile

Zwei Stufen: bauen mit Node, ausliefern mit nginx.

# ---- Build stage ----
FROM node:22-alpine AS build
WORKDIR /app

# Manifeste zuerst -> Layer-Caching für `npm ci`.
# .npmrc setzt die @gentics-Registry (kein Token), muss aber vorhanden sein.
COPY package.json package-lock.json .npmrc ./
RUN npm ci

COPY . .
RUN npm run build # tsc -b && vite build -> /app/dist

# ---- Runtime stage ----
FROM nginxinc/nginx-unprivileged:1.27-alpine
USER root

COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx/default.conf.template /etc/nginx/templates/default.conf.template

# OpenShift startet Container mit ZUFÄLLIGER UID, aber immer in Gruppe GID 0.
RUN chgrp -R 0 /etc/nginx /var/cache/nginx /tmp /usr/share/nginx/html \
&& chmod -R g=u /etc/nginx /var/cache/nginx /tmp /usr/share/nginx/html

ENV ACTANOVA_BASE_URL=""
ENV NGINX_ENVSUBST_FILTER=ACTANOVA_

USER nginx
EXPOSE 8080

Vier Entscheidungen darin sind erklärungsbedürftig:

nginx-unprivileged statt nginx

Das Image lauscht per Default auf 8080 statt 80 und läuft als Nicht-Root-User. Damit ist der Container ohne NET_BIND_SERVICE-Capability lauffähig — Voraussetzung für restriktive Plattformen wie OpenShift.

NGINX_ENVSUBST_FILTER=ACTANOVA_

Der Entrypoint des nginx-Images rendert beim Start alle Dateien aus /etc/nginx/templates/*.template per envsubst nach /etc/nginx/conf.d/. Ohne Filter würde envsubst jede $…-Stelle ersetzen — und damit auch nginx-eigene Variablen wie $host, $scheme oder $proxy_host, die dann leer in der Config landen und die Konfiguration zerstören.

Der Filter beschränkt die Ersetzung auf Variablennamen mit dem Präfix ACTANOVA_. nginx-Variablen bleiben dadurch unangetastet.

Beim Umbenennen den Filter mitziehen

Der Filter ist ein Präfix, keine Liste. Eine neue Variable PORTAL_TITLE würde nicht ersetzt, solange NGINX_ENVSUBST_FILTER=ACTANOVA_ gilt — die Platzhalter bleiben dann wörtlich in der Config stehen.

chgrp -R 0 + chmod -R g=u

OpenShift (SCC restricted-v2) ignoriert das USER aus dem Image und startet den Container mit einer zufälligen UID — die aber immer in der Gruppe GID 0 ist. Alles, was zur Laufzeit beschrieben wird, muss deshalb gruppenschreibbar sein: die gerenderte Config unter /etc/nginx/conf.d, der Cache, die PID-Datei in /tmp und der Web-Root. g=u spiegelt dazu die Owner-Rechte auf die Gruppe.

Ohne diesen Schritt startet der Container lokal einwandfrei und scheitert auf OpenShift beim Rendern der Vorlage.

ENV ACTANOVA_BASE_URL=""

Der leere Default ist Absicht: Er dokumentiert die Variable im Image, ohne ein Ziel vorzugeben. Gesetzt wird sie zur Laufzeit — bleibt sie leer, rendert envsubst ein proxy_pass /api/main/v1/; ohne Scheme und Host, also eine ungültige Direktive. Das fällt beim Start auf und nicht erst beim ersten API-Aufruf.

Die NGINX-Vorlage

server {
listen 8080;
server_name _;

# Uploads laufen als multipart/form-data -> großzügiges Limit
client_max_body_size 100m;

root /usr/share/nginx/html;
index index.html;

# authConfig.json nie cachen (das Sachbearbeiter-Cockpit lädt sie mit `cache: no-store`)
location = /authConfig.json {
add_header Cache-Control "no-store";
try_files $uri =404;
}

# API-Proxy: /api/cockpit/an/<x> -> ${ACTANOVA_BASE_URL}/api/main/v1/<x>
location /api/cockpit/an/ {
proxy_pass ${ACTANOVA_BASE_URL}/api/main/v1/;

proxy_http_version 1.1;
proxy_ssl_server_name on; # SNI an ActaNova senden
proxy_set_header Host $proxy_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;

proxy_connect_timeout 30s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}

# SPA: unbekannte Pfade auf index.html zurückfallen
location / {
try_files $uri $uri/ /index.html;
}
}

Die Datei heißt nginx/default.conf.template und ist die einzige Stelle mit einem Platzhalter. $host, $scheme, $proxy_host und $proxy_add_x_forwarded_for sind nginx-Variablen und überleben envsubst nur wegen des Filters aus dem Abschnitt oben.

Zwei Details, die leicht untergehen:

  • Der SPA-Fallback (try_files … /index.html) ist Pflicht, sobald du router="browser" verwendest — ohne ihn ergibt jeder Reload auf einer Unterseite einen 404. Beim Default router="hash" brauchst du ihn nicht, schaden tut er nicht. Siehe Routing.
  • Die authConfig.json darf nicht gecacht werden. Sonst hängt ein Browser nach einem Umgebungswechsel an der alten authority und die Anmeldung scheitert ohne erkennbaren Grund.

ACTANOVA_BASE_URL richtig setzen

Erwartet wird Scheme + Host, ohne abschließenden Slash und ohne Pfad — der Pfad /api/main/v1/ steht schon in der Vorlage:

ACTANOVA_BASE_URL=https://actanova.example.com # ✅
ACTANOVA_BASE_URL=https://actanova.example.com/ # ❌ doppelter Slash
ACTANOVA_BASE_URL=https://actanova.example.com/api/ # ❌ Pfad doppelt
ACTANOVA_BASE_URL=actanova.example.com # ❌ Scheme fehlt

Läuft die ActaNova-Instanz unter einem Unterpfad, gehört der mit hinein — im Demo-Projekt etwa https://actanova.dialogportal.ch/DialogPortalDemo/web, woraus dann …/web/api/main/v1/ wird.

Mit docker compose

services:
cockpit:
build:
context: .
image: an-cockpit
container_name: an-cockpit
ports:
- "8080:8080"
environment:
ACTANOVA_BASE_URL: https://actanova.example.com
volumes:
- ./conf/authConfig.json:/usr/share/nginx/html/authConfig.json:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s

Der Mount ist der zweite Teil des „ein Image für alle Umgebungen"-Prinzips: Die Datei liegt im Image bereits (sie kommt über publicDir: 'conf' in den Build), wird hier aber überschrieben. Read-only (:ro) genügt, das Sachbearbeiter-Cockpit liest sie nur.

Damit ergeben sich die beiden Stellschrauben pro Umgebung:

WasWomit
Ziel der API-AufrufeACTANOVA_BASE_URL
Keycloak-Realm, Client, checkPermissiongemountete authConfig.json

Auf Kubernetes/OpenShift entspricht das einer ConfigMap für die authConfig.json und einer Variable im Deployment — das Prinzip bleibt dasselbe.

Ohne Container

Der Container ist nur Verpackung. Gebraucht wird für jeden Webserver dasselbe:

  1. npm run build und den Inhalt von dist/ statisch ausliefern
  2. /authConfig.json erreichbar machen — und nicht cachen
  3. /api/cockpit/an/ auf <ActaNova>/api/main/v1/ weiterleiten, inklusive Authorization-Header
  4. bei router="browser" einen SPA-Fallback auf index.html

Fehlersuche

SymptomWahrscheinliche Ursache
Container startet nicht, nginx meldet einen Fehler in default.confACTANOVA_BASE_URL nicht gesetzt — proxy_pass bleibt ohne Scheme und Host
In der Config stehen leere Werte, wo $host stehen sollteNGINX_ENVSUBST_FILTER fehlt — envsubst hat nginx-Variablen mitersetzt
Alle API-Aufrufe ergeben 404Pfad in proxy_pass fehlt (/api/main/v1/) oder der abschließende Slash
Aufrufe gehen an …com//api/main/v1/…ACTANOVA_BASE_URL hat einen Slash am Ende
Fehlerbildschirm statt AnmeldungauthConfig.json nicht erreichbar, oder authority/client_id fehlen
Nach Umgebungswechsel die alte Keycloak-InstanzauthConfig.json wird gecacht — Cache-Control: no-store prüfen
Reload auf einer Unterseite ergibt 404SPA-Fallback fehlt, siehe Routing
Auf OpenShift Schreibfehler beim Startchgrp -R 0 / chmod -R g=u fehlen

Weiter