Files
nitrix 6aeb4b8bca Migrate project artifacts to spec-kit format
- Move cross-cutting docs (personas, design system, implementation phases,
  Ideen.md) to .specify/memory/
- Move cross-cutting research and plans to .specify/memory/research/ and
  .specify/memory/plans/
- Extract 5 setup tasks from spec/setup-tasks.md into individual
  specs/001-005/spec.md files with spec-kit template format
- Extract 20 user stories from spec/userstories.md into individual
  specs/006-026/spec.md files with spec-kit template format
- Relocate feature-specific research and plan docs into specs/[feature]/
- Add spec-kit constitution, templates, scripts, and slash commands
- Slim down CLAUDE.md to Claude-Code-specific config, delegate principles
  to .specify/memory/constitution.md
- Update ralph.sh with stream-json output and per-iteration logging
- Delete old spec/ and docs/agents/ directories
- Gitignore Ralph iteration JSONL logs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:19:41 +01:00

15 KiB

date, git_commit, branch, topic, tags, status
date git_commit branch topic tags status
2026-03-04T17:40:16+00:00 96ef8656bd master T-2: Docker Deployment Setup
plan
docker
deployment
spring-boot
spa
draft

T-2: Docker Deployment Setup

Overview

Create a multi-stage Dockerfile that builds both backend (Spring Boot) and frontend (Vue 3) and produces a single runnable container. Spring Boot serves the SPA's static files directly — one process, one port, one JAR. This requires migrating from server.servlet.context-path=/api to addPathPrefix and adding SPA forwarding so Vue Router's history mode works.

Current State Analysis

What exists:

  • Backend: Spring Boot 3.5.11, Java 25, Maven with wrapper, Actuator health endpoint
  • Frontend: Vue 3, Vite, TypeScript, Vue Router (history mode), openapi-fetch client
  • server.servlet.context-path=/api scopes everything under /api (including static resources)
  • Health test at backend/src/test/java/de/fete/FeteApplicationTest.java:27 uses get("/actuator/health")
  • OpenAPI spec at backend/src/main/resources/openapi/api.yaml has servers: [{url: /api}]
  • Frontend client at frontend/src/api/client.ts uses baseUrl: "/api"
  • ArchUnit registers de.fete.config.. as an adapter package — config classes belong there
  • package-lock.json exists (needed for npm ci)
  • Maven wrapper .properties present (JAR downloads automatically)
  • No Dockerfile, no .dockerignore exist yet

What's missing:

  • Dockerfile (multi-stage build)
  • .dockerignore
  • WebConfig to replace context-path with addPathPrefix
  • SPA forwarding config (so Vue Router history mode works when served by Spring Boot)

Key Discoveries:

  • FeteApplicationTest.java:27: MockMvc test uses get("/actuator/health") — path is relative to context path in MockMvc, so removing context-path does NOT break this test
  • api.yaml:11: OpenAPI servers: [{url: /api}] — stays unchanged, addPathPrefix produces the same routing
  • client.ts:4: baseUrl: "/api" — stays unchanged
  • HexagonalArchitectureTest.java:22: config is an adapter in ArchUnit → new config classes go in de.fete.config
  • router/index.ts:5: Uses createWebHistory → history mode, needs server-side SPA forwarding

Desired End State

After implementation:

  1. docker build -t fete . succeeds at the repo root
  2. docker run -p 8080:8080 fete starts the container
  3. curl http://localhost:8080/actuator/health returns {"status":"UP"}
  4. curl http://localhost:8080/ returns the Vue SPA's index.html
  5. curl http://localhost:8080/some/spa/route also returns index.html (SPA forwarding)
  6. curl http://localhost:8080/assets/... returns actual static files
  7. All existing tests still pass (./mvnw test and npm run test:unit)

What We're NOT Doing

  • Database wiring (DATABASE_URL, JPA, Flyway) — deferred to T-4
  • docker-compose example — deferred to T-4
  • Environment variable configuration (Unsplash key, max events) — deferred to T-4
  • README deployment documentation — deferred to T-4
  • Production hardening (JVM flags, memory limits, graceful shutdown) — not in scope
  • TLS/HTTPS — hoster's responsibility (reverse proxy)
  • gzip compression config — premature optimization at this scale

Implementation Approach

Three phases, strictly sequential: first the Spring Boot config changes (testable locally without Docker), then the Dockerfile and .dockerignore, then end-to-end verification.


Phase 1: Spring Boot Configuration Changes

Overview

Replace context-path=/api with addPathPrefix so that API endpoints live under /api/* but static resources and SPA routes are served at /. Add SPA forwarding so non-API, non-static requests return index.html.

Changes Required:

[x] 1. Remove context-path from application.properties

File: backend/src/main/resources/application.properties Changes: Remove server.servlet.context-path=/api. Explicitly set Actuator base path to keep it outside /api.

spring.application.name=fete

management.endpoints.web.exposure.include=health
management.endpoint.health.show-details=never

Note: Actuator defaults to /actuator which is exactly where we want it (outside /api). No extra config needed.

[x] 2. Create WebConfig with addPathPrefix

File: backend/src/main/java/de/fete/config/WebConfig.java Changes: New @Configuration class that prefixes only @RestController handlers with /api.

package de.fete.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.io.IOException;

/** Configures API path prefix and SPA static resource serving. */
@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void configurePathMatch(PathMatchConfigurer configurer) {
    configurer.addPathPrefix("/api",
        c -> c.isAnnotationPresent(RestController.class));
  }

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/**")
        .addResourceLocations("classpath:/static/")
        .resourceChain(true)
        .addResolver(new PathResourceResolver() {
          @Override
          protected Resource getResource(String resourcePath,
              Resource location) throws IOException {
            Resource requested = location.createRelative(resourcePath);
            return (requested.exists() && requested.isReadable())
                ? requested
                : new ClassPathResource("/static/index.html");
          }
        });
  }
}

This single class handles both concerns:

  • configurePathMatch: prefixes @RestController endpoints with /api
  • addResourceHandlers: serves static files from classpath:/static/, falls back to index.html for SPA routes

The SPA forwarding only activates when classpath:/static/index.html exists (i.e. in the Docker image where the frontend is bundled). During local backend development without frontend assets, requests to / will simply 404 as before — no behavior change for the dev workflow.

[x] 3. Write test for WebConfig behavior

File: backend/src/test/java/de/fete/config/WebConfigTest.java Changes: New test class verifying the /api prefix routing and actuator independence.

package de.fete.config;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
class WebConfigTest {

  @Autowired
  private MockMvc mockMvc;

  @Test
  void actuatorHealthIsOutsideApiPrefix() throws Exception {
    mockMvc.perform(get("/actuator/health"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.status").value("UP"));
  }

  @Test
  void apiPrefixNotAccessibleWithoutIt() throws Exception {
    // /health without /api prefix should not resolve to the API endpoint
    mockMvc.perform(get("/health"))
        .andExpect(status().isNotFound());
  }
}

Note: The existing FeteApplicationTest.healthEndpointReturns200() already tests /actuator/health. Since we're removing context-path but MockMvc paths are relative to the servlet context anyway, the existing test continues to pass without changes. The new test adds explicit verification that the /api prefix isolation works correctly.

Success Criteria:

Automated Verification:

  • cd backend && ./mvnw test — all tests pass (existing + new)
  • cd backend && ./mvnw checkstyle:check — no style violations

Manual Verification:

  • cd backend && ./mvnw spring-boot:run, then curl http://localhost:8080/actuator/health returns {"status":"UP"}

Implementation Note: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.


Phase 2: Dockerfile and .dockerignore

Overview

Create the multi-stage Dockerfile (frontend build → backend build with static assets → JRE runtime) and a .dockerignore to keep the build context clean.

Changes Required:

[x] 1. Create .dockerignore

File: .dockerignore Changes: Exclude build artifacts, dependencies, IDE files, and dev-only files from the Docker build context.

# Build artifacts
**/target/
**/dist/
**/build/

# Dependencies (rebuilt inside Docker)
**/node_modules/

# IDE
.idea/
.vscode/
**/*.iml

# Git
.git/
.gitignore

# CI/CD
.gitea/

# Agent/dev files
.claude/
.ralph/
.rodney/
.agent-tests/
docs/
spec/

# OS files
.DS_Store
Thumbs.db

# Environment
.env
.env.*

# Generated files (rebuilt in Docker)
frontend/src/api/schema.d.ts

[x] 2. Create multi-stage Dockerfile

File: Dockerfile Changes: Three-stage build — frontend, backend, runtime.

# Stage 1: Build frontend
FROM node:24-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
# OpenAPI spec needed for type generation (npm run build runs generate:api)
COPY backend/src/main/resources/openapi/api.yaml \
     ../backend/src/main/resources/openapi/api.yaml
RUN npm run build

# Stage 2: Build backend with frontend assets baked in
FROM eclipse-temurin:25-jdk-alpine AS backend-build
WORKDIR /app/backend
COPY backend/ ./
COPY --from=frontend-build /app/frontend/dist src/main/resources/static/
RUN ./mvnw -B -DskipTests -Dcheckstyle.skip -Dspotbugs.skip package

# Stage 3: Runtime
FROM eclipse-temurin:25-jre-alpine
WORKDIR /app
COPY --from=backend-build /app/backend/target/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]

Design decisions:

  • Frontend first: npm run build triggers generate:api which needs api.yaml — copied from the build context
  • Layer caching: npm ci before COPY frontend/ so dependency install is cached unless package*.json changes
  • -DskipTests -Dcheckstyle.skip -Dspotbugs.skip: Tests run in CI (T-3), not during image build
  • JRE-only runtime: No JDK, no Node, no Maven in the final image
  • Alpine images: Minimal size
  • HEALTHCHECK directive: Docker-native health checking via Actuator

Note on Java 25: eclipse-temurin:25-jdk-alpine / eclipse-temurin:25-jre-alpine may not yet be published. If unavailable, fall back to eclipse-temurin:21-jdk-alpine / eclipse-temurin:21-jre-alpine (current LTS). The Java version in the Dockerfile does not need to match the development Java version exactly — Spring Boot 3.5.x runs on both 21 and 25.

Success Criteria:

Automated Verification:

  • docker build -t fete . succeeds without errors

Manual Verification:

  • docker run --rm -p 8080:8080 fete starts the container
  • curl http://localhost:8080/actuator/health returns {"status":"UP"}
  • curl http://localhost:8080/ returns HTML (the Vue SPA's index.html)
  • docker images fete --format '{{.Size}}' shows a reasonable size (< 400MB)

Implementation Note: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.


Phase 3: Finalize and Verify

Overview

Run all existing tests to confirm no regressions, check off T-2 acceptance criteria in the spec, and commit.

Changes Required:

[x] 1. Check off T-2 acceptance criteria

File: spec/setup-tasks.md Changes: Mark completed acceptance criteria for T-2.

[x] 2. Commit the research report

File: docs/agents/research/2026-03-04-spa-springboot-docker-patterns.md Changes: Stage and commit the already-written research report alongside the implementation.

Success Criteria:

Automated Verification:

  • cd backend && ./mvnw test — all tests pass
  • cd frontend && npm run test:unit -- --run — all tests pass
  • cd backend && ./mvnw verify — full verification including SpotBugs
  • docker build -t fete . — still builds cleanly

Manual Verification:

  • Container starts and health check responds
  • SPA is accessible at http://localhost:8080/
  • All T-2 acceptance criteria in spec/setup-tasks.md are checked off

Testing Strategy

Unit Tests:

  • WebConfigTest.actuatorHealthIsOutsideApiPrefix() — actuator accessible at /actuator/health
  • WebConfigTest.apiPrefixNotAccessibleWithoutIt()/health without prefix returns 404
  • Existing FeteApplicationTest.healthEndpointReturns200() — regression test (unchanged)

Integration Tests:

  • Docker build succeeds end-to-end
  • Container starts and serves both API and static content

Manual Testing Steps:

  1. docker build -t fete .
  2. docker run --rm -p 8080:8080 fete
  3. Open http://localhost:8080/ in browser — should see Vue SPA
  4. Open http://localhost:8080/actuator/health — should see {"status":"UP"}
  5. Navigate to a non-existent SPA route like http://localhost:8080/events/test — should still see Vue SPA (SPA forwarding)

Migration Notes

  • server.servlet.context-path=/api is removed → any external clients that previously called http://host:8080/api/actuator/health must now call http://host:8080/actuator/health. Since the app is not yet deployed, this has zero impact.
  • The OpenAPI spec's servers: [{url: /api}] is unchanged. The addPathPrefix produces identical routing to the old context-path for all @RestController endpoints.

References