diff --git a/.gitignore b/.gitignore index 7d9c0e48..c6576a06 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ captures !*.xcodeproj/project.xcworkspace/ !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings +/.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..63061096 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +# ----------- Stage 1: Build Stage ----------- +FROM gradle:8.13-jdk21 AS build +WORKDIR /home/gradle/src +COPY build.gradle.kts settings.gradle.kts gradle.properties ./ +COPY gradle ./gradle +COPY shared ./shared +COPY server ./server +RUN gradle :server:shadowJar --no-configure-on-demand + +# ----------- Stage 2: Runtime Stage ----------- +FROM openjdk:21-slim-bookworm AS runtime +WORKDIR /app +COPY --from=build /home/gradle/src/server/build/libs/*.jar ./app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "/app/app.jar"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..b59c316c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,70 @@ +services: + server: + build: + context: . # Baut mit Dockerfile im Root + image: meldestelle/server:latest + container_name: meldestelle-server + restart: unless-stopped + ports: + - "8080:8080" + environment: + - DB_USER=${POSTGRES_USER} + - DB_PASSWORD=${POSTGRES_PASSWORD} + - DB_NAME=${POSTGRES_DB} + - DB_HOST=db + - DB_PORT=5432 + depends_on: + db: + condition: service_healthy + networks: + - meldestelle-net + # PostgreSQL Datenbank (Service-Name 'db') + db: + image: postgres:16-alpine # Spezifische Version + container_name: meldestelle-db + restart: unless-stopped + environment: + # Liest Werte aus .env + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + # PGDATA nicht nötig, Standard verwenden + volumes: + # Benanntes Volume für Daten auf Standardpfad + - postgres_data:/var/lib/postgresql/data + networks: + - meldestelle-net # <--- Muss zum Netzwerk-Namen passen + healthcheck: # Wichtig für depends_on + test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}" ] # Doppelte $$ beachten! + interval: 10s + timeout: 5s + retries: 5 + # ports: # Nur bei Bedarf freigeben, z.B. für lokalen Zugriff + # - "127.0.0.1:54321:5432" # Host-Port 54321 → Container-Port 5432 + + # Optional: PgAdmin Service +# pgadmin: +# image: dpage/pgadmin4:latest # Oder spezifische Version +# container_name: meldestelle-pgadmin +# restart: unless-stopped +# environment: +# # Werte aus .env lesen (oder Defaults nutzen) +# PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@example.com} +# PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-DeinSicheresPgAdminPasswort!} # UNBEDINGT IN .env SETZEN! +# PGADMIN_CONFIG_SERVER_MODE: 'False' +# volumes: +# - pgadmin_data:/var/lib/pgadmin # Benanntes Volume +# ports: +# # Port 5050 auf dem Host (nur localhost) → Port 80 im Container +# - "${PGADMIN_PORT:-127.0.0.1:5050}:80" +# networks: +# - meldestelle-net # <--- Muss zum Netzwerk-Namen passen +# depends_on: # PgAdmin braucht die DB +# - db + +networks: + meldestelle-net: + driver: bridge +volumes: + postgres_data: # <--- Konsistenter Name +# pgadmin_data: # <--- Konsistenter Name \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad373dd8..e67e2d18 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,9 @@ ktor-tests = "2.3.13" logback = "1.5.18" junit-jupiter = "5.12.0" junit-jupiter-version = "5.8.1" +exposed = "0.52.0" +postgresql = "42.7.3" +hikari = "5.1.0" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -29,6 +32,12 @@ jupiter-junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", v ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml", version.ref = "ktor" } junit-junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit-jupiter-version" } +exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } +exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } +exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } +postgresql-driver = { module = "org.postgresql:postgresql", version.ref = "postgresql" } +hikari-cp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } + [plugins] composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 40eb70c0..71b463ba 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -22,4 +22,16 @@ dependencies { testImplementation(libs.jupiter.junit.jupiter) implementation(libs.ktor.server.config.yaml) testImplementation(libs.junit.junit.jupiter) + + // Exposed für Datenbankzugriff (Core, DAO-Pattern, JDBC-Implementierung) + implementation(libs.exposed.core) + implementation(libs.exposed.dao) + implementation(libs.exposed.jdbc) + + // JDBC Treiber für PostgreSQL (nur zur Laufzeit benötigt) + runtimeOnly(libs.postgresql.driver) + + // HikariCP für Connection Pooling + implementation(libs.hikari.cp) + } \ No newline at end of file diff --git a/server/src/main/kotlin/at/mocode/Application.kt b/server/src/main/kotlin/at/mocode/Application.kt index c9ffbf12..ccd18b0a 100644 --- a/server/src/main/kotlin/at/mocode/Application.kt +++ b/server/src/main/kotlin/at/mocode/Application.kt @@ -1,5 +1,6 @@ package at.mocode +import at.mocode.plugins.configureDatabase import io.ktor.server.application.* import io.ktor.server.netty.* import io.ktor.server.response.* @@ -10,6 +11,11 @@ fun main(args: Array) { } fun Application.module() { + + // Als Erstes die Datenbank konfigurieren: + configureDatabase() + + // Danach deine anderen Konfigurationen (Routing etc.): routing { get("/") { call.respondText("Ktor: ${Greeting().greet()}") diff --git a/server/src/main/kotlin/at/mocode/plugins/Database.kt b/server/src/main/kotlin/at/mocode/plugins/Database.kt new file mode 100644 index 00000000..53f52949 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/plugins/Database.kt @@ -0,0 +1,68 @@ +package at.mocode.plugins + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import io.ktor.server.application.* +import org.jetbrains.exposed.sql.Database +import org.slf4j.LoggerFactory + +fun Application.configureDatabase() { + val log = LoggerFactory.getLogger("DatabaseInitialization") + log.info("Initializing database connection from environment variables...") + + // Lese Konfiguration direkt aus Umgebungsvariablen, + // die von Docker Compose (aus .env) gesetzt werden. + val dbHost = System.getenv("DB_HOST") ?: "db" // Fallback auf 'db', falls nicht gesetzt + val dbPort = System.getenv("DB_PORT") ?: "5432" + val dbName = System.getenv("DB_NAME") + ?: error("Database name (DB_NAME) not set in environment") // Fehler, wenn nicht gesetzt + val dbUser = System.getenv("DB_USER") + ?: error("Database user (DB_USER) not set in environment") // Fehler, wenn nicht gesetzt + val dbPassword = System.getenv("DB_PASSWORD") + ?: error("Database password (DB_PASSWORD) not set in environment") // Fehler, wenn nicht gesetzt + val driverClassName = "org.postgresql.Driver" // Ist für Postgres fix + // Pool Size auch optional aus Env Var lesen + val maxPoolSize = System.getenv("DB_POOL_SIZE")?.toIntOrNull() ?: 10 + + // Baue die JDBC URL zusammen + val jdbcURL = "jdbc:postgresql://$dbHost:$dbPort/$dbName" + + log.info("Attempting to connect to database at URL: {}", jdbcURL) // Logge die URL (ohne User/Passwort!) + + // Konfiguriere HikariCP mit den Werten aus der Umgebung + val hikariConfig = HikariConfig().apply { + this.driverClassName = driverClassName + this.jdbcUrl = jdbcURL + this.username = dbUser + this.password = dbPassword + this.maximumPoolSize = maxPoolSize + // Hier könnten weitere HikariCP-Optimierungen hin + try { + this.validate() // Prüft die Konfiguration frühzeitig + } catch (e: Exception) { + log.error("HikariCP configuration validation failed!", e) + throw e // Wirft den Fehler weiter, damit die App nicht startet + } + } + + // Erstelle DataSource und verbinde Exposed + try { + val dataSource = HikariDataSource(hikariConfig) + Database.connect(dataSource) + log.info("Database connection pool initialized successfully!") + } catch (e: Exception) { + log.error("Failed to initialize database connection pool!", e) + // Optional: Hier entscheiden, ob die App trotzdem starten soll oder nicht. + // Aktuell würde sie bei Fehlern hier abstürzen (was oft gewünscht ist). + throw e + } + + + // --- TODO für den NÄCHSTEN Schritt --- + // Hier kommt später die Logik zum Erstellen der Tabellen hin, + // z.B. innerhalb einer Transaktion: + // transaction { + // SchemaUtils.create(TurniereTable) // Erstellt die Tabelle, wenn sie nicht existiert + // } + // ------------------------------------ +} \ No newline at end of file diff --git a/server/src/main/resources/application.yaml b/server/src/main/resources/application.yaml index 6ae0785d..d6606cdb 100644 --- a/server/src/main/resources/application.yaml +++ b/server/src/main/resources/application.yaml @@ -1,5 +1,4 @@ # Grundkonfiguration für Ktor in YAML - ktor: deployment: # Der Port, auf dem der Server lauschen soll @@ -9,11 +8,7 @@ ktor: # watch: # - classes # - resources - application: # Hier wird Ktor gesagt, welche Funktion die Konfiguration enthält - # PASSE DEN PFAD AN, falls deine Application.kt oder module() anders heißt/liegt! modules: - - at.mocode.ApplicationKt.module - # Wenn Application.kt direkt unter at.mocode liegt: - # - at.mocode.ApplicationKt.module \ No newline at end of file + - at.mocode.ApplicationKt.module \ No newline at end of file