refactor: separate frontend and backend into independent containers#41
refactor: separate frontend and backend into independent containers#41tusharkhatriofficial merged 1 commit intomainfrom
Conversation
Architecture: - nginx serves React dashboard on port 80 (exposed as 8080) - nginx proxies /api and /ws to Spring Boot backend (internal) - Backend has no exposed ports (all traffic goes through nginx) - SPA routing handled by nginx try_files (no Spring Boot controller needed) Changes: - Removed SpaForwardingController (nginx handles SPA routing) - Created eventara-dashboard/Dockerfile (Node build → nginx) - Created eventara-dashboard/nginx.conf (proxy + SPA fallback) - Reverted backend Dockerfile to 2-stage (Maven → JRE only) - Updated docker-compose.prod.yaml with 5-container setup - CORS set to allow all origins by default Tested locally: all routes (/, /settings, /rules, /analytics, /api, /swagger) return 200.
There was a problem hiding this comment.
Main blocker is production security: allowedOriginPatterns("*") combined with allowCredentials(true) is an unsafe default and should be replaced with a profile/config-driven allowlist. nginx could be tightened for /ws path matching and forwarded headers to avoid subtle WebSocket routing issues. Consider preventing caching of index.html to avoid stale SPA deploys, and improve Maven Docker layer caching to speed rebuilds.
Additional notes (6)
- Security |
src/main/java/com/eventara/config/WebConfig.java:10-16
CORS configuration is effectively "allow any origin" while also enabling credentials.
With .allowCredentials(true) + .allowedOriginPatterns("*"), you are permitting cross-origin credentialed requests from arbitrary origins (subject to Spring’s header behavior). This is a significant security risk in production because cookies/Authorization headers can be sent cross-site, and it undermines the protection CORS is meant to provide.
Even if Spring blocks * with credentials in some modes, the intent here is still dangerously broad and easy to misconfigure later. If nginx is the sole entry point, you can also enforce CORS at nginx, but you still shouldn’t leave the app permissive by default.
- Performance |
eventara-dashboard/nginx.conf:44-47
The nginx config caches*.htmlindirectly via the SPA fallback and sets long-lived caching for assets, but it doesn’t explicitly prevent caching ofindex.html.
If index.html gets cached aggressively by intermediaries, users can get stuck on old bundles after deploy. The assets are immutable (good), but index.html should usually be no-cache/short TTL.
- Performance |
Dockerfile:1-7
The backend Docker build copies onlypom.xmlandsrc/then runsmvn clean package.
This misses common Maven caching optimizations (copy pom.xml, run dependency:go-offline, then copy sources). As-is, every code change invalidates the dependency layer and makes rebuilds slower—especially painful in CI/CD or on VPS rebuilds.
- Performance |
Dockerfile:1-17
The backend Docker build now copies onlypom.xmlandsrc/. This often breaks Maven builds that rely on additional files at repo root (e.g.,.mvn/,mvnw,settings.xml, or extra module directories). If this is a single-module project it’s fine, but if you have any of those, the container build may become subtly non-reproducible compared to local builds.
Also, mvn clean package -DskipTests will re-download dependencies every build; consider a dependency caching step (copy pom first, run mvn -q -DskipTests dependency:go-offline, then copy src).
- Maintainability |
eventara-dashboard/Dockerfile:1-9
npm ci --production=falseis a legacy/ambiguous flag and can behave unexpectedly across npm versions. Also, you’re not leveraging layer caching well: copyingpackage*.jsonfirst is good, but you can further improve reproducibility and build speed by ensuring you’re usingnpm ciwithout extra flags and (optionally) settingNODE_ENV=productiononly for the runtime stage (which is Nginx anyway).
Separately, setting ENV VITE_API_URL="" bakes an empty default into the build. If the app expects runtime configuration, this approach forces rebuilds per environment unless you implement runtime env injection via Nginx templating or a /config.js pattern.
- Compatibility |
docker-compose.prod.yaml:79-79
With the backend no longer publishing a host port, the only intended access path is via Nginx. That’s good, but you should ensure the backend is not accidentally reachable from the host via other means and that Spring’s notion of scheme/host is correct behind the proxy.
Right now Nginx forwards X-Forwarded-* headers, but Spring Boot may not honor them unless server.forward-headers-strategy is configured. This can affect generated OpenAPI server URLs, redirects, and cookie Secure handling.
Summary of changes
Overview
This PR refactors deployment to split the React dashboard and Spring Boot API into separate containers, fronted by an nginx container that serves the SPA and proxies API/WebSocket traffic.
Key changes
- Backend image build simplified
- Root
Dockerfilenow builds only the Spring Boot JAR (removed Node/Vite stage and static asset copy).
- Root
- New dashboard container
- Added
eventara-dashboard/Dockerfile(Node build → nginx runtime). - Added
eventara-dashboard/nginx.confwith:try_files ... /index.htmlSPA fallback- proxy routes for
/api/,/ws,/swagger-ui, and/v3/api-docs
- Added
- Production compose wiring updated
docker-compose.prod.yamlnow introduces adashboardservice as the public entry point and removes host port publishing from the backend.- Backend image renamed to
eventara-api:latest.
- SPA routing moved out of Spring
- Deleted
src/main/java/com/eventara/config/SpaForwardingController.java.
- Deleted
- CORS loosened
WebConfigchanged from explicit origins toallowedOriginPatterns("*").
| # WebSocket proxy — required for real-time dashboard | ||
| location /ws { | ||
| proxy_pass http://eventara:8080; | ||
| proxy_http_version 1.1; | ||
| proxy_set_header Upgrade $http_upgrade; | ||
| proxy_set_header Connection "upgrade"; | ||
| proxy_set_header Host $host; | ||
| proxy_set_header X-Real-IP $remote_addr; | ||
| proxy_read_timeout 86400; | ||
| } |
There was a problem hiding this comment.
location /ws is missing a trailing slash and doesn’t forward standard proxy headers used by many WebSocket/SockJS setups.
As written, /wsfoo would also match, and depending on how the backend expects the path, you can end up with subtle routing issues. Additionally, many reverse-proxy WS configs also set X-Forwarded-For/X-Forwarded-Proto (you do for /api/ but not for /ws).
Suggestion
Harden the WebSocket location matching and align headers with /api/.
Suggested nginx snippet:
location /ws/ {
proxy_pass http://eventara:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}If the backend endpoint is exactly /ws (no trailing slash), consider location = /ws plus location /ws/ to cover both. Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.
| # SPA fallback — serve index.html for all React Router paths | ||
| location / { | ||
| try_files $uri $uri/ /index.html; | ||
| } | ||
|
|
||
| # Cache static assets | ||
| location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { | ||
| expires 30d; | ||
| add_header Cache-Control "public, immutable"; | ||
| } |
There was a problem hiding this comment.
The Nginx config caches *.js/*.css/etc with immutable for 30 days, but it’s placed after the SPA fallback location /. In Nginx, location / is a prefix match; regex locations are only considered if the selected prefix location does not use ^~ (you don’t), so this may still work—but ordering here is easy to misunderstand and can lead to assets being served without the intended caching headers depending on how other locations evolve.
More importantly: try_files $uri $uri/ /index.html; can cause HTML to be served for missing asset paths, which then might get cached incorrectly if the cache headers ever apply. It’s safer to ensure missing static assets return 404 (not index.html).
Suggestion
Harden SPA + asset handling by:
- Moving the static asset location above the SPA fallback, and
- Using
try_files $uri =404;for the asset location so missing assets don’t fall back toindex.html.
Example:
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
try_files $uri =404;
expires 30d;
add_header Cache-Control "public, immutable";
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this nginx.conf adjustment.
Architecture:
Changes:
Tested locally: all routes (/, /settings, /rules, /analytics, /api, /swagger) return 200.