simple http server
This commit is contained in:
parent
921a22e645
commit
8f0bbe4e0b
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
target/
|
||||
.env
|
||||
|
||||
1722
Cargo.lock
generated
1722
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -5,8 +5,12 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8.9"
|
||||
dotenvy = "0.15.7"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "migrate"] }
|
||||
thiserror = "2.0.18"
|
||||
tokio = { version = "1.52.0", features = ["macros", "rt-multi-thread"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.23"
|
||||
tower-http = { version = "0.6.6", features = ["trace"] }
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
|
||||
45
Dockerfile
Normal file
45
Dockerfile
Normal file
@ -0,0 +1,45 @@
|
||||
# ---- STAGE 1: Build ----
|
||||
# Use an OpenJDK image that matches the version you develop with.
|
||||
# It contains the JDK, but not Maven.
|
||||
FROM eclipse-temurin:25-jdk-jammy AS builder
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# ---- Caching Dependencies ----
|
||||
# First, copy the files that define the build, including the Maven Wrapper
|
||||
COPY mvnw .
|
||||
COPY .mvn .mvn
|
||||
COPY pom.xml .
|
||||
|
||||
# Make the wrapper executable
|
||||
RUN chmod +x ./mvnw
|
||||
|
||||
# Run a Maven command to download dependencies.
|
||||
# Since pom.xml and wrapper files rarely change, this layer will be cached by Docker,
|
||||
# speeding up subsequent builds significantly.
|
||||
RUN ./mvnw dependency:go-offline
|
||||
|
||||
# ---- Building the Application ----
|
||||
# Now, copy the source code. If only source code changes, the layers above are cached.
|
||||
COPY src ./src
|
||||
|
||||
# Build the application JAR using the Maven Wrapper
|
||||
RUN ./mvnw clean package -DskipTests
|
||||
|
||||
|
||||
# ---- STAGE 2: Runtime ----
|
||||
# Use a lean Eclipse Temurin JRE image for a small and secure final container.
|
||||
FROM eclipse-temurin:25-jre-jammy
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy only the built JAR file from the 'builder' stage
|
||||
COPY --from=builder /app/target/*.jar app.jar
|
||||
|
||||
# Expose the application port
|
||||
EXPOSE 8080
|
||||
|
||||
# Command to run the application
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
58
compose.yaml
Normal file
58
compose.yaml
Normal file
@ -0,0 +1,58 @@
|
||||
services:
|
||||
# api-prod:
|
||||
# build: .
|
||||
# restart: unless-stopped
|
||||
# container_name: qrcode-api
|
||||
# ports:
|
||||
# - "8080:8080"
|
||||
# environment:
|
||||
# DB_HOST: db-prod
|
||||
# env_file:
|
||||
# - ".env"
|
||||
# depends_on:
|
||||
# db-prod:
|
||||
# condition: service_healthy
|
||||
# profiles:
|
||||
# - prod
|
||||
#
|
||||
# db-prod:
|
||||
# image: postgres:18.0-alpine
|
||||
# restart: unless-stopped
|
||||
# container_name: qrcode-database-prod
|
||||
# ports:
|
||||
# - "${DB_PORT:-5432}:5432"
|
||||
# environment:
|
||||
# POSTGRES_USER: ${DB_USERNAME}
|
||||
# POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
# POSTGRES_DB: ${DB_NAME}
|
||||
# volumes:
|
||||
# - postgres_data:/var/lib/postgresql/data
|
||||
# profiles:
|
||||
# - prod
|
||||
# healthcheck:
|
||||
# test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"]
|
||||
# interval: 5s
|
||||
# timeout: 3s
|
||||
# retries: 10
|
||||
|
||||
db-dev:
|
||||
image: postgres:18.0-alpine
|
||||
restart: unless-stopped
|
||||
container_name: rhythm-db-dev
|
||||
ports:
|
||||
- "${DB_PORT}:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
profiles:
|
||||
- dev
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
52
src/config.rs
Normal file
52
src/config.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use std::env;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub db_user: String,
|
||||
pub db_password: String,
|
||||
pub db_host: String,
|
||||
pub db_port: u16,
|
||||
pub db_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("missing env var: {0}")]
|
||||
MissingVar(&'static str),
|
||||
#[error("invalid DB_PORT: {0}")]
|
||||
InvalidPort(String),
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Result<Self, ConfigError> {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
let db_user =
|
||||
env::var("DB_USERNAME").map_err(|_| ConfigError::MissingVar("DB_USERNAME"))?;
|
||||
let db_password =
|
||||
env::var("DB_PASSWORD").map_err(|_| ConfigError::MissingVar("DB_PASSWORD"))?;
|
||||
let db_host = env::var("DB_HOST").map_err(|_| ConfigError::MissingVar("DB_HOST"))?;
|
||||
let db_name = env::var("DB_NAME").map_err(|_| ConfigError::MissingVar("DB_NAME"))?;
|
||||
let db_port_raw = env::var("DB_PORT").map_err(|_| ConfigError::MissingVar("DB_PORT"))?;
|
||||
let db_port = db_port_raw
|
||||
.parse::<u16>()
|
||||
.map_err(|_| ConfigError::InvalidPort(db_port_raw))?;
|
||||
|
||||
Ok(Self {
|
||||
db_user,
|
||||
db_password,
|
||||
db_host,
|
||||
db_port,
|
||||
db_name,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn database_url(&self) -> String {
|
||||
format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
self.db_user, self.db_password, self.db_host, self.db_port, self.db_name
|
||||
)
|
||||
}
|
||||
}
|
||||
9
src/db/database.rs
Normal file
9
src/db/database.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
|
||||
pub async fn create_pool(database_url: &str) -> Result<PgPool, crate::error::AppError> {
|
||||
PgPoolOptions::new()
|
||||
.max_connections(10)
|
||||
.connect(database_url)
|
||||
.await
|
||||
.map_err(crate::error::AppError::from)
|
||||
}
|
||||
1
src/db/mod.rs
Normal file
1
src/db/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod database;
|
||||
11
src/error.rs
Normal file
11
src/error.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AppError {
|
||||
#[error(transparent)]
|
||||
Config(#[from] crate::config::ConfigError),
|
||||
#[error(transparent)]
|
||||
Db(#[from] sqlx::Error),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
@ -1 +1 @@
|
||||
pub mod public;
|
||||
pub mod public_router;
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new().route("/health", get(get_health()))
|
||||
}
|
||||
|
||||
fn get_health() -> &'static str {
|
||||
"{'status': 'ok'}"
|
||||
}
|
||||
18
src/http/public_router.rs
Normal file
18
src/http/public_router.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use axum::{Json, Router, routing::get};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct HealthResponse {
|
||||
status: &'static str,
|
||||
}
|
||||
|
||||
pub fn router<S>() -> Router<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
Router::new().route("/health", get(get_health))
|
||||
}
|
||||
|
||||
async fn get_health() -> Json<HealthResponse> {
|
||||
Json(HealthResponse { status: "ok" })
|
||||
}
|
||||
46
src/main.rs
46
src/main.rs
@ -1,11 +1,49 @@
|
||||
mod config;
|
||||
mod db;
|
||||
mod error;
|
||||
mod http;
|
||||
|
||||
use crate::db::database;
|
||||
use crate::error::AppError;
|
||||
use crate::http::public_router;
|
||||
use axum::Router;
|
||||
use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer};
|
||||
use tracing::{Level, info};
|
||||
use tracing_subscriber::{EnvFilter, fmt};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
#[allow(dead_code)]
|
||||
db: sqlx::PgPool,
|
||||
}
|
||||
|
||||
fn init_tracing() {
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info,tower_http=info,sqlx=warn"));
|
||||
|
||||
fmt().with_env_filter(filter).init();
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = Router::new().merge(http::public::router());
|
||||
async fn main() -> Result<(), AppError> {
|
||||
init_tracing();
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
let cfg = config::Config::from_env()?;
|
||||
let pool = database::create_pool(&cfg.database_url()).await?;
|
||||
info!("database connection established");
|
||||
|
||||
let state = AppState { db: pool };
|
||||
let app = Router::new()
|
||||
.merge(public_router::router())
|
||||
.with_state(state)
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.on_request(DefaultOnRequest::new().level(Level::INFO))
|
||||
.on_response(DefaultOnResponse::new().level(Level::INFO)),
|
||||
);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
|
||||
info!("server listening on 0.0.0.0:8080");
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user