Add application-prod.yaml with secure Spring Boot production defaults #137

Open
opened 2026-03-28 08:52:29 +01:00 by marcel · 1 comment
Owner

Why

The backend currently has only two Spring profiles: a base application.yaml and application-dev.yaml. There is no production profile. This means:

  • Swagger/OpenAPI UI is enabled in production — /swagger-ui.html and /v3/api-docs are publicly accessible, exposing the full API surface.
  • Actuator endpoints are wide open/actuator/env, /actuator/beans, /actuator/mappings etc. leak internal configuration, classpath details, and route information.
  • SQL logging (enabled in the dev profile) may accidentally end up in production if the profile isn't set correctly.
  • Hibernate DDL auto — if left at anything other than validate or none, a misconfiguration could silently alter the schema.
  • JPA open-session-in-view is likely enabled (Spring Boot default), which causes N+1 queries in production REST controllers.

What to do

Create backend/src/main/resources/application-prod.yaml.

# ── Swagger / OpenAPI ────────────────────────────────────────────────────────
springdoc:
  api-docs:
    enabled: false
  swagger-ui:
    enabled: false

# ── Actuator ─────────────────────────────────────────────────────────────────
management:
  server:
    port: 8081           # management on a separate port — never exposed through Caddy
  endpoints:
    web:
      exposure:
        include: health,prometheus   # only these two are needed
  endpoint:
    health:
      show-details: never            # don't leak DB/S3 connection details in health response

# ── Database ──────────────────────────────────────────────────────────────────
spring:
  jpa:
    show-sql: false
    open-in-view: false              # prevents N+1 lazy-load issues in REST controllers
    hibernate:
      ddl-auto: validate             # Flyway owns schema changes — Hibernate must not touch it

# ── Logging ───────────────────────────────────────────────────────────────────
logging:
  level:
    root: WARN
    org.raddatz.familienarchiv: INFO
    org.springframework.web: WARN
    org.hibernate: WARN
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n"

Notes

  • The management port 8081 should not be mapped in docker-compose.prod.yml — it stays internal. Prometheus scrapes it over the Docker network directly (http://backend:8081/actuator/prometheus).
  • open-in-view: false may expose lazy-loading bugs that are currently masked. Run the E2E test suite after enabling it to catch any LazyInitializationException issues.
  • ddl-auto: validate will cause startup failure if the Flyway migration history is out of sync with the entity model — this is intentional. It's better to fail fast at startup than to silently corrupt the schema.

Activation

Activate by setting SPRING_PROFILES_ACTIVE=prod in the production environment (handled in docker-compose.prod.yml — see #136).

Acceptance criteria

  • GET /swagger-ui/index.html returns 404 when running with prod profile.
  • GET /actuator/env returns 404 when running with prod profile.
  • GET /actuator/health returns {"status":"UP"} (no details).
  • GET /actuator/prometheus (on port 8081) returns Prometheus metrics.
  • Application starts cleanly with prod profile and all E2E tests pass.
## Why The backend currently has only two Spring profiles: a base `application.yaml` and `application-dev.yaml`. There is no production profile. This means: - **Swagger/OpenAPI UI is enabled** in production — `/swagger-ui.html` and `/v3/api-docs` are publicly accessible, exposing the full API surface. - **Actuator endpoints are wide open** — `/actuator/env`, `/actuator/beans`, `/actuator/mappings` etc. leak internal configuration, classpath details, and route information. - **SQL logging** (enabled in the dev profile) may accidentally end up in production if the profile isn't set correctly. - **Hibernate DDL auto** — if left at anything other than `validate` or `none`, a misconfiguration could silently alter the schema. - JPA open-session-in-view is likely enabled (Spring Boot default), which causes N+1 queries in production REST controllers. ## What to do Create `backend/src/main/resources/application-prod.yaml`. ```yaml # ── Swagger / OpenAPI ──────────────────────────────────────────────────────── springdoc: api-docs: enabled: false swagger-ui: enabled: false # ── Actuator ───────────────────────────────────────────────────────────────── management: server: port: 8081 # management on a separate port — never exposed through Caddy endpoints: web: exposure: include: health,prometheus # only these two are needed endpoint: health: show-details: never # don't leak DB/S3 connection details in health response # ── Database ────────────────────────────────────────────────────────────────── spring: jpa: show-sql: false open-in-view: false # prevents N+1 lazy-load issues in REST controllers hibernate: ddl-auto: validate # Flyway owns schema changes — Hibernate must not touch it # ── Logging ─────────────────────────────────────────────────────────────────── logging: level: root: WARN org.raddatz.familienarchiv: INFO org.springframework.web: WARN org.hibernate: WARN pattern: console: "%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n" ``` ## Notes - The management port `8081` should **not** be mapped in `docker-compose.prod.yml` — it stays internal. Prometheus scrapes it over the Docker network directly (`http://backend:8081/actuator/prometheus`). - `open-in-view: false` may expose lazy-loading bugs that are currently masked. Run the E2E test suite after enabling it to catch any `LazyInitializationException` issues. - `ddl-auto: validate` will cause startup failure if the Flyway migration history is out of sync with the entity model — this is intentional. It's better to fail fast at startup than to silently corrupt the schema. ## Activation Activate by setting `SPRING_PROFILES_ACTIVE=prod` in the production environment (handled in `docker-compose.prod.yml` — see #136). ## Acceptance criteria - `GET /swagger-ui/index.html` returns 404 when running with `prod` profile. - `GET /actuator/env` returns 404 when running with `prod` profile. - `GET /actuator/health` returns `{"status":"UP"}` (no details). - `GET /actuator/prometheus` (on port 8081) returns Prometheus metrics. - Application starts cleanly with `prod` profile and all E2E tests pass.
marcel added the devopsphase-4: spring-prod-profile labels 2026-03-28 10:46:43 +01:00
Author
Owner

Audit-derived AC additions (2026-05-07)

The audit surfaced two prod-profile gaps that fit naturally inside this issue's scope. Folding them in here avoids creating fragmented small issues.

F-10 — HikariCP pool tuning

backend/src/main/resources/application.yaml currently sets only url/username/password/driver-class-name — Hikari falls through to defaults (maximumPoolSize=10). Spring Session JDBC, Flyway, JPA, and audit-log writes all share this pool. Add to application-prod.yaml:

spring:
  datasource:
    hikari:
      maximum-pool-size: 20             # ((cores * 2) + 1) for typical workloads
      minimum-idle: 5
      connection-timeout: 5000           # 5s — fail fast, don't hang threads
      max-lifetime: 1700000              # < Postgres idle_in_transaction_session_timeout
      idle-timeout: 300000               # 5min
      leak-detection-threshold: 30000    # 30s — surfaces forgotten unclosed connections in tests
      validation-timeout: 3000

F-30 — JSON structured logging + MDC trace IDs

Currently plain text via Logback default. Add logback-spring.xml:

<configuration>
  <springProfile name="prod">
    <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
      <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <includeMdcKeyName>traceId</includeMdcKeyName>
        <includeMdcKeyName>spanId</includeMdcKeyName>
        <includeMdcKeyName>userId</includeMdcKeyName>
      </encoder>
    </appender>
    <root level="INFO"><appender-ref ref="JSON"/></root>
  </springProfile>
</configuration>

Add MDCInsertingServletFilter so request-scoped trace IDs flow into every log line. Pair with #140 (Prometheus/Loki/Grafana) so Loki parses JSON natively.

Other prod-profile defaults to bake in here

  • spring.jpa.show-sql: false (already correct in dev).
  • spring.flyway.validate-migrations-on-startup: true.
  • server.shutdown: graceful + spring.lifecycle.timeout-per-shutdown-phase: 30s.
  • management.endpoints.web.exposure.include: health,info,prometheus (no wildcards) — coordinates with #87.
  • server.error.include-stacktrace: never, server.error.include-message: never for prod.
  • springdoc.api-docs.enabled: false already correct in base profile; explicit in prod for safety.

Tracked in audit doc as F-10 + F-30 (both High). See docs/audits/2026-05-07-pre-prod-architectural-review.md.

## Audit-derived AC additions (2026-05-07) The audit surfaced two prod-profile gaps that fit naturally inside this issue's scope. Folding them in here avoids creating fragmented small issues. ### F-10 — HikariCP pool tuning `backend/src/main/resources/application.yaml` currently sets only `url`/`username`/`password`/`driver-class-name` — Hikari falls through to defaults (`maximumPoolSize=10`). Spring Session JDBC, Flyway, JPA, and audit-log writes all share this pool. Add to `application-prod.yaml`: ```yaml spring: datasource: hikari: maximum-pool-size: 20 # ((cores * 2) + 1) for typical workloads minimum-idle: 5 connection-timeout: 5000 # 5s — fail fast, don't hang threads max-lifetime: 1700000 # < Postgres idle_in_transaction_session_timeout idle-timeout: 300000 # 5min leak-detection-threshold: 30000 # 30s — surfaces forgotten unclosed connections in tests validation-timeout: 3000 ``` ### F-30 — JSON structured logging + MDC trace IDs Currently plain text via Logback default. Add `logback-spring.xml`: ```xml <configuration> <springProfile name="prod"> <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <includeMdcKeyName>traceId</includeMdcKeyName> <includeMdcKeyName>spanId</includeMdcKeyName> <includeMdcKeyName>userId</includeMdcKeyName> </encoder> </appender> <root level="INFO"><appender-ref ref="JSON"/></root> </springProfile> </configuration> ``` Add `MDCInsertingServletFilter` so request-scoped trace IDs flow into every log line. Pair with #140 (Prometheus/Loki/Grafana) so Loki parses JSON natively. ### Other prod-profile defaults to bake in here - [ ] `spring.jpa.show-sql: false` (already correct in dev). - [ ] `spring.flyway.validate-migrations-on-startup: true`. - [ ] `server.shutdown: graceful` + `spring.lifecycle.timeout-per-shutdown-phase: 30s`. - [ ] `management.endpoints.web.exposure.include: health,info,prometheus` (no wildcards) — coordinates with #87. - [ ] `server.error.include-stacktrace: never`, `server.error.include-message: never` for prod. - [ ] `springdoc.api-docs.enabled: false` already correct in base profile; explicit in prod for safety. Tracked in audit doc as **F-10** + **F-30** (both High). See `docs/audits/2026-05-07-pre-prod-architectural-review.md`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#137