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
This commit is contained in:
Claude 2026-03-26 10:56:04 +00:00
parent 97929c11de
commit 9ba81bd809
No known key found for this signature in database
3 changed files with 38 additions and 1 deletions

View file

@ -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

View file

@ -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))

View file

@ -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) {