Compare commits
No commits in common. "a5006bff6347388865670244b8c15d3d0e6d02c5" and "43a427e2dd7464c406d12da8ee4ebbdfe8898789" have entirely different histories.
a5006bff63
...
43a427e2dd
4 changed files with 0 additions and 217 deletions
|
|
@ -42,26 +42,5 @@ services:
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
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:
|
volumes:
|
||||||
mongodb_data:
|
mongodb_data:
|
||||||
|
|
|
||||||
|
|
@ -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"] }
|
|
||||||
|
|
@ -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<ProxyState>,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -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"]
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue