Run the sync server as a plain container; stop cleanly on SIGTERM

Drop the Compose/Caddy example: production runs on Kubernetes, and locally
the container only needs a forwarded port and a volume. The README now gives
the `docker run` steps and what a Kubernetes deployment needs (one replica,
since it's a single SQLite file; a volume at /data; TLS at the ingress).

The server only shut down on Ctrl-C, so `docker stop` and pod termination,
which send SIGTERM, waited out the grace period and killed it. It now shuts
down gracefully on either.

Also ignore `.env`, which can hold the OIDC client secret.
This commit is contained in:
2026-10-08 23:09:34 -05:00
parent de55fc634a
commit 5b4fa81bc1
6 changed files with 63 additions and 49 deletions

View File

@@ -9,7 +9,7 @@
//! ```
//!
//! The `user` commands read the same `MTGC_DB` as the server, so in a container
//! they run with `docker compose exec mtgc-server mtgc-server user add me`.
//! they run with `docker exec -it <container> mtgc-server user add me`.
use std::io::{BufRead, IsTerminal};
@@ -63,15 +63,29 @@ fn serve() -> anyhow::Result<()> {
.await
.with_context(|| format!("binding {bind}"))?;
tracing::info!("listening on {bind}");
axum::serve(listener, router(state))
.with_graceful_shutdown(async {
let _ = tokio::signal::ctrl_c().await;
})
.await?;
axum::serve(listener, router(state)).with_graceful_shutdown(shutdown_signal()).await?;
Ok(())
})
}
/// Resolves on Ctrl-C, or on SIGTERM — what `docker stop` and Kubernetes send.
/// As PID 1 in a container the process gets no default signal handling, so
/// without catching SIGTERM it would sit out the grace period and be killed.
async fn shutdown_signal() {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
let mut term = signal(SignalKind::terminate()).expect("install SIGTERM handler");
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = term.recv() => {}
}
}
#[cfg(not(unix))]
let _ = tokio::signal::ctrl_c().await;
tracing::info!("shutting down");
}
/// Account management needs only the database, not the rest of the server's
/// configuration, so it works before OIDC (say) is set up.
fn open_db() -> anyhow::Result<rusqlite::Connection> {