diff --git a/backend/Dockerfile b/backend/Dockerfile index 3f39c30..cf59206 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,12 +1,29 @@ # =================================================================== -# Stage 1: Build the Rust backend +# Stage 1: Build the frontend +# =================================================================== +FROM node:22-alpine AS frontend-builder + +WORKDIR /app + +# Copy package files first for dependency caching +COPY frontend/package.json frontend/package-lock.json ./ + +RUN npm ci + +# Copy source and build +COPY frontend/ ./ + +RUN npm run build + +# =================================================================== +# Stage 2: Build the Rust backend # =================================================================== FROM rust:1.85-bookworm AS builder WORKDIR /app # Copy manifests first for dependency caching -COPY Cargo.toml Cargo.lock ./ +COPY backend/Cargo.toml backend/Cargo.lock ./ # Create a dummy main.rs to build dependencies RUN mkdir src && echo "fn main() {}" > src/main.rs @@ -14,8 +31,8 @@ RUN cargo build --release RUN rm -rf src # Copy the actual source code and migrations -COPY src/ src/ -COPY migrations/ migrations/ +COPY backend/src/ src/ +COPY backend/migrations/ migrations/ # Set sqlx offline mode (no live DB needed during build) ENV SQLX_OFFLINE=true @@ -26,7 +43,7 @@ RUN touch src/main.rs RUN cargo build --release # =================================================================== -# Stage 2: Minimal runtime image +# Stage 3: Minimal runtime image # =================================================================== FROM debian:bookworm-slim AS runtime @@ -50,6 +67,9 @@ COPY --from=builder /app/target/release/ai-synth-backend ./ai-synth-backend # Copy migrations (run at startup) COPY --from=builder /app/migrations/ ./migrations/ +# Copy built frontend +COPY --from=frontend-builder /app/dist/ ./static/ + # Set ownership RUN chown -R appuser:appuser /app diff --git a/backend/src/main.rs b/backend/src/main.rs index 77b26f9..7360825 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -59,6 +59,7 @@ async fn main() -> anyhow::Result<()> { tracing::warn!("Failed to load provider rate limits from DB: {:?}. Using defaults.", e); } + let shutdown_pool = state.pool.clone(); let app = router::build_router(state, &config); let addr = format!("0.0.0.0:{}", config.port); @@ -69,8 +70,13 @@ async fn main() -> anyhow::Result<()> { .context("Failed to bind to address")?; axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) .await .context("Server error")?; + + tracing::info!("Server shut down, closing database pool..."); + shutdown_pool.close().await; + tracing::info!("Shutdown complete."); } Commands::CreateAdmin { email } => { tracing::info!("Creating admin user: {}", email); @@ -103,3 +109,28 @@ async fn run_migrations(pool: &sqlx::PgPool) -> anyhow::Result<()> { tracing::info!("Migrations complete."); Ok(()) } + +/// Wait for SIGTERM or Ctrl+C, then return to trigger graceful shutdown. +async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install SIGTERM handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => tracing::info!("Received Ctrl+C, starting graceful shutdown..."), + _ = terminate => tracing::info!("Received SIGTERM, starting graceful shutdown..."), + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 4f7383f..7aa0c53 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ services: app: build: - context: ./backend - dockerfile: Dockerfile + context: . + dockerfile: backend/Dockerfile container_name: ai-synth restart: unless-stopped env_file: .env