From 9ba81bd809d2567418e07c26e5f28a384f3f274c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 10:56:04 +0000 Subject: [PATCH] fix: drop Caddy --resume, restore app routes from DB on startup --resume caused Caddyfile changes (e.g. new Forgejo block) to be silently ignored on restart because Caddy preferred its saved in-memory config. Instead, Caddy now always starts clean from the Caddyfile, and the HIY server re-registers every app's Caddy route from the DB on startup (restore_caddy_routes). This gives us the best of both worlds: - Caddyfile changes (static services, TLS config) are always picked up - App routes are restored automatically without needing a redeploy https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH --- infra/docker-compose.yml | 2 +- server/src/main.rs | 8 ++++++++ server/src/routes/apps.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 8d69b1b..53c1a97 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -113,7 +113,7 @@ services: - ../proxy/Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data - caddy-config:/config - command: caddy run --config /etc/caddy/Caddyfile --adapter caddyfile --resume + command: caddy run --config /etc/caddy/Caddyfile --adapter caddyfile networks: - hiy-net - default diff --git a/server/src/main.rs b/server/src/main.rs index 444a151..152bfe8 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -154,6 +154,14 @@ async fn main() -> anyhow::Result<()> { builder::build_worker(worker_state).await; }); + // Re-register all app Caddy routes from the DB on startup. + // Caddy no longer uses --resume, so routes must be restored each time the + // stack restarts (ensures Caddyfile changes are always picked up). + let restore_db = state.db.clone(); + tokio::spawn(async move { + routes::apps::restore_caddy_routes(&restore_db).await; + }); + // ── Protected routes (admin login required) ─────────────────────────────── let protected = Router::new() .route("/", get(routes::ui::index)) diff --git a/server/src/routes/apps.rs b/server/src/routes/apps.rs index cce0e68..58f8345 100644 --- a/server/src/routes/apps.rs +++ b/server/src/routes/apps.rs @@ -47,6 +47,35 @@ fn caddy_route(app_host: &str, upstream: &str, is_public: bool) -> serde_json::V } } +/// Re-register every app's Caddy route from the database. +/// Called at startup so that removing `--resume` from Caddy doesn't lose +/// routes when the stack restarts. +pub async fn restore_caddy_routes(db: &crate::DbPool) { + // Give Caddy a moment to finish loading the Caddyfile before we PATCH it. + let caddy_api = std::env::var("CADDY_API_URL").unwrap_or_else(|_| "http://caddy:2019".into()); + let client = reqwest::Client::new(); + for attempt in 1..=10u32 { + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + if client.get(format!("{}/config/", caddy_api)).send().await.is_ok() { + break; + } + tracing::info!("restore_caddy_routes: waiting for Caddy ({}/10)…", attempt); + } + + let apps = match sqlx::query_as::<_, crate::models::App>("SELECT * FROM apps") + .fetch_all(db) + .await + { + Ok(a) => a, + Err(e) => { tracing::error!("restore_caddy_routes: DB error: {}", e); return; } + }; + + for app in &apps { + push_visibility_to_caddy(&app.id, app.port, app.is_public != 0).await; + } + tracing::info!("restore_caddy_routes: registered {} app routes", apps.len()); +} + /// Push a visibility change to Caddy without requiring a full redeploy. /// Best-effort: logs a warning on failure but does not surface an error to the caller. async fn push_visibility_to_caddy(app_id: &str, port: i64, is_public: bool) {