feat: k8s health lifecycle + standalone Dockerfile for internal deployment
Add /health/startup and /health/live probes, flip readiness to 503 on SIGTERM so k8s stops routing before drain. Update standalone Dockerfile for internal deployment: port 9500, schema mounted at runtime (not baked in), persistent data dir, non-root user with fixed UID 10001. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5ceef74f3b
commit
3101789d32
@ -1,5 +1,6 @@
|
||||
FROM rust:1.91 AS builder
|
||||
WORKDIR /app
|
||||
FROM rust:1.91-slim AS builder
|
||||
WORKDIR /build
|
||||
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy workspace manifests first for layer caching.
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
@ -16,20 +17,15 @@ COPY . .
|
||||
RUN cargo build -p tidal-server --release
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
WORKDIR /srv
|
||||
RUN useradd --system --home /srv tidal && \
|
||||
apt-get update && apt-get install -y ca-certificates curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /app/target/release/tidal-server /usr/local/bin/tidal-server
|
||||
COPY tidal-server/config /etc/tidal-server
|
||||
|
||||
USER tidal
|
||||
EXPOSE 9400 9091
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD curl -f -H "Authorization: Bearer ${TIDAL_API_KEY:-}" http://localhost:9400/health || exit 1
|
||||
|
||||
ENTRYPOINT ["tidal-server", "standalone", \
|
||||
"--listen", "0.0.0.0:9400", \
|
||||
"--metrics", "0.0.0.0:9091"]
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*
|
||||
RUN useradd -m -u 10001 tidal
|
||||
COPY --from=builder --chown=tidal:tidal /build/target/release/tidal-server /usr/local/bin/tidal-server
|
||||
RUN mkdir -p /config && chown tidal:tidal /config
|
||||
USER tidal:tidal
|
||||
WORKDIR /data
|
||||
EXPOSE 9500
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD curl -sf http://localhost:9500/health || exit 1
|
||||
ENV TIDAL_SERVER_LOG=info
|
||||
ENTRYPOINT ["/usr/local/bin/tidal-server"]
|
||||
CMD ["standalone", "--listen", "0.0.0.0:9500", "--schema", "/config/eros-schema.yaml", "--data-dir", "/data"]
|
||||
|
||||
@ -111,13 +111,16 @@ async fn serve(state: ServerState, addr: &str, api_key: Option<Arc<str>>) -> Res
|
||||
let actual = listener.local_addr()?;
|
||||
tracing::info!("listening on http://{actual}");
|
||||
|
||||
axum::serve(listener, build_router(Arc::new(state), api_key))
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
let state = Arc::new(state);
|
||||
let shutdown_state = state.clone();
|
||||
|
||||
axum::serve(listener, build_router(state, api_key))
|
||||
.with_graceful_shutdown(shutdown_signal(shutdown_state))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
async fn shutdown_signal(state: Arc<ServerState>) {
|
||||
// SIGTERM is Unix-only; on other platforms we fall back to ctrl-c alone.
|
||||
#[cfg(unix)]
|
||||
let sigterm = async {
|
||||
@ -146,5 +149,7 @@ async fn shutdown_signal() {
|
||||
_ = sigterm => {}
|
||||
}
|
||||
|
||||
tracing::info!("shutdown signal received");
|
||||
// Flip readiness BEFORE axum starts draining
|
||||
state.set_shutting_down();
|
||||
tracing::info!("readiness flipped to not-ready, draining in-flight requests");
|
||||
}
|
||||
|
||||
@ -75,6 +75,8 @@ pub fn build_router(state: Arc<ServerState>, api_key: Option<Arc<str>>) -> Route
|
||||
// Public routes — exempt from auth so health probes always work.
|
||||
let public = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/health/startup", get(health_startup))
|
||||
.route("/health/live", get(health_live))
|
||||
.with_state(Arc::clone(&state));
|
||||
|
||||
// Protected routes — gated by Bearer token when a key is configured.
|
||||
@ -387,24 +389,43 @@ async fn search(
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct HealthResponse {
|
||||
status: &'static str,
|
||||
mode: &'static str,
|
||||
items: u64,
|
||||
/// Startup probe: always 200 (no migrations to check).
|
||||
async fn health_startup() -> Json<serde_json::Value> {
|
||||
Json(serde_json::json!({ "ok": true, "service": "tidaldb" }))
|
||||
}
|
||||
|
||||
/// Liveness probe: always 200 (process is alive).
|
||||
async fn health_live() -> Json<serde_json::Value> {
|
||||
Json(serde_json::json!({ "ok": true, "service": "tidaldb" }))
|
||||
}
|
||||
|
||||
/// Readiness probe: 200 when ready, 503 when shutting down.
|
||||
async fn health(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Query(query): Query<HashMap<String, String>>,
|
||||
) -> Result<Json<HealthResponse>, AppError> {
|
||||
) -> std::result::Result<(StatusCode, Json<serde_json::Value>), AppError> {
|
||||
if state.is_shutting_down() {
|
||||
return Ok((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(serde_json::json!({
|
||||
"ok": false,
|
||||
"service": "tidaldb",
|
||||
"cause": "shutting down"
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
let region = query.get("region").map(|s| s.as_str());
|
||||
let items = state.item_count(region).map_err(AppError)?;
|
||||
Ok(Json(HealthResponse {
|
||||
status: "ok",
|
||||
mode: "standalone",
|
||||
items,
|
||||
}))
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"mode": "standalone",
|
||||
"items": items,
|
||||
})),
|
||||
))
|
||||
}
|
||||
|
||||
struct TidalErrorWrapper(tidaldb::TidalError);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use tidaldb::TidalDb;
|
||||
use tidaldb::query::retrieve::Retrieve;
|
||||
@ -25,11 +26,23 @@ fn ensure_standalone(region_name: Option<&str>) -> Result<()> {
|
||||
#[derive(Clone)]
|
||||
pub struct ServerState {
|
||||
db: Arc<TidalDb>,
|
||||
shutting_down: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl ServerState {
|
||||
pub fn new(db: TidalDb) -> Self {
|
||||
Self { db: Arc::new(db) }
|
||||
Self {
|
||||
db: Arc::new(db),
|
||||
shutting_down: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_shutting_down(&self) {
|
||||
self.shutting_down.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn is_shutting_down(&self) -> bool {
|
||||
self.shutting_down.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn write_item(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user