diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 8b5e777..bb68275 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -42,26 +42,5 @@ services: retries: 3 start_period: 40s - frontend: - build: - context: .. - dockerfile: web/normogen-web/Dockerfile - container_name: normogen-frontend - restart: unless-stopped - ports: - - "8080:8080" - environment: - - BACKEND_URL=http://backend:6500 - - FRONTEND_PORT=8080 - depends_on: - backend: - condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:8080/"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - volumes: mongodb_data: diff --git a/web/frontend-server/Cargo.toml b/web/frontend-server/Cargo.toml deleted file mode 100644 index 00b6189..0000000 --- a/web/frontend-server/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "normogen-frontend-server" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "normogen-frontend-server" -path = "src/main.rs" - -[dependencies] -axum = "0.7" -tokio = { version = "1", features = ["full"] } -tower-http = { version = "0.5", features = ["fs", "trace"] } -reqwest = { version = "0.12", features = ["json"] } diff --git a/web/frontend-server/src/main.rs b/web/frontend-server/src/main.rs deleted file mode 100644 index e55427b..0000000 --- a/web/frontend-server/src/main.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Normogen frontend server — a minimal Axum app that: -//! -//! 1. Reverse-proxies `/api/*` requests to the backend container. -//! 2. Serves the built Vite SPA bundle from `dist/` (static files). -//! 3. Falls back to `index.html` for any unmatched route (SPA history mode). - -use axum::{ - body::Body, - extract::{Request, State}, - http::StatusCode, - response::{IntoResponse, Response}, - routing::any, - Router, -}; -use std::env; -use tower_http::services::{ServeDir, ServeFile}; -use tower_http::trace::TraceLayer; - -#[derive(Clone)] -struct ProxyState { - backend_url: String, - client: reqwest::Client, -} - -#[tokio::main] -async fn main() { - let backend_url = - env::var("BACKEND_URL").unwrap_or_else(|_| "http://backend:6500".to_string()); - let port: u16 = env::var("FRONTEND_PORT") - .unwrap_or_else(|_| "8080".to_string()) - .parse() - .expect("FRONTEND_PORT must be a number"); - - let client = reqwest::Client::builder() - .build() - .expect("failed to build reqwest client"); - - let state = ProxyState { - backend_url: backend_url.clone(), - client, - }; - - // Static file serving with SPA fallback. - let serve_dir = ServeDir::new("dist").fallback(ServeFile::new("dist/index.html")); - - let app = Router::new() - // Proxy /api and everything under /api/ to the backend. - .route("/api", any(proxy_to_backend)) - .route("/api/*path", any(proxy_to_backend)) - .with_state(state) - // Everything else: static files + SPA fallback. - .fallback_service(serve_dir) - .layer(TraceLayer::new_for_http()); - - let addr = format!("0.0.0.0:{port}"); - eprintln!("Frontend server listening on {addr}"); - eprintln!("Proxying /api/* to {backend_url}"); - eprintln!("Serving static files from dist/"); - - let listener = tokio::net::TcpListener::bind(&addr).await.expect("bind failed"); - axum::serve(listener, app).await.expect("server error"); -} - -/// Forward a request to the backend, preserving method, headers, body, and -/// query string. Return the backend's response. -async fn proxy_to_backend( - State(state): State, - req: Request, -) -> Response { - let path = req.uri().path(); - let query = req.uri().query().map(|q| format!("?{q}")).unwrap_or_default(); - let target_url = format!("{}{}{}", state.backend_url, path, query); - - let method = reqwest::Method::from_bytes(req.method().as_str().as_bytes()) - .unwrap_or(reqwest::Method::GET); - - let mut headers = reqwest::header::HeaderMap::new(); - for (name, value) in req.headers().iter() { - if name == "host" { - continue; - } - if let (Ok(n), Ok(v)) = ( - reqwest::header::HeaderName::from_bytes(name.as_str().as_bytes()), - reqwest::header::HeaderValue::from_bytes(value.as_bytes()), - ) { - headers.insert(n, v); - } - } - - let body_bytes = axum::body::to_bytes(req.into_body(), usize::MAX) - .await - .unwrap_or_default(); - - let backend_req = state - .client - .request(method, &target_url) - .headers(headers) - .body(body_bytes); - - let backend_resp = match backend_req.send().await { - Ok(r) => r, - Err(e) => { - eprintln!("Proxy error to {target_url}: {e}"); - return (StatusCode::BAD_GATEWAY, "Failed to reach backend").into_response(); - } - }; - - let status = StatusCode::from_u16(backend_resp.status().as_u16()) - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); - - let mut resp_headers = axum::http::HeaderMap::new(); - for (name, value) in backend_resp.headers().iter() { - if let (Ok(n), Ok(v)) = ( - axum::http::HeaderName::from_bytes(name.as_str().as_bytes()), - axum::http::HeaderValue::from_bytes(value.as_bytes()), - ) { - resp_headers.insert(n, v); - } - } - - let body = backend_resp.bytes().await.unwrap_or_default(); - - let mut response = Response::new(Body::from(body)); - *response.status_mut() = status; - *response.headers_mut() = resp_headers; - response -} diff --git a/web/normogen-web/Dockerfile b/web/normogen-web/Dockerfile deleted file mode 100644 index a456606..0000000 --- a/web/normogen-web/Dockerfile +++ /dev/null @@ -1,55 +0,0 @@ -# Multi-stage Dockerfile for the Normogen frontend server. -# -# Stage 1: Build the Vite SPA bundle. -# Stage 2: Build the Axum frontend-server binary. -# Stage 3: Slim runtime with the binary + static files. - -# ============================================================================ -# Stage 1: Node — build the Vite SPA -# ============================================================================ -FROM node:20-slim AS node-builder - -WORKDIR /app - -# Copy package files and install deps. -COPY web/normogen-web/package.json web/normogen-web/package-lock.json ./ -RUN npm ci - -# Copy the source and build. -COPY web/normogen-web/ ./ -RUN npm run build - -# ============================================================================ -# Stage 2: Rust — build the frontend-server binary -# ============================================================================ -FROM rust:latest AS rust-builder - -WORKDIR /server - -# Copy the frontend-server crate. -COPY web/frontend-server/ ./ - -# Build release. -RUN cargo build --release - -# ============================================================================ -# Stage 3: Runtime — slim image with binary + static files -# ============================================================================ -FROM debian:bookworm-slim - -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -# Copy the frontend-server binary. -COPY --from=rust-builder /server/target/release/normogen-frontend-server /app/frontend-server - -# Copy the built static files. -COPY --from=node-builder /app/dist /app/dist - -EXPOSE 8080 - -CMD ["./frontend-server"]