Add Axum backend, Dockerfiles, and K8s deployment manifests
- Axum backend server: health check, transfer matrix cache (base64 BYTEA), route cache (JSONB), CORS, gzip compression, tracing - Postgres schema: transfer_matrices + cached_routes tables with upserts - Dockerfile.frontend: 3-stage (wasm-pack → SvelteKit → nginx:alpine) - Dockerfile.backend: 2-stage (rust build → debian:bookworm-slim) - nginx.conf: SPA fallback, WASM mime type, /api proxy to backend - docker-compose.yml: Postgres + backend for local development - K8s manifests: namespace, frontend/backend deployments with services, ingress routing, health probes, secret-based DATABASE_URL Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
14
Dockerfile.backend
Normal file
14
Dockerfile.backend
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Stage 1: Build the backend binary
|
||||||
|
FROM rust:1.82-slim AS builder
|
||||||
|
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /app
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY crates/ crates/
|
||||||
|
RUN cargo build --release --bin mass-driver-backend
|
||||||
|
|
||||||
|
# Stage 2: Minimal runtime image
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=builder /app/target/release/mass-driver-backend /usr/local/bin/mass-driver-backend
|
||||||
|
EXPOSE 3001
|
||||||
|
CMD ["mass-driver-backend"]
|
||||||
24
Dockerfile.frontend
Normal file
24
Dockerfile.frontend
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Stage 1: Build WASM package
|
||||||
|
FROM rust:1.82-slim AS wasm-builder
|
||||||
|
RUN apt-get update && apt-get install -y curl pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
|
||||||
|
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||||
|
WORKDIR /app
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY crates/ crates/
|
||||||
|
RUN wasm-pack build crates/mass-driver-wasm --target web --out-dir pkg
|
||||||
|
|
||||||
|
# Stage 2: Build SvelteKit frontend
|
||||||
|
FROM node:22-slim AS frontend-builder
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY --from=wasm-builder /app/crates/mass-driver-wasm/pkg ./node_modules/mass-driver-wasm
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 3: Serve with nginx
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=frontend-builder /app/frontend/build /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -4,13 +4,13 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
orbital-mechanics = { path = "../orbital-mechanics" }
|
|
||||||
mass-driver-core = { path = "../mass-driver-core" }
|
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono"] }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tower-http = { version = "0.6", features = ["cors", "compression-gzip"] }
|
tower-http = { version = "0.6", features = ["cors", "compression-gzip"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
base64 = "0.22"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|||||||
152
crates/mass-driver-backend/src/db.rs
Normal file
152
crates/mass-driver-backend/src/db.rs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
pub async fn init_db(pool: &PgPool) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS transfer_matrices (
|
||||||
|
config_hash VARCHAR(64) PRIMARY KEY,
|
||||||
|
station_count INTEGER NOT NULL,
|
||||||
|
launch_velocity_kms REAL NOT NULL,
|
||||||
|
matrix_bytes BYTEA NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
accessed_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS cached_routes (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
config_hash VARCHAR(64) REFERENCES transfer_matrices(config_hash) ON DELETE CASCADE,
|
||||||
|
from_station INTEGER NOT NULL,
|
||||||
|
to_station INTEGER NOT NULL,
|
||||||
|
route_json JSONB NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
UNIQUE(config_hash, from_station, to_station)
|
||||||
|
);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tracing::info!("Database tables initialized");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn store_matrix(
|
||||||
|
pool: &PgPool,
|
||||||
|
config_hash: &str,
|
||||||
|
station_count: i32,
|
||||||
|
launch_velocity_kms: f32,
|
||||||
|
matrix_bytes: &[u8],
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO transfer_matrices (config_hash, station_count, launch_velocity_kms, matrix_bytes)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT (config_hash) DO UPDATE
|
||||||
|
SET station_count = EXCLUDED.station_count,
|
||||||
|
launch_velocity_kms = EXCLUDED.launch_velocity_kms,
|
||||||
|
matrix_bytes = EXCLUDED.matrix_bytes,
|
||||||
|
accessed_at = NOW()
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(config_hash)
|
||||||
|
.bind(station_count)
|
||||||
|
.bind(launch_velocity_kms)
|
||||||
|
.bind(matrix_bytes)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_matrix(
|
||||||
|
pool: &PgPool,
|
||||||
|
config_hash: &str,
|
||||||
|
) -> Result<Option<MatrixRow>, sqlx::Error> {
|
||||||
|
// Update accessed_at and return the row
|
||||||
|
let row = sqlx::query_as::<_, MatrixRow>(
|
||||||
|
r#"
|
||||||
|
UPDATE transfer_matrices
|
||||||
|
SET accessed_at = NOW()
|
||||||
|
WHERE config_hash = $1
|
||||||
|
RETURNING config_hash, station_count, launch_velocity_kms, matrix_bytes, created_at, accessed_at
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(config_hash)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn store_route(
|
||||||
|
pool: &PgPool,
|
||||||
|
config_hash: &str,
|
||||||
|
from_station: i32,
|
||||||
|
to_station: i32,
|
||||||
|
route_json: &serde_json::Value,
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO cached_routes (config_hash, from_station, to_station, route_json)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT (config_hash, from_station, to_station) DO UPDATE
|
||||||
|
SET route_json = EXCLUDED.route_json
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(config_hash)
|
||||||
|
.bind(from_station)
|
||||||
|
.bind(to_station)
|
||||||
|
.bind(route_json)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_route(
|
||||||
|
pool: &PgPool,
|
||||||
|
config_hash: &str,
|
||||||
|
from_station: i32,
|
||||||
|
to_station: i32,
|
||||||
|
) -> Result<Option<RouteRow>, sqlx::Error> {
|
||||||
|
let row = sqlx::query_as::<_, RouteRow>(
|
||||||
|
r#"
|
||||||
|
SELECT id, config_hash, from_station, to_station, route_json, created_at
|
||||||
|
FROM cached_routes
|
||||||
|
WHERE config_hash = $1 AND from_station = $2 AND to_station = $3
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(config_hash)
|
||||||
|
.bind(from_station)
|
||||||
|
.bind(to_station)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow, serde::Serialize)]
|
||||||
|
pub struct MatrixRow {
|
||||||
|
pub config_hash: String,
|
||||||
|
pub station_count: i32,
|
||||||
|
pub launch_velocity_kms: f32,
|
||||||
|
pub matrix_bytes: Vec<u8>,
|
||||||
|
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub accessed_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow, serde::Serialize)]
|
||||||
|
pub struct RouteRow {
|
||||||
|
pub id: i32,
|
||||||
|
pub config_hash: Option<String>,
|
||||||
|
pub from_station: i32,
|
||||||
|
pub to_station: i32,
|
||||||
|
pub route_json: serde_json::Value,
|
||||||
|
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
}
|
||||||
@@ -1,3 +1,49 @@
|
|||||||
fn main() {
|
mod db;
|
||||||
println!("mass-driver backend — not yet implemented");
|
mod routes;
|
||||||
|
|
||||||
|
use axum::{routing::{get, post}, Router};
|
||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
use tower_http::{compression::CompressionLayer, cors::CorsLayer};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let database_url =
|
||||||
|
std::env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set");
|
||||||
|
let port = std::env::var("PORT").unwrap_or_else(|_| "3001".to_string());
|
||||||
|
|
||||||
|
tracing::info!("Connecting to database...");
|
||||||
|
let pool = PgPoolOptions::new()
|
||||||
|
.max_connections(10)
|
||||||
|
.connect(&database_url)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to Postgres");
|
||||||
|
|
||||||
|
db::init_db(&pool).await.expect("Failed to initialize database tables");
|
||||||
|
tracing::info!("Database initialized");
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/api/health", get(routes::health))
|
||||||
|
.route("/api/cache/transfer-matrix", post(routes::store_matrix))
|
||||||
|
.route(
|
||||||
|
"/api/cache/transfer-matrix/{config_hash}",
|
||||||
|
get(routes::get_matrix),
|
||||||
|
)
|
||||||
|
.route("/api/cache/route", post(routes::store_route))
|
||||||
|
.route("/api/cache/route", get(routes::get_route))
|
||||||
|
.layer(CompressionLayer::new())
|
||||||
|
.layer(CorsLayer::permissive())
|
||||||
|
.with_state(pool);
|
||||||
|
|
||||||
|
let addr = format!("0.0.0.0:{port}");
|
||||||
|
tracing::info!("Listening on {addr}");
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(&addr)
|
||||||
|
.await
|
||||||
|
.expect("Failed to bind listener");
|
||||||
|
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.await
|
||||||
|
.expect("Server error");
|
||||||
}
|
}
|
||||||
|
|||||||
134
crates/mass-driver-backend/src/routes.rs
Normal file
134
crates/mass-driver-backend/src/routes.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::IntoResponse,
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::db;
|
||||||
|
|
||||||
|
// ---------- Health ----------
|
||||||
|
|
||||||
|
pub async fn health() -> impl IntoResponse {
|
||||||
|
Json(serde_json::json!({ "status": "ok" }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Transfer Matrix ----------
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct StoreMatrixRequest {
|
||||||
|
pub config_hash: String,
|
||||||
|
pub station_count: i32,
|
||||||
|
pub launch_velocity_kms: f32,
|
||||||
|
pub matrix_bytes: String, // base64-encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct MatrixResponse {
|
||||||
|
pub config_hash: String,
|
||||||
|
pub station_count: i32,
|
||||||
|
pub launch_velocity_kms: f32,
|
||||||
|
pub matrix_bytes: String, // base64-encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn store_matrix(
|
||||||
|
State(pool): State<PgPool>,
|
||||||
|
Json(req): Json<StoreMatrixRequest>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
|
let bytes = BASE64
|
||||||
|
.decode(&req.matrix_bytes)
|
||||||
|
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid base64: {e}")))?;
|
||||||
|
|
||||||
|
db::store_matrix(
|
||||||
|
&pool,
|
||||||
|
&req.config_hash,
|
||||||
|
req.station_count,
|
||||||
|
req.launch_velocity_kms,
|
||||||
|
&bytes,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("DB error storing matrix: {e}");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((StatusCode::CREATED, Json(serde_json::json!({ "stored": true }))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_matrix(
|
||||||
|
State(pool): State<PgPool>,
|
||||||
|
Path(config_hash): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
|
let row = db::get_matrix(&pool, &config_hash).await.map_err(|e| {
|
||||||
|
tracing::error!("DB error fetching matrix: {e}");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
Some(m) => Ok(Json(MatrixResponse {
|
||||||
|
config_hash: m.config_hash,
|
||||||
|
station_count: m.station_count,
|
||||||
|
launch_velocity_kms: m.launch_velocity_kms,
|
||||||
|
matrix_bytes: BASE64.encode(&m.matrix_bytes),
|
||||||
|
})
|
||||||
|
.into_response()),
|
||||||
|
None => Ok(StatusCode::NOT_FOUND.into_response()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Cached Route ----------
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct StoreRouteRequest {
|
||||||
|
pub config_hash: String,
|
||||||
|
pub from_station: i32,
|
||||||
|
pub to_station: i32,
|
||||||
|
pub route_json: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RouteQuery {
|
||||||
|
pub config_hash: String,
|
||||||
|
pub from: i32,
|
||||||
|
pub to: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn store_route(
|
||||||
|
State(pool): State<PgPool>,
|
||||||
|
Json(req): Json<StoreRouteRequest>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
|
db::store_route(
|
||||||
|
&pool,
|
||||||
|
&req.config_hash,
|
||||||
|
req.from_station,
|
||||||
|
req.to_station,
|
||||||
|
&req.route_json,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("DB error storing route: {e}");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((StatusCode::CREATED, Json(serde_json::json!({ "stored": true }))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_route(
|
||||||
|
State(pool): State<PgPool>,
|
||||||
|
Query(q): Query<RouteQuery>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
|
let row = db::get_route(&pool, &q.config_hash, q.from, q.to)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("DB error fetching route: {e}");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
Some(r) => Ok(Json(r).into_response()),
|
||||||
|
None => Ok(StatusCode::NOT_FOUND.into_response()),
|
||||||
|
}
|
||||||
|
}
|
||||||
28
docker-compose.yml
Normal file
28
docker-compose.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: massdriver
|
||||||
|
POSTGRES_PASSWORD: massdriver
|
||||||
|
POSTGRES_DB: massdriver
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.backend
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgres://massdriver:massdriver@postgres:5432/massdriver
|
||||||
|
PORT: "3001"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
62
k8s/backend.yaml
Normal file
62
k8s/backend.yaml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: backend
|
||||||
|
namespace: mass-driver
|
||||||
|
labels:
|
||||||
|
app: backend
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: backend
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: backend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: backend
|
||||||
|
image: mass-driver-backend:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 3001
|
||||||
|
env:
|
||||||
|
- name: DATABASE_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: backend-secrets
|
||||||
|
key: database-url
|
||||||
|
- name: PORT
|
||||||
|
value: "3001"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 256Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: 3001
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: 3001
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 30
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: backend
|
||||||
|
namespace: mass-driver
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: backend
|
||||||
|
ports:
|
||||||
|
- port: 3001
|
||||||
|
targetPort: 3001
|
||||||
|
protocol: TCP
|
||||||
42
k8s/frontend.yaml
Normal file
42
k8s/frontend.yaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: frontend
|
||||||
|
namespace: mass-driver
|
||||||
|
labels:
|
||||||
|
app: frontend
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: frontend
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: frontend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: frontend
|
||||||
|
image: mass-driver-frontend:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: frontend
|
||||||
|
namespace: mass-driver
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: frontend
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
protocol: TCP
|
||||||
26
k8s/ingress.yaml
Normal file
26
k8s/ingress.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: mass-driver-ingress
|
||||||
|
namespace: mass-driver
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /api
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: backend
|
||||||
|
port:
|
||||||
|
number: 3001
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: frontend
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
4
k8s/namespace.yaml
Normal file
4
k8s/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: mass-driver
|
||||||
46
nginx.conf
Normal file
46
nginx.conf
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
types {
|
||||||
|
application/wasm wasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/wasm;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|wasm)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy /api/ requests to the backend
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:3001;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# SPA fallback — serve index.html for all other routes
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user