# Single-Container Deployment: Spring Boot + Vue SPA **Date:** 2026-03-04 **Context:** T-2 research — how to serve a Spring Boot API and Vue SPA from one Docker container. ## The Three Approaches ### Approach A: Spring Boot Serves Static Files (Recommended for fete) Frontend `dist/` is built by Vite and copied into Spring Boot's `classpath:/static/` during the Docker build. One JAR serves everything. **Pros:** One process, one port, one health check. No process manager. The JAR is the single artifact. **Cons:** Embedded Tomcat is not optimized for static files (no `sendfile`, no `gzip_static`). Irrelevant at fete's scale (tens to hundreds of users). **Real-world examples:** [jonashackt/spring-boot-vuejs](https://github.com/jonashackt/spring-boot-vuejs), [georgwittberger/spring-boot-embedded-spa-example](https://github.com/georgwittberger/spring-boot-embedded-spa-example), [bootify-io/spring-boot-react-example](https://github.com/bootify-io/spring-boot-react-example). ### Approach B: nginx + Spring Boot (Two Processes) nginx serves static files, proxies `/api` to Spring Boot. Needs supervisord or a wrapper script. **Pros:** nginx is battle-tested for static files (gzip, caching, HTTP/2). Clean separation. **Cons:** Two processes, supervisord complexity, partial-failure detection issues, larger image. **When to use:** High-traffic apps where static file performance matters. ### Approach C: Separate Containers Not relevant — violates the single-container requirement. ## The Context-Path Problem Current config: `server.servlet.context-path=/api`. This scopes **everything** under `/api`, including static resources. Frontend at `classpath:/static/` would be served at `/api/index.html`, not `/`. ### Solution: Remove context-path, use `addPathPrefix` Remove `server.servlet.context-path=/api`. Instead, use Spring's `PathMatchConfigurer`: ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.addPathPrefix("/api", c -> c.isAnnotationPresent(RestController.class)); } } ``` Result: - API: `/api/...` (only `@RestController` classes) - Static files: `/` (from `classpath:/static/`) - Actuator: `/actuator/health` (outside `/api` — correct for infra health checks) - SPA forwarding controller (`@Controller`, not `@RestController`) is not prefixed OpenAPI spec keeps `servers: [{url: /api}]`. Frontend client keeps `baseUrl: "/api"`. No changes needed. ## SPA Routing (Vue Router History Mode) Vue Router in history mode uses real URL paths. The server must return `index.html` for all paths that aren't API endpoints or actual static files. ### Recommended: Custom `PathResourceResolver` ```java @Configuration public class SpaConfig implements WebMvcConfigurer { @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"); } }); } } ``` Requests flow: `@RestController` match → serve API. No match → resource handler → file exists → serve file. No file → serve `index.html` → Vue Router handles route. ## Multi-Stage Dockerfile Pattern ```dockerfile # 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/ ./ 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) FROM eclipse-temurin:25-jdk-alpine AS backend-build WORKDIR /app COPY backend/ backend/ COPY --from=frontend-build /app/frontend/dist backend/src/main/resources/static/ WORKDIR /app/backend RUN ./mvnw -B -DskipTests package # Stage 3: Runtime FROM eclipse-temurin:25-jre-alpine WORKDIR /app COPY --from=backend-build /app/backend/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] ``` Key decisions: - Frontend built first (needs `api.yaml` for type generation) - `npm ci` before `COPY frontend/` for Docker layer caching - Frontend output copied into `backend/src/main/resources/static/` before Maven build → ends up in JAR - `-DskipTests` in Docker build (tests run in CI) - JRE-only final image (no JDK, no Node) - Alpine-based for minimal size ## Open-Source Consensus All surveyed Spring Boot + SPA projects use Approach A. None use supervisord + nginx for small-to-medium apps. ## Sources - [jonashackt/spring-boot-vuejs](https://github.com/jonashackt/spring-boot-vuejs) - [georgwittberger/spring-boot-embedded-spa-example](https://github.com/georgwittberger/spring-boot-embedded-spa-example) - [Bundling React/Vite with Spring Boot (jessym.com)](https://www.jessym.com/articles/bundling-react-vite-with-spring-boot) - [Spring Framework PathMatchConfigurer docs](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/path-matching.html) - [Docker: Multi-service containers](https://docs.docker.com/engine/containers/multi-service_container/)