chore: add /status /ready best practices for Docker & K8S

This commit is contained in:
Edouard Vanbelle
2026-05-11 00:22:03 +02:00
parent 8b08ee165d
commit 03aac93db3
6 changed files with 58 additions and 5 deletions
+2 -2
View File
@@ -67,10 +67,10 @@ WORKDIR /app
# Expose application port # Expose application port
EXPOSE 8086 EXPOSE 8086
# Basic health check — verifies the HTTP server responds on the main port. # Liveness probe — verifies the HTTP server is up (no DB check, fast).
# Docker / Compose / Swarm will mark the container unhealthy after 3 failures. # Docker / Compose / Swarm will mark the container unhealthy after 3 failures.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:8086/api/version || exit 1 CMD wget -qO- http://localhost:8086/health || exit 1
# Entrypoint fixes volume permissions then drops to oxicloud user. # Entrypoint fixes volume permissions then drops to oxicloud user.
# The container starts as root so it can chown mounted volumes, # The container starts as root so it can chown mounted volumes,
+6
View File
@@ -35,6 +35,12 @@ services:
- .env - .env
volumes: volumes:
- storage_data:/app/storage - storage_data:/app/storage
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8086/ready || exit 1"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 3
networks: networks:
oxicloud: oxicloud:
+1
View File
@@ -4,6 +4,7 @@ pub mod handlers;
pub mod routes; pub mod routes;
pub use routes::create_api_routes; pub use routes::create_api_routes;
pub use routes::create_health_routes;
pub use routes::create_public_api_routes; pub use routes::create_public_api_routes;
use utoipa::OpenApi; use utoipa::OpenApi;
+40 -2
View File
@@ -2,8 +2,9 @@ use crate::application::services::batch_operations::BatchOperationService;
use crate::common::di::AppState; use crate::common::di::AppState;
use axum::{ use axum::{
Router, Router,
extract::DefaultBodyLimit, extract::{DefaultBodyLimit, State},
response::Json as AxumJson, http::StatusCode,
response::{IntoResponse, Json as AxumJson},
routing::{delete, get, post, put}, routing::{delete, get, post, put},
}; };
use serde_json::json; use serde_json::json;
@@ -11,6 +12,31 @@ use std::sync::Arc;
use tower_http::{compression::CompressionLayer, trace::TraceLayer}; use tower_http::{compression::CompressionLayer, trace::TraceLayer};
use utoipa::OpenApi; use utoipa::OpenApi;
/// Liveness probe — returns 200 if the process is running, no DB check.
async fn health() -> impl IntoResponse {
(StatusCode::OK, AxumJson(json!({"status": "ok"})))
}
/// Readiness probe — returns 200 if the DB pool can serve queries, 503 otherwise.
async fn ready(State(state): State<Arc<AppState>>) -> impl IntoResponse {
match &state.db_pool {
Some(pool) => match sqlx::query("SELECT 1").execute(pool.as_ref()).await {
Ok(_) => (
StatusCode::OK,
AxumJson(json!({"status": "ok", "db": "ok"})),
),
Err(_) => (
StatusCode::SERVICE_UNAVAILABLE,
AxumJson(json!({"status": "error", "db": "error"})),
),
},
None => (
StatusCode::SERVICE_UNAVAILABLE,
AxumJson(json!({"status": "error", "db": "not configured"})),
),
}
}
/// Returns the application version from Cargo.toml (compile-time constant) /// Returns the application version from Cargo.toml (compile-time constant)
async fn get_version() -> AxumJson<serde_json::Value> { async fn get_version() -> AxumJson<serde_json::Value> {
AxumJson(json!({ AxumJson(json!({
@@ -45,6 +71,18 @@ use crate::interfaces::api::handlers::search_handler::{
}; };
use crate::interfaces::api::handlers::trash_handler; use crate::interfaces::api::handlers::trash_handler;
/// Creates root-level health check routes — mounted directly at `/`, not under `/api/`.
/// (follow docker/kubernetes best practices)
///
/// - `GET /health` — liveness probe, no DB check, always 200 if process is up.
/// - `GET /ready` — readiness probe, pings DB pool, returns 503 if unreachable.
pub fn create_health_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
Router::new()
.route("/health", get(health))
.route("/ready", get(ready))
.with_state(app_state.clone())
}
/// Creates public API routes that should NOT require authentication. /// Creates public API routes that should NOT require authentication.
pub fn create_public_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> { pub fn create_public_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
let share_service = app_state.share_service.clone(); let share_service = app_state.share_service.clone();
+1
View File
@@ -5,4 +5,5 @@ pub mod nextcloud;
pub mod web; pub mod web;
pub use api::create_api_routes; pub use api::create_api_routes;
pub use api::create_health_routes;
pub use api::create_public_api_routes; pub use api::create_public_api_routes;
+8 -1
View File
@@ -50,7 +50,9 @@ use oxicloud::interfaces;
use common::di::AppServiceFactory; use common::di::AppServiceFactory;
use infrastructure::db::create_database_pools; use infrastructure::db::create_database_pools;
use interfaces::{create_api_routes, create_public_api_routes, web::create_web_routes}; use interfaces::{
create_api_routes, create_health_routes, create_public_api_routes, web::create_web_routes,
};
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -115,6 +117,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Build application router // Build application router
let api_routes = create_api_routes(&app_state); let api_routes = create_api_routes(&app_state);
let public_api_routes = create_public_api_routes(&app_state); let public_api_routes = create_public_api_routes(&app_state);
let health_routes = create_health_routes(&app_state);
let web_routes = create_web_routes(); let web_routes = create_web_routes();
let mut app; let mut app;
@@ -319,6 +322,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
)); ));
app = Router::new() app = Router::new()
// Health / readiness probes — no auth, mounted at root
.merge(health_routes)
// Rate-limited auth endpoints (login, register, refresh) // Rate-limited auth endpoints (login, register, refresh)
.nest("/api/auth", auth_login) .nest("/api/auth", auth_login)
.nest("/api/auth", auth_register) .nest("/api/auth", auth_register)
@@ -375,6 +380,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Auth disabled — no middleware applied // Auth disabled — no middleware applied
tracing::warn!("Authentication is DISABLED — all API routes are publicly accessible"); tracing::warn!("Authentication is DISABLED — all API routes are publicly accessible");
app = Router::new() app = Router::new()
// Health / readiness probes — no auth, mounted at root
.merge(health_routes)
.nest("/api", public_api_routes) .nest("/api", public_api_routes)
.nest("/api", api_routes) .nest("/api", api_routes)
// RFC 6764 well-known discovery (just redirects) // RFC 6764 well-known discovery (just redirects)