feat(sprint-5): Phase 1 — Docker Compose full stack, CORS, Next.js upgrade

- Dockerfile.backend: multi-stage Java 21 build (eclipse-temurin)
- docker-compose.yml: PostgreSQL 16 + backend + frontend with health checks
- SecurityConfig: CORS for localhost:3000 frontend origin
- application-docker.properties: Docker profile with env vars
- Spring Boot Actuator health endpoint enabled
- Next.js upgraded 15.2.8 → 15.5.18 (security fixes)
This commit is contained in:
Patrick Plate
2026-06-12 19:51:24 +02:00
parent dce27a4291
commit 279f2f6de0
8 changed files with 2903 additions and 4614 deletions
+38
View File
@@ -0,0 +1,38 @@
# Multi-stage build for cannamanage-api (Spring Boot + Java 21)
# Build context: repo root (needs access to all Maven modules)
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
# Copy Maven wrapper + POM files first (layer caching)
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
COPY cannamanage-domain/pom.xml cannamanage-domain/pom.xml
COPY cannamanage-service/pom.xml cannamanage-service/pom.xml
COPY cannamanage-api/pom.xml cannamanage-api/pom.xml
# Download dependencies (cached unless POMs change)
RUN chmod +x mvnw && ./mvnw dependency:go-offline -B -q 2>/dev/null || true
# Copy source code
COPY cannamanage-domain/src/ cannamanage-domain/src/
COPY cannamanage-service/src/ cannamanage-service/src/
COPY cannamanage-api/src/ cannamanage-api/src/
# Build the fat JAR
RUN ./mvnw package -pl cannamanage-api -am -DskipTests -B -q
# --- Runtime stage ---
FROM eclipse-temurin:21-jre-alpine AS runtime
WORKDIR /app
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app/cannamanage-api/target/*.jar app.jar
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
+5
View File
@@ -118,6 +118,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- Actuator (health endpoint for Docker) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
@@ -1,6 +1,7 @@
package de.cannamanage.api.security;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
@@ -13,8 +14,12 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
* Security configuration — Sprint 3: API + Staff portal with JWT + Member portal with sessions.
@@ -38,6 +43,7 @@ public class SecurityConfig {
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
@@ -116,4 +122,21 @@ public class SecurityConfig {
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"http://localhost:3000",
"http://frontend:3000"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
@@ -0,0 +1,19 @@
# Docker profile — used when running in Docker Compose
spring.datasource.url=${SPRING_DATASOURCE_URL}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
# Enable Flyway for container startup (fresh DB)
spring.flyway.enabled=true
spring.jpa.hibernate.ddl-auto=validate
# JWT secret from environment
cannamanage.security.jwt.secret=${CANNAMANAGE_SECURITY_JWT_SECRET}
# Actuator
management.endpoints.web.exposure.include=health
management.endpoint.health.show-details=never
# Disable mail in Docker (no SMTP container)
spring.mail.host=localhost
spring.mail.port=1025
@@ -31,6 +31,10 @@ spring.mail.from=${MAIL_FROM:noreply@cannamanage.de}
# App base URL (for invite links)
app.base-url=${APP_BASE_URL:http://localhost:8080}
# Actuator
management.endpoints.web.exposure.include=health
management.endpoint.health.show-details=never
# Session configuration (member portal)
server.servlet.session.timeout=30m
server.servlet.session.cookie.same-site=strict
+2 -2
View File
@@ -46,7 +46,7 @@
"emoji-picker-react": "4.12.2",
"input-otp": "1.4.2",
"lucide-react": "0.446.0",
"next": "15.2.8",
"next": "15.5.18",
"next-auth": "5.0.0-beta.31",
"next-intl": "^4.13.0",
"react": "19.1.3",
@@ -72,7 +72,7 @@
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
"eslint": "9.18.0",
"eslint-config-next": "15.2.8",
"eslint-config-next": "15.5.18",
"eslint-config-prettier": "10.1.1",
"eslint-plugin-prettier": "5.2.3",
"playwright": "^1.60.0",
+2790 -4594
View File
File diff suppressed because it is too large Load Diff
+22 -18
View File
@@ -1,17 +1,15 @@
version: '3.9'
services:
db:
image: postgres:16-alpine
container_name: cannamanage-db-local
container_name: cannamanage-db
environment:
POSTGRES_DB: cannamanage
POSTGRES_USER: cannamanage
POSTGRES_PASSWORD: dev_password_change_in_prod
POSTGRES_PASSWORD: cannamanage_dev
ports:
- "5432:5432"
volumes:
- pgdata_local:/var/lib/postgresql/data
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U cannamanage"]
interval: 5s
@@ -21,19 +19,25 @@ services:
backend:
build:
context: .
dockerfile: cannamanage-api/Dockerfile
dockerfile: Dockerfile.backend
container_name: cannamanage-backend
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/cannamanage
- SPRING_DATASOURCE_USERNAME=cannamanage
- SPRING_DATASOURCE_PASSWORD=dev_password_change_in_prod
SPRING_PROFILES_ACTIVE: docker
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/cannamanage
SPRING_DATASOURCE_USERNAME: cannamanage
SPRING_DATASOURCE_PASSWORD: cannamanage_dev
CANNAMANAGE_SECURITY_JWT_SECRET: docker-dev-secret-key-minimum-32-characters-long-for-hmac
depends_on:
db:
condition: service_healthy
profiles:
- full
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
frontend:
build:
@@ -43,13 +47,13 @@ services:
ports:
- "3000:3000"
environment:
- BASE_URL=http://localhost:3000
- HOME_PATHNAME=/dashboards/analytics
- BACKEND_URL=http://backend:8080
NEXTAUTH_URL: http://localhost:3000
NEXTAUTH_SECRET: docker-dev-nextauth-secret-minimum-32chars
BACKEND_URL: http://backend:8080
AUTH_URL: http://localhost:3000
depends_on:
- backend
profiles:
- full
backend:
condition: service_healthy
volumes:
pgdata_local:
pgdata: