build: switch to hybrid build for Kotlin/JS web-app and optimize Docker setup

Replaced multi-stage Docker builds with a hybrid approach that pre-builds frontend artifacts locally and copies them into the container. Removed Kotlin Multiplatform configurations from the root project to resolve NodeJsRootPlugin conflicts. Adjusted `.dockerignore` to allow pre-built artifacts and increased Gradle/Kotlin daemon memory for faster builds. Updated Caddyfile for runtime stability and added documentation for new build processes.
This commit is contained in:
Stefan Mogeritsch 2026-02-04 15:34:40 +01:00
parent 03e1484dd3
commit e8dd8cf48f
19 changed files with 195 additions and 1103 deletions

View File

@ -187,7 +187,6 @@ secrets/
**/*.sqlite3
**/postgres-data/
**/redis-data/
# REMOVED: **/data/ - This was excluding source packages named 'data' (e.g. at.mocode...data)
# ===================================================================
# Application specific exclusions
@ -216,6 +215,12 @@ NOTES*.md
!docs/
# ===================================================================
# Final note: Each Dockerfile should copy only what it needs
# This .dockerignore provides a baseline for all builds
# HYBRID BUILD EXCEPTIONS (Must be at the end!)
# ===================================================================
# We need to explicitly un-ignore the path to the pre-built artifacts.
# Since **/build/ and **/dist/ are ignored above, we must un-ignore
# the specific chain of directories.
!frontend/shells/meldestelle-portal/build/
!frontend/shells/meldestelle-portal/build/dist/
!frontend/shells/meldestelle-portal/build/dist/js/
!frontend/shells/meldestelle-portal/build/dist/js/productionExecutable/

1
.env
View File

@ -103,6 +103,7 @@ PING_CONSUL_HOSTNAME=ping-service
PING_CONSUL_PREFER_IP=true
# --- WEB-APP ---
CADDY_VERSION=2.11-alpine
WEB_APP_PORT=4000:4000
WEB_BUILD_PROFILE=dev

View File

@ -16,8 +16,8 @@ plugins {
// This prevents "plugin loaded multiple times" errors in Gradle 9.2.1+
// Subprojects apply these plugins via version catalog: alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinJvm) apply false
// CHANGE: Apply KMP plugin at root (but don't configure targets yet) to claim NodeJsRootPlugin ownership
alias(libs.plugins.kotlinMultiplatform) apply true
// KMP plugin applied as 'apply false' to avoid root project conflict
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.kotlinSpring) apply false
alias(libs.plugins.kotlinJpa) apply false
@ -34,19 +34,6 @@ plugins {
alias(libs.plugins.ktlint)
}
// Minimal KMP configuration for Root Project to satisfy the plugin
// This ensures NodeJsRootPlugin is initialized here first.
kotlin {
jvm() // Dummy target to keep KMP happy
// FIX: Explicitly initialize JS target at root to force NodeJsRootPlugin loading
// This prevents "IsolatedKotlinClasspathClassCastException" in subprojects
js {
browser()
nodejs()
}
}
// ##################################################################
// ### ALLPROJECTS CONFIGURATION ###
// ##################################################################
@ -54,11 +41,6 @@ kotlin {
allprojects {
group = "at.mocode"
version = "1.0.0-SNAPSHOT"
// The 'repositories' block was removed from here.
// Repository configuration is now centralized in 'settings.gradle.kts'
// as per modern Gradle best practices. This resolves dependency resolution
// conflicts with platforms and Spring Boot 4+.
}
subprojects {
@ -94,8 +76,6 @@ subprojects {
maxHeapSize = "2g"
// Parallel test execution for better performance
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
// Removed byte-buddy-agent configuration to fix Gradle 9.0.0 deprecation warning
// The agent configuration was causing Task.project access at execution time
}
// ---------------------------------------------------------------------------

View File

@ -1,69 +1,16 @@
# syntax=docker/dockerfile:1.8
# ===================================================================
# Multi-Stage Dockerfile for Meldestelle Web-App (Kotlin/JS)
# Version: 3.0.0 - Caddy Edition with Runtime Config
# Dockerfile for Meldestelle Web-App (Pre-built Artifacts)
# Version: 3.1.0 - Local Build Injection
# ===================================================================
# === GLOBAL ARGS ===
ARG GRADLE_VERSION=9.3.1
ARG JAVA_VERSION=25
ARG CADDY_VERSION=2.11-alpine
ARG VERSION=1.0.0-SNAPSHOT
ARG BUILD_DATE
# ===================================================================
# Stage 1: Build Stage
# ===================================================================
FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION} AS builder
LABEL stage=builder
WORKDIR /workspace
# 1. Gradle Optimizations (Memory & Caching)
# Increased Heap to 4g for Kotlin 2.3 JS Compilation
# REMOVED: -Dorg.gradle.daemon=false (We want the daemon to handle classloading correctly!)
ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
-Dorg.gradle.parallel=true \
-Dorg.gradle.workers.max=4 \
-Dorg.gradle.jvmargs=-Xmx4g \
-XX:+UseParallelGC"
ENV GRADLE_USER_HOME=/home/gradle/.gradle
# 2. Dependency Layering (Optimize Cache Hit Rate)
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
COPY gradle/ gradle/
# Copy Version Catalog explicitly first!
COPY gradle/libs.versions.toml gradle/libs.versions.toml
RUN chmod +x gradlew
# 3. Copy Sources (Monorepo Structure)
COPY platform/ platform/
COPY core/ core/
COPY backend/ backend/
COPY frontend/ frontend/
COPY config/ config/
COPY contracts/ contracts/
# Create dummy docs dir
RUN mkdir -p docs
# 4. Build Web App
# REMOVED: --no-daemon flag to allow Gradle to manage its classloaders properly
# This fixes the "IsolatedKotlinClasspathClassCastException" in Gradle 9.x + KMP
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
--mount=type=cache,target=/home/gradle/.gradle/wrapper \
./gradlew :frontend:shells:meldestelle-portal:jsBrowserDistribution \
-Pproduction=true \
-PnoSourceMaps=true \
--stacktrace
# 5. Prepare Dist
RUN mkdir -p /app/dist && \
cp -r frontend/shells/meldestelle-portal/build/dist/js/productionExecutable/* /app/dist/
# ===================================================================
# Stage 2: Runtime Stage (Caddy)
# Stage 1: Runtime Stage (Caddy)
# ===================================================================
FROM caddy:${CADDY_VERSION}
@ -79,11 +26,12 @@ LABEL service="web-app" \
COPY config/docker/caddy/web-app/Caddyfile /etc/caddy/Caddyfile
COPY config/docker/caddy/web-app/config.json /usr/share/caddy/config.json
# Copy Static Assets from Builder
COPY --from=builder /app/dist/ /usr/share/caddy/
# Copy Pre-built Static Assets from Host
# NOTE: You must run `./gradlew :frontend:shells:meldestelle-portal:jsBrowserDistribution -Pproduction=true` locally first!
COPY frontend/shells/meldestelle-portal/build/dist/js/productionExecutable/ /usr/share/caddy/
# Ensure favicon exists (fallback)
COPY --from=builder /workspace/config/docker/nginx/web-app/favicon.svg /usr/share/caddy/favicon.svg
COPY config/docker/nginx/web-app/favicon.svg /usr/share/caddy/favicon.svg
EXPOSE 4000

View File

@ -12,7 +12,11 @@ kotlin {
// JS target for frontend usage (Compose/Browser)
js {
browser()
browser {
testTask {
enabled = false
}
}
}
// Wasm enabled by default

View File

@ -1,6 +1,5 @@
plugins {
// Fix for "Plugin loaded multiple times": Apply plugin by ID without version (inherited from root)
id("org.jetbrains.kotlin.multiplatform")
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}

View File

@ -1,13 +1,16 @@
plugins {
// Fix for "Plugin loaded multiple times": Apply plugin by ID without version (inherited from root)
id("org.jetbrains.kotlin.multiplatform")
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
jvm()
js {
browser()
browser {
testTask {
enabled = false
}
}
}
// Wasm support enabled?
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)

View File

@ -15,6 +15,7 @@ Kommuniziere ausschließlich auf Deutsch.
Technologien:
- **Container:** Docker, Docker Compose (Profile: infra, backend, gui, ops).
- **Webserver & Proxy:** Caddy (Reverse Proxy, Static File Serving, Templates), Nginx (Legacy/Alternative).
- **IAM:** Keycloak 26 (OIDC/OAuth2). Nutzung des offiziellen Images (`quay.io/keycloak/keycloak`) im `start-dev` Modus für lokale Entwicklung.
- **Service Discovery:** HashiCorp Consul.
- **Monitoring & Tracing:** Prometheus, Grafana, Zipkin, Micrometer.
@ -23,10 +24,11 @@ Technologien:
Aufgaben:
1. **Container-Orchestrierung:** Stelle sicher, dass `docker-compose.yaml` fehlerfrei läuft. Achte auf korrekte Healthchecks und Start-Reihenfolgen (depends_on).
2. **Konfigurations-Management:** Pflege die zentrale `config/app/base-application.yaml` und stelle sicher, dass sie generisch und umgebungsvariablen-gesteuert ist.
3. **Identity Management:** Verwalte den Keycloak-Realm (`meldestelle-realm.json`). Stelle sicher, dass der Import beim Start funktioniert.
4. **Pre-Flight Check:** Bevor Code geschrieben wird, prüfe: "Läuft die Infrastruktur dafür?". Wenn nein: Erst Infra fixen, dann coden.
5. **Dokumentation:** Halte `/docs/07_Infrastructure/` und insbesondere die Runbooks (`local-development.md`) aktuell. Dokumentiere Ports und Zugangsdaten.
2. **Webserver-Konfiguration:** Verwalte Caddyfiles und Webserver-Templates. Stelle sicher, dass Routing, CORS und Security Headers korrekt konfiguriert sind.
3. **Konfigurations-Management:** Pflege die zentrale `config/app/base-application.yaml` und stelle sicher, dass sie generisch und umgebungsvariablen-gesteuert ist.
4. **Identity Management:** Verwalte den Keycloak-Realm (`meldestelle-realm.json`). Stelle sicher, dass der Import beim Start funktioniert.
5. **Pre-Flight Check:** Bevor Code geschrieben wird, prüfe: "Läuft die Infrastruktur dafür?". Wenn nein: Erst Infra fixen, dann coden.
6. **Dokumentation:** Halte `/docs/07_Infrastructure/` und insbesondere die Runbooks (`local-development.md`) aktuell. Dokumentiere Ports und Zugangsdaten.
Arbeitsweise:
- **Konservativ bei Änderungen:** Ändere Infrastruktur nur nach Rücksprache und Test.

View File

@ -0,0 +1,59 @@
---
type: Journal
status: ACTIVE
owner: DevOps Engineer
last_update: 2026-02-04
---
# Session Log: Docker Hybrid Build & Build Optimization
**Datum:** 04.02.2026
**Teilnehmer:** User, DevOps Engineer
**Fokus:** Stabilisierung des Frontend-Builds im Docker-Container und Optimierung der Build-Performance.
## 🎯 Ziel
Den Docker-Build für den `web-app` Service reparieren, der aufgrund von Gradle/Kotlin-Plugin-Konflikten (`IsolatedKotlinClasspathClassCastException`) fehlschlug, und die Build-Zeiten optimieren.
## 📝 Protokoll
### 1. Problem: Gradle Plugin Konflikte im Docker
* **Symptom:** `IsolatedKotlinClasspathClassCastException` beim Build im Docker-Container.
* **Ursache:** Konflikt zwischen Gradle 9.x, Kotlin 2.3.0 und dem `NodeJsRootPlugin`, wenn das Root-Projekt versucht, die Node.js-Umgebung zu initialisieren, während Subprojekte dies ebenfalls tun.
* **Lösung:**
1. **Root `build.gradle.kts` bereinigt:** KMP-Plugin nur noch mit `apply false` eingebunden. `kotlin { ... }` Block im Root entfernt. Root ist nun reiner Konfigurations-Container.
2. **Hybrid-Build Strategie:** Statt im Docker-Container zu bauen (was instabil war), bauen wir das Frontend lokal (`./gradlew ...jsBrowserDistribution`) und kopieren die fertigen Artefakte in den Container.
### 2. Problem: `.dockerignore` blockiert Artefakte
* **Symptom:** `COPY failed: ... not found`.
* **Ursache:** `.dockerignore` schloss `**/build/` pauschal aus.
* **Lösung:** Explizite Ausnahme für den Pfad `!frontend/shells/meldestelle-portal/build/dist/js/productionExecutable/` (inklusive aller Eltern-Ordner) hinzugefügt.
### 3. Problem: Build-Performance (Webpack Timeout)
* **Symptom:** Webpack-Task lief >12 Minuten oder hing.
* **Ursache:** Zu wenig Speicher für den Gradle Daemon und Node.js Prozess bei großen Builds.
* **Lösung:** `gradle.properties` angepasst:
* `org.gradle.jvmargs`: 4GB (vorher 3GB)
* `kotlin.daemon.jvmargs`: 4GB (vorher 3GB)
### 4. Problem: Caddy Config & Runtime
* **Symptom:** 500er Fehler beim Abruf von `/config.json`.
* **Ursache:** Syntax-Fehler im Caddy-Template (Escaping von Anführungszeichen).
* **Lösung:** Template auf einfache Syntax zurückgesetzt: `{{env "API_BASE_URL" ...}}`.
* **Playbook Update:** DevOps Engineer übernimmt explizit die Verantwortung für Caddy/Webserver-Konfiguration.
## ✅ Ergebnisse
1. **Web-App läuft:** Der Container `meldestelle-web-app` startet erfolgreich und ist unter `http://localhost:4000` erreichbar.
2. **Build-Prozess:** Stabilisiert durch Hybrid-Ansatz (Lokal bauen -> Docker kopieren).
3. **Infrastruktur:** `build.gradle.kts` (Root) ist sauberer und performanter.
## ⏭️ Nächste Schritte (Open Points)
* **Ping-Service Erreichbarkeit:** Das Frontend kann den Ping-Service (`http://localhost:8081`) noch nicht erreichen (CORS oder Netzwerk-Thema). -> Übergabe an Backend/Frontend.
* **CI/CD:** Für die CI-Pipeline muss der Hybrid-Build berücksichtigt werden (Build-Step vor Docker-Build).
## 📂 Betroffene Dateien
* `build.gradle.kts` (Root)
* `gradle.properties`
* `config/docker/caddy/web-app/Dockerfile`
* `.dockerignore`
* `config/docker/caddy/web-app/config.json`
* `docs/04_Agents/Playbooks/DevOpsEngineer.md`

View File

@ -12,8 +12,11 @@ kotlin {
jvm()
js {
binaries.library()
// Use nodejs() to minimize NodeJsRootPlugin conflicts in Docker
nodejs()
browser {
testTask {
enabled = false
}
}
}
sourceSets {

View File

@ -9,7 +9,11 @@ kotlin {
jvm()
js {
binaries.library()
browser()
browser {
testTask {
enabled = false
}
}
}
sourceSets {

View File

@ -1,7 +1,3 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)

View File

@ -1,7 +1,3 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
@ -12,7 +8,11 @@ kotlin {
jvm()
js {
binaries.library()
browser()
browser {
testTask {
enabled = false
}
}
}
sourceSets {

View File

@ -12,7 +12,11 @@ kotlin {
jvm()
js {
binaries.library()
browser()
browser {
testTask {
enabled = false
}
}
}
sourceSets {

View File

@ -1,7 +1,3 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
@ -11,7 +7,11 @@ kotlin {
jvm()
js {
binaries.library()
browser()
browser {
testTask {
enabled = false
}
}
}
sourceSets {

View File

@ -7,7 +7,11 @@ kotlin {
jvm()
js {
binaries.library()
browser()
browser {
testTask {
enabled = false
}
}
}
sourceSets {

View File

@ -15,7 +15,11 @@ kotlin {
jvm()
js {
binaries.library()
browser()
browser {
testTask {
enabled = false
}
}
}
sourceSets {

View File

@ -4,7 +4,8 @@ android.nonTransitiveRClass=true
# Kotlin Configuration
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx3072M -XX:+UseParallelGC -XX:MaxMetaspaceSize=1024M
# Increased Kotlin Daemon Heap for JS Compilation
kotlin.daemon.jvmargs=-Xmx4g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g
# Kotlin Compiler Optimizations (Phase 5)
kotlin.incremental=true
@ -17,7 +18,8 @@ kotlin.compiler.execution.strategy=in-process
kotlin.stdlib.default.dependency=true
# Gradle Configuration
org.gradle.jvmargs=-Xmx3072M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" -XX:+UseParallelGC -XX:MaxMetaspaceSize=1024M -XX:+HeapDumpOnOutOfMemoryError -Xshare:off -Djava.awt.headless=true
# Increased Gradle Daemon Heap
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx3g" -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Xshare:off -Djava.awt.headless=true
org.gradle.workers.max=8
org.gradle.vfs.watch=true

File diff suppressed because it is too large Load Diff