This commit is contained in:
2025-12-04 03:34:11 +01:00
parent 95fe3e0573
commit 80ac574116
57 changed files with 583 additions and 1418 deletions
@@ -0,0 +1,52 @@
build:
maxIssues: 0
excludeCorrectable: false
config:
validation: true
warningsAsErrors: false
processors:
active: true
console-reports:
active: true
exclude:
- 'ProjectStatisticsReport'
- 'ComplexityReport'
- 'NotificationReport'
comments:
active: true
AbsentOrWrongFileLicense:
active: false
style:
active: true
MagicNumber:
active: false
WildcardImport:
active: false
MaxLineLength:
active: true
maxLineLength: 140
UnusedImports:
active: true
complexity:
active: true
LongMethod:
active: true
threshold: 80
TooManyFunctions:
active: true
thresholdInClasses: 30
performance:
active: true
potential-bugs:
active: true
exceptions:
active: true
@@ -0,0 +1,20 @@
// Kafka JAAS Configuration for Production
// =============================================================================
// This file configures SASL authentication for Kafka in production
// Change the passwords to strong, randomly generated values
// =============================================================================
KafkaServer {
org.apache.kafka.common.security.plain.PlainLoginModule required
username="admin"
password="CHANGE_ME_STRONG_KAFKA_ADMIN_PASSWORD"
user_admin="CHANGE_ME_STRONG_KAFKA_ADMIN_PASSWORD"
user_producer="CHANGE_ME_STRONG_KAFKA_PRODUCER_PASSWORD"
user_consumer="CHANGE_ME_STRONG_KAFKA_CONSUMER_PASSWORD";
};
Client {
org.apache.kafka.common.security.plain.PlainLoginModule required
username="admin"
password="CHANGE_ME_STRONG_KAFKA_ADMIN_PASSWORD";
};
@@ -0,0 +1,17 @@
// Zookeeper JAAS Configuration for Production
// =============================================================================
// This file configures SASL authentication for Zookeeper in production
// Change the passwords to strong, randomly generated values
// =============================================================================
Server {
org.apache.zookeeper.server.auth.DigestLoginModule required
user_admin="CHANGE_ME_STRONG_ZOOKEEPER_ADMIN_PASSWORD"
user_kafka="CHANGE_ME_STRONG_ZOOKEEPER_KAFKA_PASSWORD";
};
Client {
org.apache.zookeeper.server.auth.DigestLoginModule required
username="kafka"
password="CHANGE_ME_STRONG_ZOOKEEPER_KAFKA_PASSWORD";
};
@@ -0,0 +1,36 @@
# syntax=docker/dockerfile:1.8
# ===================================================================
# Production-Ready Keycloak Dockerfile
# ===================================================================
# Based on: quay.io/keycloak/keycloak:26.4
# Features:
# - Pre-built optimized image (faster startup)
# - Security hardening
# - Custom theme support
# - Health monitoring
# ===================================================================
ARG KEYCLOAK_IMAGE_TAG
FROM quay.io/keycloak/keycloak:${KEYCLOAK_IMAGE_TAG}
LABEL maintainer="Meldestelle Development Team"
LABEL description="Production-ready Keycloak for Meldestelle authentication"
LABEL version="${KEYCLOAK_IMAGE_TAG}"
# Set environment variables for build
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
ENV KC_DB=postgres
WORKDIR /opt/keycloak
# Pre-build Keycloak for faster startup
RUN /opt/keycloak/bin/kc.sh build \
--db=postgres \
--health-enabled=true \
--metrics-enabled=true
# Set user
USER 1000
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
@@ -0,0 +1,296 @@
{
"realm": "meldestelle",
"enabled": true,
"displayName": "Meldestelle Authentication",
"displayNameHtml": "<div class=\"kc-logo-text\"><span>Meldestelle</span></div>",
"sslRequired": "external",
"registrationAllowed": false,
"registrationEmailAsUsername": false,
"rememberMe": true,
"verifyEmail": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"editUsernameAllowed": false,
"bruteForceProtected": true,
"permanentLockout": false,
"maxFailureWaitSeconds": 900,
"minimumQuickLoginWaitSeconds": 60,
"waitIncrementSeconds": 60,
"quickLoginCheckMilliSeconds": 1000,
"maxDeltaTimeSeconds": 43200,
"failureFactor": 5,
"defaultSignatureAlgorithm": "RS256",
"offlineSessionMaxLifespan": 5184000,
"offlineSessionMaxLifespanEnabled": false,
"accessTokenLifespan": 300,
"accessTokenLifespanForImplicitFlow": 900,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"refreshTokenMaxReuse": 0,
"accessCodeLifespan": 60,
"accessCodeLifespanUserAction": 300,
"accessCodeLifespanLogin": 1800,
"actionTokenGeneratedByAdminLifespan": 43200,
"actionTokenGeneratedByUserLifespan": 300,
"oauth2DeviceCodeLifespan": 600,
"oauth2DevicePollingInterval": 5,
"internationalizationEnabled": true,
"supportedLocales": ["de", "en"],
"defaultLocale": "de",
"roles": {
"realm": [
{
"name": "ADMIN",
"description": "Administrator role with full system access",
"composite": false,
"clientRole": false
},
{
"name": "USER",
"description": "Standard user role with limited access",
"composite": false,
"clientRole": false
},
{
"name": "MONITORING",
"description": "Monitoring role for system health checks",
"composite": false,
"clientRole": false
},
{
"name": "GUEST",
"description": "Guest role with minimal access",
"composite": false,
"clientRole": false
}
]
},
"clients": [
{
"clientId": "api-gateway",
"name": "API Gateway Client",
"description": "OAuth2 client for the Meldestelle API Gateway",
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "K5RqonwVOaxPKaXVH4mbthSRbjRh5tOK",
"redirectUris": [
"http://localhost:8081/*",
"http://localhost:3000/*",
"https://app.meldestelle.at/*"
],
"webOrigins": [
"http://localhost:8081",
"http://localhost:3000",
"https://app.meldestelle.at"
],
"protocol": "openid-connect",
"bearerOnly": false,
"publicClient": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": true,
"authorizationServicesEnabled": false,
"fullScopeAllowed": true,
"frontchannelLogout": true,
"attributes": {
"access.token.lifespan": "300",
"client.secret.creation.time": "0",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"backchannel.logout.revoke.offline.tokens": "false"
},
"protocolMappers": [
{
"name": "realm-roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "realm_access.roles",
"jsonType.label": "String"
}
},
{
"name": "client-roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "resource_access.${client_id}.roles",
"jsonType.label": "String"
}
},
{
"name": "username",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "username",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "preferred_username",
"jsonType.label": "String"
}
},
{
"name": "email",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "email",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email",
"jsonType.label": "String"
}
},
{
"name": "full-name",
"protocol": "openid-connect",
"protocolMapper": "oidc-full-name-mapper",
"consentRequired": false,
"config": {
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
},
{
"clientId": "web-app",
"name": "Web Application Client",
"description": "Public client for web frontend",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"redirectUris": [
"http://localhost:4000/*",
"http://localhost:3000/*",
"https://app.meldestelle.at/*"
],
"webOrigins": [
"http://localhost:4000",
"http://localhost:3000",
"https://app.meldestelle.at"
],
"protocol": "openid-connect",
"attributes": {
"pkce.code.challenge.method": "S256"
}
}
],
"users": [
{
"username": "admin",
"enabled": true,
"emailVerified": true,
"firstName": "System",
"lastName": "Administrator",
"email": "admin@meldestelle.local",
"credentials": [
{
"type": "password",
"value": "Change_Me_In_Production!",
"temporary": true
}
],
"realmRoles": ["ADMIN", "USER"],
"clientRoles": {
"api-gateway": ["ADMIN"]
}
}
],
"groups": [],
"defaultRoles": ["USER", "GUEST"],
"requiredCredentials": ["password"],
"passwordPolicy": "length(8)",
"otpPolicyType": "totp",
"otpPolicyAlgorithm": "HmacSHA1",
"otpPolicyInitialCounter": 0,
"otpPolicyDigits": 6,
"otpPolicyLookAheadWindow": 1,
"otpPolicyPeriod": 30,
"otpSupportedApplications": ["FreeOTP", "Google Authenticator"],
"webAuthnPolicyRpEntityName": "meldestelle",
"webAuthnPolicySignatureAlgorithms": ["ES256", "RS256"],
"smtpServer": {},
"eventsEnabled": true,
"eventsListeners": ["jboss-logging"],
"enabledEventTypes": [
"LOGIN",
"LOGIN_ERROR",
"LOGOUT",
"REGISTER",
"REGISTER_ERROR",
"UPDATE_PASSWORD",
"UPDATE_PASSWORD_ERROR"
],
"adminEventsEnabled": true,
"adminEventsDetailsEnabled": true,
"identityProviders": [],
"identityProviderMappers": [],
"components": {
"org.keycloak.keys.KeyProvider": [
{
"name": "rsa-generated",
"providerId": "rsa-generated",
"subComponents": {},
"config": {
"priority": ["100"]
}
},
{
"name": "hmac-generated",
"providerId": "hmac-generated",
"subComponents": {},
"config": {
"priority": ["100"],
"algorithm": ["HS256"]
}
},
{
"name": "aes-generated",
"providerId": "aes-generated",
"subComponents": {},
"config": {
"priority": ["100"]
}
}
]
},
"authenticationFlows": [],
"authenticatorConfig": [],
"requiredActions": [],
"browserFlow": "browser",
"registrationFlow": "registration",
"directGrantFlow": "direct grant",
"resetCredentialsFlow": "reset credentials",
"clientAuthenticationFlow": "clients",
"dockerAuthenticationFlow": "docker auth",
"attributes": {
"frontendUrl": "",
"acr.loa.map": "{}",
"clientOfflineSessionMaxLifespan": "0",
"clientSessionIdleTimeout": "0",
"clientSessionMaxLifespan": "0",
"clientOfflineSessionIdleTimeout": "0"
}
}
@@ -0,0 +1,82 @@
global:
resolve_timeout: 5m
# SMTP configuration for email alerts - use environment variables
smtp_smarthost: '${SMTP_SMARTHOST:-smtp.example.com:587}'
smtp_from: '${SMTP_FROM:-alertmanager@meldestelle.at}'
smtp_auth_username: '${SMTP_AUTH_USERNAME:-alertmanager@meldestelle.at}'
smtp_auth_password: '${SMTP_AUTH_PASSWORD}'
smtp_require_tls: true
# The root route on which each incoming alert enters.
route:
# The root route must not have any matchers as it is the entry point for all alerts
# The default receiver is the one that handles alerts that don't match any of the specific routes
receiver: 'email-notifications'
# How long to wait before sending a notification again if it has already been sent successfully
repeat_interval: 4h
# How long to initially wait to send a notification for a group of alerts
group_wait: 30s
# How long to wait before sending a notification about new alerts that are added to a group
group_interval: 5m
# A default grouping of alerts
group_by: ['alertname', 'cluster', 'service']
# Child routes for specific alert categories
routes:
- receiver: 'slack-critical'
matchers:
- severity="critical"
repeat_interval: 1h
- receiver: 'slack-warnings'
matchers:
- severity="warning"
repeat_interval: 12h
# Inhibition rules allow to mute a set of alerts given that another alert is firing
inhibit_rules:
- source_matchers:
- severity="critical"
target_matchers:
- severity="warning"
# Apply inhibition if the alertname is the same
equal: ['alertname', 'cluster', 'service']
# Receivers define notification integrations
receivers:
- name: 'email-notifications'
email_configs:
- to: 'admin@meldestelle.at'
send_resolved: true
- name: 'slack-critical'
slack_configs:
- api_url: '${SLACK_WEBHOOK_URL_CRITICAL}'
channel: '${SLACK_CHANNEL_CRITICAL:-#alerts-critical}'
send_resolved: true
title: '{{ .CommonAnnotations.summary }}'
text: >-
{{ range .Alerts }}
*Alert:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*Severity:* {{ .Labels.severity }}
*Instance:* {{ .Labels.instance }}
{{ end }}
- name: 'slack-warnings'
slack_configs:
- api_url: '${SLACK_WEBHOOK_URL_WARNINGS}'
channel: '${SLACK_CHANNEL_WARNINGS:-#alerts-warnings}'
send_resolved: true
title: '{{ .CommonAnnotations.summary }}'
text: >-
{{ range .Alerts }}
*Alert:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*Severity:* {{ .Labels.severity }}
*Instance:* {{ .Labels.instance }}
{{ end }}
@@ -0,0 +1,13 @@
---
## Default Elasticsearch configuration
cluster.name: "meldestelle-elk"
network.host: 0.0.0.0
# Minimum memory requirements
discovery.type: single-node
# X-Pack security disabled for development
xpack.security.enabled: false
# Enable monitoring
xpack.monitoring.collection.enabled: true
@@ -0,0 +1,51 @@
input {
# TCP input for logback appender
tcp {
port => 5000
codec => json_lines
}
# File input for server logs
file {
path => "/var/log/meldestelle/*.log"
start_position => "beginning"
sincedb_path => "/dev/null"
}
}
filter {
if [type] == "syslog" {
grok {
match => { "message" => "%{SYSLOGTIMESTAMP:syslog_timestamp} %{SYSLOGHOST:syslog_hostname} %{DATA:syslog_program}(?:\[%{POSINT:syslog_pid}\])?: %{GREEDYDATA:syslog_message}" }
add_field => [ "received_at", "%{@timestamp}" ]
add_field => [ "received_from", "%{host}" ]
}
date {
match => [ "syslog_timestamp", "MMM d HH:mm:ss", "MMM dd HH:mm:ss" ]
}
}
# Parse JSON logs
if [message] =~ /^\{.*\}$/ {
json {
source => "message"
}
}
# Add application name
mutate {
add_field => { "application" => "meldestelle" }
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "meldestelle-logs-%{+YYYY.MM.dd}"
}
# For debugging
stdout {
codec => rubydebug
}
}
@@ -0,0 +1,389 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"description": "Meldestelle Application Overview Dashboard - Key metrics and health indicators",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"vis": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "rate(http_server_requests_seconds_count{application=\"meldestelle\"}[5m])",
"interval": "",
"legendFormat": "{{method}} {{uri}}",
"refId": "A"
}
],
"title": "HTTP Request Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 1
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"values": false,
"calcs": [
"lastNotNull"
],
"fields": ""
},
"textMode": "auto"
},
"pluginVersion": "8.5.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "up{application=\"meldestelle\"}",
"interval": "",
"legendFormat": "{{instance}}",
"refId": "A"
}
],
"title": "Application Status",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"vis": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "histogram_quantile(0.95, rate(http_server_requests_seconds_bucket{application=\"meldestelle\"}[5m])) * 1000",
"interval": "",
"legendFormat": "95th percentile",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "histogram_quantile(0.50, rate(http_server_requests_seconds_bucket{application=\"meldestelle\"}[5m])) * 1000",
"interval": "",
"legendFormat": "50th percentile",
"refId": "B"
}
],
"title": "HTTP Response Times",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"vis": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "rate(http_server_requests_seconds_count{application=\"meldestelle\",status=~\"[45].*\"}[5m]) / rate(http_server_requests_seconds_count{application=\"meldestelle\"}[5m]) * 100",
"interval": "",
"legendFormat": "Error Rate",
"refId": "A"
}
],
"title": "Error Rate",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 36,
"style": "dark",
"tags": [
"meldestelle",
"application",
"overview"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Meldestelle - Application Overview",
"uid": "meldestelle-app-overview",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,599 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"description": "Infrastructure Components Dashboard - Monitoring of PostgreSQL, Redis, Kafka, and other supporting services",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 1
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"reduceOptions": {
"values": false,
"calcs": [
"lastNotNull"
],
"fields": ""
},
"textMode": "auto"
},
"pluginVersion": "8.5.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "up{job=\"postgres\"}",
"interval": "",
"legendFormat": "PostgreSQL",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "up{job=\"redis\"}",
"interval": "",
"legendFormat": "Redis",
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "up{job=\"kafka\"}",
"interval": "",
"legendFormat": "Kafka",
"refId": "C"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "up{job=\"keycloak\"}",
"interval": "",
"legendFormat": "Keycloak",
"refId": "D"
}
],
"title": "Infrastructure Services Status",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"vis": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 4
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
"interval": "",
"legendFormat": "CPU Usage",
"refId": "A"
}
],
"title": "System CPU Usage",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"vis": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 4
},
"id": 3,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes",
"interval": "",
"legendFormat": "Memory Used",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "node_memory_MemTotal_bytes",
"interval": "",
"legendFormat": "Memory Total",
"refId": "B"
}
],
"title": "System Memory Usage",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"vis": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 12
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "pg_stat_database_numbackends{job=\"postgres\"}",
"interval": "",
"legendFormat": "{{datname}}",
"refId": "A"
}
],
"title": "PostgreSQL Active Connections",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"vis": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 12
},
"id": 5,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "redis_connected_clients{job=\"redis\"}",
"interval": "",
"legendFormat": "Connected Clients",
"refId": "A"
}
],
"title": "Redis Connected Clients",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"vis": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 12
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "kafka_server_brokertopicmetrics_messagesin_total{job=\"kafka\"}",
"interval": "",
"legendFormat": "{{topic}}",
"refId": "A"
}
],
"title": "Kafka Messages In",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 36,
"style": "dark",
"tags": [
"meldestelle",
"infrastructure",
"postgres",
"redis",
"kafka"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Meldestelle - Infrastructure Components",
"uid": "meldestelle-infrastructure",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,659 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 1,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "jvm_memory_used_bytes{area=\"heap\"}",
"legendFormat": "Used Heap",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "jvm_memory_committed_bytes{area=\"heap\"}",
"hide": false,
"legendFormat": "Committed Heap",
"range": true,
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "jvm_memory_max_bytes{area=\"heap\"}",
"hide": false,
"legendFormat": "Max Heap",
"range": true,
"refId": "C"
}
],
"title": "JVM Heap Memory",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "jvm_memory_used_bytes{area=\"nonheap\"}",
"legendFormat": "Used Non-Heap",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "jvm_memory_committed_bytes{area=\"nonheap\"}",
"hide": false,
"legendFormat": "Committed Non-Heap",
"range": true,
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "jvm_memory_max_bytes{area=\"nonheap\"}",
"hide": false,
"legendFormat": "Max Non-Heap",
"range": true,
"refId": "C"
}
],
"title": "JVM Non-Heap Memory",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "jvm_threads_live_threads",
"legendFormat": "Live Threads",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "jvm_threads_daemon_threads",
"hide": false,
"legendFormat": "Daemon Threads",
"range": true,
"refId": "B"
}
],
"title": "JVM Threads",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "jvm_gc_pause_seconds_sum / jvm_gc_pause_seconds_count",
"legendFormat": "GC Pause Time",
"range": true,
"refId": "A"
}
],
"title": "JVM GC Pause Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "rate(http_server_requests_seconds_count[1m])",
"legendFormat": "{{method}} {{uri}} {{status}}",
"range": true,
"refId": "A"
}
],
"title": "HTTP Request Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "http_server_requests_seconds_sum / http_server_requests_seconds_count",
"legendFormat": "{{method}} {{uri}} {{status}}",
"range": true,
"refId": "A"
}
],
"title": "HTTP Request Duration",
"type": "timeseries"
}
],
"refresh": "5s",
"schemaVersion": 38,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Meldestelle JVM Metrics",
"uid": "meldestelle-jvm",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: 'Meldestelle Dashboards'
orgId: 1
folder: 'Meldestelle'
type: file
disableDeletion: false
editable: true
options:
path: /var/lib/grafana/dashboards
@@ -0,0 +1,10 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
version: 1
@@ -0,0 +1,46 @@
# Prometheus configuration for Meldestelle project
# Basic configuration to enable service monitoring
global:
scrape_interval: 15s
evaluation_interval: 15s
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
- "alertmanager:9093"
rule_files:
- "/etc/prometheus/rules/alerts.yaml"
scrape_configs:
# Job 1: Prometheus überwacht sich selbst
- job_name: 'prometheus'
static_configs:
- targets: [ 'localhost:9090' ]
# Job 2: API Gateway (Spring Boot Actuator)
- job_name: 'api-gateway'
metrics_path: '/actuator/prometheus'
scrape_interval: "30s"
static_configs:
- targets: [ 'api-gateway:8081' ]
# Job 3: Postgres (ACHTUNG)
# Postgres direkt auf 5432 zu scrapen geht nicht.
# Entweder auskommentieren oder 'postgres-exporter' Container hinzufügen.
# - job_name: 'postgres-exporter'
# static_configs:
# - targets: ['postgres-exporter:9187']
# Add consul for service discovery monitoring
- job_name: 'consul'
metrics_path: '/v1/agent/metrics'
params:
format: [ 'prometheus' ]
static_configs:
- targets: [ 'consul:8500' ]
@@ -0,0 +1,73 @@
groups:
- name: meldestelle_alerts
rules:
# 1. Memory: Passt soweit, ist okay.
- alert: HighMemoryUsage
expr: (jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}) * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "High memory usage ({{ $value | humanize }}%)"
description: "JVM Heap usage is above 85%.\n Instance: {{ $labels.instance }}"
# 2. CPU: Passt auch.
- alert: HighCpuUsage
expr: process_cpu_usage * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "High CPU usage ({{ $value | humanize }}%)"
description: "CPU usage is above 85%.\n Instance: {{ $labels.instance }}"
# 3. Error Rate: FIX - Division durch null abfangen & Rate nutzen
- alert: HighErrorRate
# Wir prüfen nur, wenn überhaupt Requests > 0 da sind, um DivByZero zu vermeiden
expr: |
(
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count[5m]))
) * 100 > 5
for: 2m
labels:
severity: critical
annotations:
summary: "High error rate ({{ $value | humanize }}%)"
description: "More than 5% of requests resulted in 5xx errors.\n Instance: {{ $labels.instance }}"
# 4. Service Down: FIX - Job Name Regex
- alert: ServiceDown
# Prüft alle Jobs, die du in prometheus.yml definiert hast (api-gateway, consul etc.),
# 'up == 0' bedeutet: Target ist konfiguriert, aber nicht erreichbar.
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Service {{ $labels.job }} is down"
description: "Service instance {{ $labels.instance }} of job {{ $labels.job }} is not reachable."
# 5. Slow Response: FIX - 'rate' benutzen!
- alert: SlowResponseTime
# Berechnet die durchschnittliche Dauer pro request im 5-Minuten-Fenster
expr: rate(http_server_requests_seconds_sum[5m]) / rate(http_server_requests_seconds_count[5m]) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "Slow response time ({{ $value | humanizeDuration }})"
description: "Average response time is > 1s for the last 5 minutes.\n Instance: {{ $labels.instance }}\n Path: {{ $labels.uri }}"
# 6. GC Pause: FIX - 'rate' benutzen!
- alert: HighGcPauseTime
# Zeigt an, wie viel Zeit PRO SEKUNDE für GC draufgeht (nicht pro GC Event, das ist oft aussagekräftiger)
# Oder "Durchschnittliche Dauer pro GC Event im Zeitfenster":
expr: rate(jvm_gc_pause_seconds_sum[5m]) / rate(jvm_gc_pause_seconds_count[5m]) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "High GC pause time ({{ $value | humanizeDuration }})"
description: "Average GC pause is > 0.5s.\n Instance: {{ $labels.instance }}"
@@ -0,0 +1,133 @@
# Nginx Production Configuration
# =============================================================================
# This configuration provides secure reverse proxy with SSL termination,
# security headers, and performance optimizations for production
# =============================================================================
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
# Performance and Security Settings
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
# Basic Settings
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Performance Settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# Buffer Settings
client_body_buffer_size 128k;
client_max_body_size 10m;
client_header_buffer_size 1k;
large_client_header_buffers 4 4k;
output_buffers 1 32k;
postpone_output 1460;
# Timeout Settings
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
# Gzip Compression
gzip on;
gzip_vary on;
gzip_min_length 10240;
gzip_proxied expired no-cache no-store private must-revalidate auth;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/x-javascript
application/xml+rss
application/javascript
application/json
application/xml
application/rss+xml
application/atom+xml
image/svg+xml;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Rate Limiting
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
limit_req_zone $binary_remote_addr zone=general:10m rate=1000r/m;
# Logging Format
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time';
access_log /var/log/nginx/access.log main;
# SSL Configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
# Upstream Definitions
upstream keycloak {
server keycloak:8443;
keepalive 32;
}
upstream grafana {
server grafana:3000;
keepalive 32;
}
upstream prometheus {
server prometheus:9090;
keepalive 32;
}
# HTTP to HTTPS Redirect
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
# Health Check Endpoint
server {
listen 80;
server_name localhost;
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
# Include additional server configurations
include /etc/nginx/conf.d/*.conf;
}
@@ -0,0 +1,35 @@
-- ===================================================================
-- PostgreSQL Initialization Script for Keycloak
-- ===================================================================
-- Dieses Skript erstellt ein separates Schema für Keycloak-Daten innerhalb der
-- meldestelle-Datenbank und sorgt so für Isolation und bessere Organisation.
--
-- Ausführung: Wird automatisch von PostgreSQL beim ersten Start ausgeführt
-- über den docker-entrypoint-initdb.d-Mechanismus.
-- ===================================================================
-- Erstellt das Keycloak-Schema, falls es noch nicht existiert.
CREATE SCHEMA IF NOT EXISTS keycloak;
-- Da der "POSTGRES_USER" (Superuser) das Skript ausführt,
-- gehört ihm das Schema automatisch oder er hat Zugriff.
-- Explizite GRANTS auf "pg-user" entfernen, um .env-Unabhängigkeit zu wahren.
-- Falls du es explizit willst, nutze current_user (der ausführende User):
GRANT ALL PRIVILEGES ON SCHEMA keycloak TO current_user;
-- Gewährt dem Benutzer „meldestelle“ alle Berechtigungen für das Schema.
-- GRANT ALL PRIVILEGES ON SCHEMA keycloak TO "pg-user";
-- Gewährt die Nutzung des Schemas
GRANT USAGE ON SCHEMA keycloak TO "pg-user";
-- Standardberechtigungen für zukünftige Tabellen im Keycloak-Schema festlegen
ALTER DEFAULT PRIVILEGES IN SCHEMA keycloak GRANT ALL ON TABLES TO "pg-user";
ALTER DEFAULT PRIVILEGES IN SCHEMA keycloak GRANT ALL ON SEQUENCES TO "pg-user";
ALTER DEFAULT PRIVILEGES IN SCHEMA keycloak GRANT ALL ON FUNCTIONS TO "pg-user";
-- Log successful schema Erstellung
DO $$
BEGIN
RAISE NOTICE 'Keycloak schema created successfully in database';
END $$;
@@ -0,0 +1,11 @@
-- ===================================================================
-- Keycloak Schema Init (No-Op)
-- ===================================================================
-- DEPRECATED: Schema-initialization erfolgt über die Datei 01-init-keycloak-schema.sql.
-- Diese Datei dient lediglich der Sicherstellung der Ausführungsreihenfolge, führt aber keine Aktionen aus.
-- ===================================================================
DO $$
BEGIN
RAISE NOTICE '02-init-keycloak-schema.sql is a no-op (handled by 01-init-keycloak-schema.sql)';
END $$;
@@ -0,0 +1,90 @@
# PostgreSQL Configuration File
# Optimized for Meldestelle application
# Connection Settings
listen_addresses = '*'
max_connections = 100
superuser_reserved_connections = 3
# Memory Settings
# These will be overridden by environment variables in docker-compose.yaml
shared_buffers = 256MB # min 128kB
work_mem = 16MB # min 64kB
maintenance_work_mem = 64MB # min 1MB
effective_cache_size = 768MB
# Write-Ahead Log (WAL)
wal_level = replica # minimal, replica, or logical
max_wal_size = 1GB
min_wal_size = 80MB
wal_buffers = 16MB # min 32kB, -1 sets based on shared_buffers
checkpoint_completion_target = 0.9 # checkpoint target duration, 0.0 - 1.0
random_page_cost = 1.1 # For SSD storage
# Background Writer
bgwriter_delay = 200ms
bgwriter_lru_maxpages = 100
bgwriter_lru_multiplier = 2.0
# Asynchronous Behavior
effective_io_concurrency = 200 # For SSD storage
max_worker_processes = 8
max_parallel_workers_per_gather = 4
max_parallel_workers = 8
max_parallel_maintenance_workers = 4
# Query Planner
default_statistics_target = 100
constraint_exclusion = partition
# Logging
log_destination = 'stderr'
logging_collector = off
# log_directory = 'log'
# log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_truncate_on_rotation = off
log_rotation_age = 1d
log_rotation_size = 100MB
log_min_duration_statement = 250ms # Log slow queries (250ms or slower)
log_checkpoints = on
log_connections = on
log_disconnections = on
log_lock_waits = on
log_temp_files = 0
log_autovacuum_min_duration = 250ms
log_line_prefix = '%m [%p] %q%u@%d '
# Autovacuum
autovacuum = on
autovacuum_max_workers = 3
autovacuum_naptime = 1min
autovacuum_vacuum_threshold = 50
autovacuum_analyze_threshold = 50
autovacuum_vacuum_scale_factor = 0.05
autovacuum_analyze_scale_factor = 0.025
autovacuum_vacuum_cost_delay = 20ms
autovacuum_vacuum_cost_limit = 2000
# Statement Behavior
search_path = '"$user", public'
row_security = on
# Client Connection Defaults
client_min_messages = notice
statement_timeout = 60000 # 60 seconds, prevents long-running queries
lock_timeout = 10000 # 10 seconds, prevents lock contention
idle_in_transaction_session_timeout = 600000 # 10 minutes, prevents idle transactions
# Disk
temp_file_limit = 1GB # Limits temp file size
# SSL
ssl = off
ssl_prefer_server_ciphers = on
# Performance Monitoring
track_activities = on
track_counts = on
track_io_timing = on
track_functions = pl # none, pl, all
track_activity_query_size = 2048
@@ -0,0 +1,149 @@
# Redis Production Configuration
# =============================================================================
# This configuration file contains production-ready settings for Redis
# with security, performance, and reliability optimizations.
# =============================================================================
# Network and Security
bind 0.0.0.0
protected-mode yes
port 6379
# Authentication (password will be set via command line)
# requirepass will be set via --requirepass flag in docker-compose
# General Settings
timeout 300
tcp-keepalive 300
tcp-backlog 511
# Memory Management
maxmemory 256mb
maxmemory-policy allkeys-lru
maxmemory-samples 5
# Persistence Settings
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /data
# Append Only File (AOF)
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
# Logging
loglevel notice
logfile ""
syslog-enabled no
# Database Settings
databases 16
# Slow Log
slowlog-log-slower-than 10000
slowlog-max-len 128
# Latency Monitoring
latency-monitor-threshold 100
# Client Settings
maxclients 10000
# Security Settings
rename-command FLUSHDB ""
rename-command FLUSHALL ""
# KEYS ist langsam, sperren ist okay (Admin tools funktionieren dann aber evtl. nicht mehr)
rename-command KEYS ""
rename-command CONFIG "CONFIG_b835c3f8a5d2e7f1"
rename-command SHUTDOWN "SHUTDOWN_a9b4c2d1e3f5g6h7"
rename-command DEBUG ""
# EVAL wird für Lua-Skripte benötigt (Locks, Rate Limiting etc.)
# rename-command EVAL ""
# DEL wird benötigt, damit die App Cache-Einträge invalidieren kann!
# rename-command DEL "DEL_prod_safe"
# TLS Configuration (uncomment and configure for TLS)
# port 0
# tls-port 6380
# tls-cert-file /tls/redis.crt
# tls-key-file /tls/redis.key
# tls-ca-cert-file /tls/ca.crt
# tls-dh-params-file /tls/redis.dh
# tls-protocols "TLSv1.2 TLSv1.3"
# tls-ciphers "ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS"
# tls-ciphersuites "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"
# tls-prefer-server-ciphers yes
# tls-session-caching no
# tls-session-cache-size 5000
# tls-session-cache-timeout 60
# Performance Tuning
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
# Active Rehashing
activerehashing yes
# Client Output Buffer Limits
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
# Client Query Buffer
client-query-buffer-limit 1gb
# Protocol Buffer
proto-max-bulk-len 512mb
# Replication (for Redis cluster/replica setup)
# replica-serve-stale-data yes
# replica-read-only yes
# repl-diskless-sync no
# repl-diskless-sync-delay 5
# repl-ping-replica-period 10
# repl-timeout 60
# repl-disable-tcp-nodelay no
# repl-backlog-size 1mb
# repl-backlog-ttl 3600
# Security: Disable potentially dangerous features
enable-protected-configs no
enable-debug-command no
enable-module-command no
# Notifications (disable for performance)
notify-keyspace-events ""
# Advanced Configuration
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
# Jemalloc Configuration
jemalloc-bg-thread yes
# Threading (Redis 6.0+)
# io-threads 4
# io-threads-do-reads yes
@@ -0,0 +1,619 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://meldestelle.at/schemas/docker-versions.json",
"title": "Docker Versions TOML Schema",
"description": "Schema for docker/versions.toml - centralized Docker version management",
"type": "object",
"properties": {
"versions": {
"type": "object",
"description": "Central version definitions for all Docker components",
"properties": {
"gradle": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "Gradle version for build tools",
"examples": ["9.0.0", "8.14.0"]
},
"java": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "Java version (LTS recommended)",
"examples": ["21", "17", "11"]
},
"node": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "Node.js version for client builds",
"examples": ["20.12.0", "18.19.0"]
},
"nginx": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+-alpine$",
"description": "Nginx version with Alpine base",
"examples": ["1.25-alpine", "1.24-alpine"]
},
"alpine": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+$",
"description": "Alpine Linux base image version",
"examples": ["3.19", "3.18"]
},
"eclipse-temurin-jdk": {
"type": "string",
"pattern": "^[0-9]+-jdk-alpine$",
"description": "Eclipse Temurin JDK image tag",
"examples": ["21-jdk-alpine", "17-jdk-alpine"]
},
"eclipse-temurin-jre": {
"type": "string",
"pattern": "^[0-9]+-jre-alpine$",
"description": "Eclipse Temurin JRE image tag",
"examples": ["21-jre-alpine", "17-jre-alpine"]
},
"prometheus": {
"type": "string",
"pattern": "^v[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "Prometheus monitoring version",
"examples": ["v2.54.1", "v2.47.0"]
},
"grafana": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "Grafana visualization version",
"examples": ["11.3.0", "10.1.0"]
},
"keycloak": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "Keycloak authentication version",
"examples": ["26.0.7", "25.0.6"]
},
"spring-profiles-default": {
"type": "string",
"enum": ["default", "dev", "test", "prod"],
"description": "Default Spring profile for infrastructure services"
},
"spring-profiles-docker": {
"type": "string",
"enum": ["docker", "dev", "test", "prod"],
"description": "Spring profile for Docker containers"
},
"spring-profiles-prod": {
"type": "string",
"enum": ["prod", "production"],
"description": "Spring profile for production environment"
},
"app-version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "Application version for all services",
"examples": ["1.0.0", "2.1.3"]
}
},
"required": [
"gradle",
"java",
"node",
"nginx",
"alpine",
"prometheus",
"grafana",
"keycloak",
"app-version"
],
"additionalProperties": false
},
"service-ports": {
"type": "object",
"description": "Centralized port definitions for all services",
"properties": {
"api-gateway": {
"type": "integer",
"minimum": 8081,
"maximum": 8081,
"description": "API Gateway port"
},
"auth-server": {
"type": "integer",
"minimum": 8087,
"maximum": 8087,
"description": "Authentication server port"
},
"monitoring-server": {
"type": "integer",
"minimum": 8088,
"maximum": 8088,
"description": "Monitoring server port"
},
"ping-service": {
"type": "integer",
"minimum": 8082,
"maximum": 8082,
"description": "Ping service port"
},
"members-service": {
"type": "integer",
"minimum": 8083,
"maximum": 8083,
"description": "Members service port"
},
"horses-service": {
"type": "integer",
"minimum": 8084,
"maximum": 8084,
"description": "Horses service port"
},
"events-service": {
"type": "integer",
"minimum": 8085,
"maximum": 8085,
"description": "Events service port"
},
"masterdata-service": {
"type": "integer",
"minimum": 8086,
"maximum": 8086,
"description": "Masterdata service port"
},
"postgres": {
"type": "integer",
"minimum": 5432,
"maximum": 5432,
"description": "PostgreSQL database port"
},
"redis": {
"type": "integer",
"minimum": 6379,
"maximum": 6379,
"description": "Redis cache port"
},
"keycloak": {
"type": "integer",
"minimum": 8180,
"maximum": 8180,
"description": "Keycloak authentication port"
},
"consul": {
"type": "integer",
"minimum": 8500,
"maximum": 8500,
"description": "Consul service discovery port"
},
"zookeeper": {
"type": "integer",
"minimum": 2181,
"maximum": 2181,
"description": "Zookeeper coordination port"
},
"kafka": {
"type": "integer",
"minimum": 9092,
"maximum": 9092,
"description": "Kafka messaging port"
},
"prometheus": {
"type": "integer",
"minimum": 9090,
"maximum": 9090,
"description": "Prometheus monitoring port"
},
"grafana": {
"type": "integer",
"minimum": 3000,
"maximum": 3000,
"description": "Grafana visualization port"
},
"web-app": {
"type": "integer",
"minimum": 4000,
"maximum": 4000,
"description": "Web application port"
},
"desktop-app-vnc": {
"type": "integer",
"minimum": 5901,
"maximum": 5901,
"description": "Desktop app VNC port"
},
"desktop-app-novnc": {
"type": "integer",
"minimum": 6080,
"maximum": 6080,
"description": "Desktop app noVNC web port"
}
},
"required": [
"api-gateway",
"auth-server",
"monitoring-server",
"ping-service",
"postgres",
"redis",
"keycloak",
"prometheus",
"grafana",
"web-app"
],
"additionalProperties": false
},
"port-ranges": {
"type": "object",
"description": "Port range definitions for service categories",
"properties": {
"infrastructure": {
"type": "string",
"pattern": "^[0-9]+-[0-9]+$",
"description": "Port range for infrastructure services",
"examples": ["8081-8088"]
},
"services": {
"type": "string",
"pattern": "^[0-9]+-[0-9]+$",
"description": "Port range for application services",
"examples": ["8082-8099"]
},
"monitoring": {
"type": "string",
"pattern": "^[0-9]+-[0-9]+$",
"description": "Port range for monitoring services",
"examples": ["9090-9099"]
},
"clients": {
"type": "string",
"pattern": "^[0-9]+-[0-9]+$",
"description": "Port range for client applications",
"examples": ["4000-4099"]
},
"vnc": {
"type": "string",
"pattern": "^[0-9]+-[0-9]+$",
"description": "Port range for VNC connections",
"examples": ["5901-5999"]
},
"debug": {
"type": "string",
"pattern": "^[0-9]+-[0-9]+$",
"description": "Port range for debug connections",
"examples": ["5005-5009"]
},
"system-reserved": {
"type": "string",
"pattern": "^[0-9]+-[0-9]+$",
"description": "System reserved port range",
"examples": ["0-1023"]
},
"ephemeral": {
"type": "string",
"pattern": "^[0-9]+-[0-9]+$",
"description": "Ephemeral port range",
"examples": ["32768-65535"]
}
},
"required": [
"infrastructure",
"services",
"monitoring",
"clients",
"debug"
],
"additionalProperties": false
},
"build-args": {
"type": "object",
"description": "Build argument categories for different service types",
"properties": {
"global": {
"type": "array",
"description": "Global build arguments used by all services",
"items": {
"type": "string",
"enum": [
"GRADLE_VERSION",
"JAVA_VERSION",
"BUILD_DATE",
"VERSION"
]
},
"uniqueItems": true
},
"spring-services": {
"type": "array",
"description": "Spring Boot service-specific build arguments",
"items": {
"type": "string",
"enum": [
"SPRING_PROFILES_ACTIVE",
"SERVICE_PATH",
"SERVICE_NAME",
"SERVICE_PORT"
]
},
"uniqueItems": true
},
"web-clients": {
"type": "array",
"description": "Web client-specific build arguments",
"items": {
"type": "string",
"enum": [
"NODE_VERSION",
"NGINX_VERSION",
"CLIENT_PATH",
"CLIENT_MODULE",
"CLIENT_NAME"
]
},
"uniqueItems": true
}
},
"required": ["global"],
"additionalProperties": false
},
"categories": {
"type": "object",
"description": "Service category configurations",
"properties": {
"services": {
"type": "object",
"properties": {
"default-spring-profile": {
"type": "string",
"enum": ["docker", "dev", "test", "prod"]
},
"default-port-start": {
"type": "integer",
"minimum": 8082,
"maximum": 8099
},
"services": {
"type": "array",
"items": {
"type": "string",
"enum": [
"ping-service",
"members-service",
"horses-service",
"events-service",
"masterdata-service"
]
},
"uniqueItems": true
}
},
"required": ["services"],
"additionalProperties": false
},
"infrastructure": {
"type": "object",
"properties": {
"default-spring-profile": {
"type": "string",
"enum": ["default", "dev", "test", "prod"]
},
"services": {
"type": "array",
"items": {
"type": "string",
"enum": [
"gateway",
"auth-server",
"monitoring-server"
]
},
"uniqueItems": true
}
},
"required": ["services"],
"additionalProperties": false
},
"clients": {
"type": "object",
"properties": {
"default-node-version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
},
"default-nginx-version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+-alpine$"
},
"clients": {
"type": "array",
"items": {
"type": "string",
"enum": [
"web-app",
"desktop-app"
]
},
"uniqueItems": true
}
},
"required": ["clients"],
"additionalProperties": false
}
},
"required": ["services", "infrastructure", "clients"],
"additionalProperties": false
},
"environment-mapping": {
"type": "object",
"description": "Maps internal version names to environment variable names",
"properties": {
"gradle-version": {
"type": "string",
"enum": ["DOCKER_GRADLE_VERSION"]
},
"java-version": {
"type": "string",
"enum": ["DOCKER_JAVA_VERSION"]
},
"node-version": {
"type": "string",
"enum": ["DOCKER_NODE_VERSION"]
},
"nginx-version": {
"type": "string",
"enum": ["DOCKER_NGINX_VERSION"]
},
"prometheus-version": {
"type": "string",
"enum": ["DOCKER_PROMETHEUS_VERSION"]
},
"grafana-version": {
"type": "string",
"enum": ["DOCKER_GRAFANA_VERSION"]
},
"keycloak-version": {
"type": "string",
"enum": ["DOCKER_KEYCLOAK_VERSION"]
},
"spring-profiles-default": {
"type": "string",
"enum": ["DOCKER_SPRING_PROFILES_DEFAULT"]
},
"spring-profiles-docker": {
"type": "string",
"enum": ["DOCKER_SPRING_PROFILES_DOCKER"]
},
"app-version": {
"type": "string",
"enum": ["DOCKER_APP_VERSION"]
}
},
"required": [
"gradle-version",
"java-version",
"node-version",
"nginx-version",
"prometheus-version",
"grafana-version",
"keycloak-version",
"app-version"
],
"additionalProperties": false
},
"environments": {
"type": "object",
"description": "Environment-specific configurations",
"properties": {
"development": {
"$ref": "#/definitions/environment-config"
},
"production": {
"$ref": "#/definitions/environment-config"
},
"testing": {
"$ref": "#/definitions/environment-config"
}
},
"required": ["development", "production", "testing"],
"additionalProperties": false
}
},
"required": [
"versions",
"service-ports",
"port-ranges",
"build-args",
"categories",
"environment-mapping",
"environments"
],
"additionalProperties": false,
"definitions": {
"environment-config": {
"type": "object",
"description": "Environment-specific configuration settings",
"properties": {
"spring-profiles": {
"type": "string",
"enum": ["dev", "test", "prod", "docker"],
"description": "Default Spring profiles for this environment"
},
"debug-enabled": {
"type": "boolean",
"description": "Whether debug mode is enabled"
},
"log-level": {
"type": "string",
"enum": ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"],
"description": "Default log level for services"
},
"health-check-interval": {
"type": "string",
"pattern": "^[0-9]+s$",
"description": "Health check interval (e.g., '30s')"
},
"health-check-timeout": {
"type": "string",
"pattern": "^[0-9]+s$",
"description": "Health check timeout (e.g., '5s')"
},
"health-check-retries": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"description": "Number of health check retries"
},
"health-check-start-period": {
"type": "string",
"pattern": "^[0-9]+s$",
"description": "Health check start period (e.g., '40s')"
},
"resource-limits": {
"type": "boolean",
"description": "Whether to enforce resource limits"
},
"jvm-debug-port": {
"oneOf": [
{
"type": "integer",
"minimum": 5005,
"maximum": 5009
},
{
"type": "boolean",
"enum": [false]
}
],
"description": "JVM debug port (5005-5009) or false to disable"
},
"hot-reload": {
"type": "boolean",
"description": "Whether hot reload is enabled for development"
},
"security-headers": {
"type": "boolean",
"description": "Whether to add security headers (production)"
},
"tls-enabled": {
"type": "boolean",
"description": "Whether TLS/SSL is enabled (production)"
},
"ephemeral-storage": {
"type": "boolean",
"description": "Whether to use ephemeral storage (testing)"
},
"test-containers": {
"type": "boolean",
"description": "Whether test containers are used (testing)"
}
},
"required": [
"spring-profiles",
"debug-enabled",
"log-level",
"health-check-interval",
"health-check-timeout",
"health-check-retries",
"health-check-start-period",
"resource-limits",
"jvm-debug-port",
"hot-reload"
],
"additionalProperties": false
}
}
}
@@ -0,0 +1,270 @@
# SSL/TLS Zertifikat-Setup für die Produktionsumgebung
Dieses Verzeichnis enthält SSL/TLS-Zertifikate und Schlüssel zur Absicherung der Meldestelle-Anwendung in der Produktionsumgebung.
## Verzeichnisstruktur
```
config/ssl/
├── postgres/ # PostgreSQL SSL-Zertifikate
├── redis/ # Redis TLS-Zertifikate
├── keycloak/ # Keycloak HTTPS-Zertifikate
├── prometheus/ # Prometheus HTTPS-Zertifikate
├── grafana/ # Grafana HTTPS-Zertifikate
├── nginx/ # Nginx SSL-Zertifikate
└── README.md # Diese Datei
```
## Zertifikat-Anforderungen
### 1. PostgreSQL SSL-Zertifikate
Platzieren Sie die folgenden Dateien in `config/ssl/postgres/`:
- `server.crt` - Server-Zertifikat
- `server.key` - Privater Server-Schlüssel
- `ca.crt` - Certificate Authority-Zertifikat
### 2. Redis TLS-Zertifikate
Platzieren Sie die folgenden Dateien in `config/ssl/redis/`:
- `redis.crt` - Redis Server-Zertifikat
- `redis.key` - Privater Redis Server-Schlüssel
- `ca.crt` - Certificate Authority-Zertifikat
- `redis.dh` - Diffie-Hellman Parameter
### 3. Keycloak HTTPS-Zertifikate
Platzieren Sie die folgenden Dateien in `config/ssl/keycloak/`:
- `server.crt.pem` - Server-Zertifikat im PEM-Format
- `server.key.pem` - Privater Server-Schlüssel im PEM-Format
### 4. Prometheus HTTPS-Zertifikate
Platzieren Sie die folgenden Dateien in `config/ssl/prometheus/`:
- `prometheus.crt` - Prometheus Server-Zertifikat
- `prometheus.key` - Privater Prometheus Server-Schlüssel
- `web.yml` - Prometheus Web-Konfigurationsdatei
### 5. Grafana HTTPS-Zertifikate
Platzieren Sie die folgenden Dateien in `config/ssl/grafana/`:
- `server.crt` - Grafana Server-Zertifikat
- `server.key` - Privater Grafana Server-Schlüssel
### 6. Nginx SSL-Zertifikate
Platzieren Sie die folgenden Dateien in `config/ssl/nginx/`:
- `server.crt` - Haupt-SSL-Zertifikat
- `server.key` - Privater Haupt-SSL-Schlüssel
- `dhparam.pem` - Diffie-Hellman Parameter
## Generierung selbstsignierter Zertifikate (Entwicklung/Test)
⚠️ **Warnung**: Verwenden Sie selbstsignierte Zertifikate nur für Entwicklung und Tests. Nutzen Sie ordnungsgemäß von einer CA signierte Zertifikate in der Produktion.
### CA-Zertifikat generieren
```bash
# CA privaten Schlüssel erstellen
openssl genrsa -out ca.key 4096
# CA-Zertifikat erstellen
openssl req -new -x509 -days 365 -key ca.key -out ca.crt \
-subj "/C=AT/ST=Vienna/L=Vienna/O=Meldestelle/OU=IT/CN=Meldestelle-CA"
```
### Server-Zertifikate generieren
```bash
# Für jeden Service privaten Schlüssel und Certificate Signing Request generieren
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
-subj "/C=AT/ST=Vienna/L=Vienna/O=Meldestelle/OU=IT/CN=ihre-domain.com"
# Zertifikat mit CA signieren
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt
# Aufräumen
rm server.csr
```
### Diffie-Hellman Parameter generieren
```bash
openssl dhparam -out dhparam.pem 2048
```
## Produktions-Zertifikat Setup
### Option 1: Let's Encrypt (Empfohlen)
Verwenden Sie Certbot, um kostenlose SSL-Zertifikate zu erhalten:
```bash
# Certbot installieren
sudo apt-get install certbot
# Zertifikate erhalten
sudo certbot certonly --standalone -d ihre-domain.com -d www.ihre-domain.com
# Zertifikate in entsprechende Verzeichnisse kopieren
sudo cp /etc/letsencrypt/live/ihre-domain.com/fullchain.pem config/ssl/nginx/server.crt
sudo cp /etc/letsencrypt/live/ihre-domain.com/privkey.pem config/ssl/nginx/server.key
```
### Option 2: Kommerzielle CA
1. Certificate Signing Requests (CSRs) generieren
2. CSRs an Ihre Certificate Authority übermitteln
3. Signierte Zertifikate herunterladen
4. Zertifikate in entsprechende Verzeichnisse platzieren
### Option 3: Interne CA
Bei Verwendung einer internen Certificate Authority:
1. CSRs für jeden Service generieren
2. Zertifikate mit Ihrer internen CA signieren
3. CA-Zertifikat an alle Clients verteilen
## Dateiberechtigungen
Stellen Sie ordnungsgemäße Dateiberechtigungen für die Sicherheit sicher:
```bash
# Restriktive Berechtigungen für private Schlüssel setzen
chmod 600 config/ssl/*/server.key
chmod 600 config/ssl/*/redis.key
chmod 600 config/ssl/*/prometheus.key
# Lesbare Berechtigungen für Zertifikate setzen
chmod 644 config/ssl/*/server.crt
chmod 644 config/ssl/*/ca.crt
# Verzeichnisberechtigungen setzen
chmod 755 config/ssl/*/
```
## Docker Volume Mounts
Die Zertifikate werden als schreibgeschützte Volumes in die Docker-Container eingebunden:
```yaml
volumes:
- ./config/ssl/nginx:/etc/ssl/nginx:ro
- ./config/ssl/keycloak:/opt/keycloak/conf:ro
# ... weitere Mounts
```
## Zertifikat-Erneuerung
### Automatisierte Erneuerung (Let's Encrypt)
Richten Sie einen Cron-Job für automatische Erneuerung ein:
```bash
# Zu Crontab hinzufügen
0 12 * * * /usr/bin/certbot renew --quiet --post-hook "docker-compose -f docker-compose.prod.yml restart nginx"
```
### Manuelle Erneuerung
1. Neue Zertifikate generieren
2. Alte Zertifikate in SSL-Verzeichnissen ersetzen
3. Betroffene Services neu starten:
```bash
docker-compose -f docker-compose.prod.yml restart nginx keycloak grafana prometheus
```
## Sicherheits-Best-Practices
1. **Starke Verschlüsselung verwenden**: Mindestens 2048-Bit RSA-Schlüssel oder 256-Bit ECDSA-Schlüssel verwenden
2. **Regelmäßige Rotation**: Zertifikate regelmäßig rotieren (jährlich oder halbjährlich)
3. **Sichere Speicherung**: Private Schlüssel sicher speichern und Zugriff beschränken
4. **Ablauf überwachen**: Überwachung für Zertifikat-Ablauf einrichten
5. **HSTS verwenden**: HTTP Strict Transport Security aktivieren
6. **Perfect Forward Secrecy**: ECDHE-Cipher-Suites verwenden
7. **Certificate Transparency**: CT-Logs auf unbefugte Zertifikate überwachen
## Fehlerbehebung
### Häufige Probleme
1. **Berechtigung verweigert**
```bash
# Dateiberechtigungen korrigieren
sudo chown -R $USER:$USER config/ssl/
chmod -R 755 config/ssl/
chmod 600 config/ssl/*/server.key
```
2. **Zertifikat-Verifizierung fehlgeschlagen**
```bash
# Zertifikat verifizieren
openssl x509 -in config/ssl/nginx/server.crt -text -noout
# Zertifikatskette prüfen
openssl verify -CAfile config/ssl/nginx/ca.crt config/ssl/nginx/server.crt
```
3. **TLS-Handshake-Fehler**
- Gültigkeitsdaten des Zertifikats prüfen
- Verifizieren, dass Zertifikat zum Hostnamen passt
- Ordnungsgemäße Cipher-Suite-Konfiguration sicherstellen
### SSL-Konfiguration testen
```bash
# SSL-Zertifikat testen
openssl s_client -connect ihre-domain.com:443 -servername ihre-domain.com
# Mit spezifischem Protokoll testen
openssl s_client -connect ihre-domain.com:443 -tls1_2
# Zertifikat-Ablauf prüfen
openssl x509 -in config/ssl/nginx/server.crt -noout -dates
# Zertifikat-Details anzeigen
openssl x509 -in config/ssl/nginx/server.crt -text -noout
```
## Monitoring und Wartung
### Zertifikat-Überwachung
Implementieren Sie Überwachung für:
- Zertifikat-Ablaufdaten
- Zertifikat-Gültigkeit
- SSL/TLS-Handshake-Erfolg
- Cipher-Suite-Verwendung
### Wartungsaufgaben
- Regelmäßige Überprüfung der Zertifikat-Gültigkeit
- Aktualisierung der Cipher-Suites
- Überwachung der Sicherheitsupdates
- Backup der Zertifikate und privaten Schlüssel
## Weitere Ressourcen
- [Mozilla SSL Configuration Generator](https://ssl-config.mozilla.org/)
- [SSL Labs Server Test](https://www.ssllabs.com/ssltest/)
- [Let's Encrypt Dokumentation](https://letsencrypt.org/docs/)
- [OpenSSL Dokumentation](https://www.openssl.org/docs/)
---
**Letzte Aktualisierung**: 25. Juli 2025
Für weitere Informationen zur Produktionsumgebung siehe [README-PRODUCTION.md](../../Tagebuch/README-PRODUCTION.md).
@@ -0,0 +1,141 @@
# ===================================================================
# Multi-stage Dockerfile Template for Kotlin Multiplatform Web Client
# Features: Kotlin/JS compilation, Nginx serving, development support, centralized version management
# Version: 3.0.0 - Central Version Management Implementation
# ===================================================================
# IMPORTANT: Build arguments are now managed centrally via docker/versions.toml
# Use: docker-compose build or scripts/docker-build.sh for automated version injection
# === CENTRALIZED BUILD ARGUMENTS ===
# Values sourced from docker/versions.toml and docker/build-args/
# Global arguments (docker/build-args/global.env)
ARG GRADLE_VERSION
ARG JAVA_VERSION
ARG BUILD_DATE
ARG VERSION
# Client-specific arguments (docker/build-args/clients.env)
ARG NODE_VERSION
ARG NGINX_VERSION
# Client-specific build arguments (can be overridden at build time)
ARG CLIENT_PATH=client/web-app
ARG CLIENT_MODULE=client:web-app
ARG CLIENT_NAME=web-app
# ===================================================================
# Build Stage - Kotlin/JS Compilation
# ===================================================================
FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION}-alpine AS kotlin-builder
# Re-declare build arguments for kotlin-builder stage
ARG CLIENT_PATH=client/web-app
ARG CLIENT_MODULE=client:web-app
ARG CLIENT_NAME=web-app
ARG NODE_VERSION
LABEL stage=kotlin-builder
LABEL maintainer="Meldestelle Development Team"
WORKDIR /workspace
# Install specific Node.js version for Kotlin/JS compatibility
RUN apk add --no-cache wget ca-certificates && \
wget -q -O - https://unofficial-builds.nodejs.org/download/release/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64-musl.tar.xz | \
tar -xJ -C /usr/local --strip-components=1 && \
apk del wget ca-certificates && \
rm -rf /var/cache/apk/* && \
npm config set cache /tmp/.npm-cache && \
npm config set progress false && \
npm config set audit false
# Gradle optimizations for Kotlin Multiplatform builds
ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
-Dorg.gradle.daemon=false \
-Dorg.gradle.parallel=true \
-Dorg.gradle.configureondemand=true \
-Dorg.gradle.jvmargs=-Xmx3g \
-Dkotlin.compiler.execution.strategy=in-process"
# Kotlin/JS and Node.js environment variables
ENV NODE_OPTIONS="--max-old-space-size=4096" \
NPM_CONFIG_CACHE="/tmp/.npm-cache" \
KOTLIN_JS_GENERATE_EXTERNALS=false
# Copy build configuration files first for optimal caching
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
COPY gradle/ gradle/
COPY build.gradle.kts ./
# Copy platform and core dependencies
COPY platform/ platform/
COPY core/ core/
# Copy client modules in dependency order
COPY client/common-ui/ client/common-ui/
COPY ${CLIENT_PATH}/ ${CLIENT_PATH}/
# Clear npm cache and verify Node.js installation
RUN npm cache clean --force && \
node --version && npm --version
# Download dependencies in a separate layer
RUN ./gradlew :${CLIENT_MODULE}:dependencies --no-daemon --info --stacktrace
# Build web application with production optimizations and better error handling
RUN ./gradlew :${CLIENT_MODULE}:jsBrowserProductionWebpack --no-daemon --info --stacktrace --debug
# Verify build output
RUN ls -la /workspace/${CLIENT_PATH}/build/dist/ || (echo "Build failed - no dist directory found" && exit 1)
# ===================================================================
# Production Stage - Nginx serving
# ===================================================================
FROM nginx:${NGINX_VERSION} AS runtime
# Re-declare build arguments for runtime stage
ARG CLIENT_PATH=client/web-app
ARG CLIENT_MODULE=client:web-app
ARG CLIENT_NAME=web-app
# Metadata
LABEL service="${CLIENT_NAME}" \
version="1.0.0" \
description="Kotlin Multiplatform Web Client for Meldestelle" \
maintainer="Meldestelle Development Team"
# Security and system setup
RUN apk update && \
apk upgrade && \
apk add --no-cache curl jq && \
rm -rf /var/cache/apk/*
# Remove default nginx content and logs
RUN rm -rf /usr/share/nginx/html/* && \
rm -f /var/log/nginx/*.log
# Copy built web application from builder stage
COPY --from=kotlin-builder /workspace/${CLIENT_PATH}/build/dist/ /usr/share/nginx/html/
# Copy nginx configuration
COPY ${CLIENT_PATH}/nginx.conf /etc/nginx/nginx.conf
# Set proper permissions for nginx
RUN chown -R nginx:nginx /usr/share/nginx/html /var/cache/nginx /var/run /var/log/nginx && \
chmod -R 755 /usr/share/nginx/html
# Switch to nginx user for security
USER nginx
# Health check specifically for the web application
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD curl -f http://localhost/health || exit 1
# Expose HTTP port
EXPOSE 80
# Start nginx with proper signal handling for graceful shutdowns
STOPSIGNAL SIGQUIT
# Run nginx in foreground with error handling
CMD ["sh", "-c", "nginx -t && exec nginx -g 'daemon off;'"]
@@ -0,0 +1,147 @@
# syntax=docker/dockerfile:1.7
# ===================================================================
# Multi-stage Dockerfile Template for Spring Boot Services
# Features: Security hardening, monitoring support, optimal caching, centralized version management
# Version: 3.0.0 - Central Version Management Implementation
# ===================================================================
# IMPORTANT: Build arguments are now managed centrally via docker/versions.toml
# Use: docker-compose build or scripts/docker-build.sh for automated version injection
# === CENTRALIZED BUILD ARGUMENTS ===
# Values sourced from docker/versions.toml and docker/build-args/
# Global arguments (docker/build-args/global.env)
ARG GRADLE_VERSION
ARG JAVA_VERSION
ARG BUILD_DATE
ARG VERSION
# Service-specific arguments (docker/build-args/services.env or infrastructure.env)
# Note: No runtime profiles/ports as build ARGs
ARG SERVICE_PATH=.
ARG SERVICE_NAME=spring-boot-service
# ===================================================================
# Build Stage
# ===================================================================
FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION}-alpine AS builder
# Re-declare build arguments for this stage
ARG SERVICE_PATH=.
ARG SERVICE_NAME=spring-boot-service
LABEL stage=builder
LABEL maintainer="Meldestelle Development Team"
WORKDIR /workspace
# Gradle optimizations
ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
-Dorg.gradle.daemon=false \
-Dorg.gradle.parallel=true \
-Dorg.gradle.configureondemand=true \
-Xmx2g"
# Copy build files in optimal order for caching
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
COPY gradle/ gradle/
COPY platform/ platform/
COPY build.gradle.kts ./
# Create standalone project structure when using template generically
RUN if [ "${SERVICE_PATH}" = "." ]; then \
echo "Creating isolated standalone Spring Boot application..."; \
mkdir -p /tmp/standalone-app/src/main/kotlin/com/example /tmp/standalone-app/src/main/resources; \
cd /tmp/standalone-app; \
echo 'plugins { id("org.springframework.boot") version "3.2.0"; id("io.spring.dependency-management") version "1.1.4"; kotlin("jvm") version "2.2.0"; kotlin("plugin.spring") version "2.2.0" }' > build.gradle.kts; \
echo 'group = "com.example"; version = "1.0.0"; java { sourceCompatibility = JavaVersion.VERSION_21 }' >> build.gradle.kts; \
echo 'repositories { mavenCentral() }' >> build.gradle.kts; \
echo 'dependencies { implementation("org.springframework.boot:spring-boot-starter-web"); testImplementation("org.springframework.boot:spring-boot-starter-test") }' >> build.gradle.kts; \
echo 'package com.example; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.runApplication; @SpringBootApplication class Application; fun main(args: Array<String>) { runApplication<Application>(*args) }' > src/main/kotlin/com/example/Application.kt; \
echo 'rootProject.name = "standalone-app"' > settings.gradle.kts; \
cp /workspace/gradlew /workspace/gradlew.bat .; \
cp -r /workspace/gradle .; \
echo "Building standalone application..."; \
./gradlew bootJar --no-daemon --info; \
cp build/libs/*.jar /workspace/app.jar; \
else \
echo "Building specific service: ${SERVICE_NAME}"; \
./gradlew :${SERVICE_NAME}:dependencies --no-daemon --info; \
./gradlew :${SERVICE_NAME}:bootJar --no-daemon --info; \
cp ${SERVICE_PATH}/build/libs/*.jar /workspace/app.jar; \
fi
# ===================================================================
# Runtime Stage
# ===================================================================
FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine AS runtime
# Metadata
LABEL service="${SERVICE_NAME}" \
version="1.0.0" \
maintainer="Meldestelle Development Team" \
java.version="${JAVA_VERSION}"
# Build arguments
ARG APP_USER=appuser
ARG APP_GROUP=appgroup
ARG APP_UID=1001
ARG APP_GID=1001
WORKDIR /app
# System setup
RUN apk update && \
apk upgrade && \
apk add --no-cache curl jq tzdata && \
rm -rf /var/cache/apk/*
# Non-root user creation
RUN addgroup -g ${APP_GID} -S ${APP_GROUP} && \
adduser -u ${APP_UID} -S ${APP_USER} -G ${APP_GROUP} -h /app -s /bin/sh
# Directory setup
RUN mkdir -p /app/logs /app/tmp && \
chown -R ${APP_USER}:${APP_GROUP} /app
# Re-declare build arguments for runtime stage
ARG SERVICE_PATH=.
ARG SERVICE_NAME=spring-boot-service
# Copy JAR (different locations for standalone vs service-specific builds)
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} \
/workspace/app.jar app.jar
USER ${APP_USER}
# Expose ports (runtime port configured via environment)
EXPOSE 8080 5005
# Health check
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
CMD curl -fsS --max-time 2 http://localhost:${SERVER_PORT:-8080}/actuator/health/readiness || exit 1
# JVM configuration
ENV JAVA_OPTS="-XX:MaxRAMPercentage=80.0 \
-XX:+UseG1GC \
-XX:+UseStringDeduplication \
-XX:+UseContainerSupport \
-Djava.security.egd=file:/dev/./urandom \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8 \
-Duser.timezone=UTC \
-Dmanagement.endpoints.web.exposure.include=health,info,metrics,prometheus"
# Spring Boot configuration
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS
ENV LOGGING_LEVEL_ROOT=INFO
ENV SERVER_PORT=8080
# Startup command with debug support
ENTRYPOINT ["sh", "-c", "\
if [ \"${DEBUG:-false}\" = \"true\" ]; then \
echo 'Starting ${SERVICE_NAME} in DEBUG mode on port 5005...'; \
exec java $JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar; \
else \
exec java $JAVA_OPTS -jar app.jar; \
fi"]