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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,3 +28,6 @@ Thumbs.db
|
|||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
|
# Server settings, which may hold an OIDC client secret. Only the example is tracked.
|
||||||
|
.env
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -159,18 +159,44 @@ signed out from another one.
|
|||||||
|
|
||||||
## Running the sync server
|
## Running the sync server
|
||||||
|
|
||||||
`mtgc-server` is a single binary with a SQLite database. Run it behind a
|
`mtgc-server` is a single binary with a SQLite database, shipped as a
|
||||||
reverse proxy that provides HTTPS (the Android app refuses plain HTTP outside
|
container image. Inside the container it listens on port 8080 and keeps its
|
||||||
debug builds). `crates/mtgc-server/deploy/` has a Docker Compose example with
|
database in `/data`; everything else is environment variables (below).
|
||||||
Caddy:
|
|
||||||
|
To run it locally, build the image, then run it with the port forwarded to
|
||||||
|
localhost and a named volume for the data:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd crates/mtgc-server/deploy
|
docker build -f crates/mtgc-server/Dockerfile -t mtgc-sync-server .
|
||||||
cp .env.example .env # set MTGC_PUBLIC_URL, sign-in options
|
|
||||||
docker compose up -d
|
docker run -d --name mtgc-sync-server \
|
||||||
docker compose exec mtgc-server mtgc-server user add yourname
|
-p 127.0.0.1:8080:8080 \
|
||||||
|
-v mtgc-sync-data:/data \
|
||||||
|
-e MTGC_PUBLIC_URL=http://localhost:8080 \
|
||||||
|
mtgc-sync-server
|
||||||
|
|
||||||
|
docker exec -it mtgc-sync-server mtgc-server user add yourname # prompts for a password
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then sign in from the desktop app with `http://localhost:8080`. For more
|
||||||
|
settings, copy `crates/mtgc-server/deploy/.env.example` to `.env` beside it
|
||||||
|
(git ignores it) and pass `--env-file crates/mtgc-server/deploy/.env` instead
|
||||||
|
of the `-e`. Podman takes the same commands.
|
||||||
|
|
||||||
|
The container runs as UID 10001. A named volume, as above, picks up the right
|
||||||
|
ownership by itself; if you bind-mount a host directory instead, `chown 10001`
|
||||||
|
it first.
|
||||||
|
|
||||||
|
In production, put it behind something that terminates HTTPS — the Android
|
||||||
|
app refuses plain HTTP outside debug builds — and set `MTGC_PUBLIC_URL` to
|
||||||
|
the `https://` address users reach it at. On Kubernetes that means a
|
||||||
|
Deployment with **one replica** (it's a single SQLite file, so replicas can't
|
||||||
|
share it) and a `Recreate` strategy, a PersistentVolumeClaim mounted at
|
||||||
|
`/data`, a Service on port 8080, and an Ingress for TLS. `GET /healthz`
|
||||||
|
answers `ok` for probes. Keep `MTGC_OIDC_CLIENT_SECRET` in a Secret. Run the
|
||||||
|
account commands with `kubectl exec -it deploy/<name> -- mtgc-server user add
|
||||||
|
<name>`.
|
||||||
|
|
||||||
To build the image yourself and push it to your own registry:
|
To build the image yourself and push it to your own registry:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
# Where users reach the server, exactly as the reverse proxy serves it.
|
# Settings for the mtgc-server container, for `docker run --env-file`. Copy to
|
||||||
|
# .env (which git ignores) and fill in. See the README's "Running the sync
|
||||||
|
# server" for the full list.
|
||||||
|
|
||||||
|
# Where users reach the server, exactly as they reach it: through your ingress
|
||||||
|
# in production, or the forwarded port locally (http://localhost:8080).
|
||||||
MTGC_PUBLIC_URL=https://mtgc.example.com
|
MTGC_PUBLIC_URL=https://mtgc.example.com
|
||||||
|
|
||||||
# Username/password accounts, managed with `mtgc-server user ...`.
|
# Username/password accounts, managed with `mtgc-server user ...`.
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# Replace with the hostname in MTGC_PUBLIC_URL. Caddy obtains the certificate.
|
|
||||||
mtgc.example.com {
|
|
||||||
encode gzip
|
|
||||||
reverse_proxy mtgc-server:8080
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Example deployment: mtgc-server behind Caddy, which provides HTTPS.
|
|
||||||
#
|
|
||||||
# cp .env.example .env # then fill it in
|
|
||||||
# docker compose up -d
|
|
||||||
# docker compose exec mtgc-server mtgc-server user add yourname
|
|
||||||
|
|
||||||
services:
|
|
||||||
mtgc-server:
|
|
||||||
build:
|
|
||||||
context: ../../..
|
|
||||||
dockerfile: crates/mtgc-server/Dockerfile
|
|
||||||
restart: unless-stopped
|
|
||||||
env_file: .env
|
|
||||||
volumes:
|
|
||||||
- mtgc-data:/data
|
|
||||||
|
|
||||||
caddy:
|
|
||||||
image: caddy:2
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
volumes:
|
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
||||||
- caddy-data:/data
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mtgc-data:
|
|
||||||
caddy-data:
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! The `user` commands read the same `MTGC_DB` as the server, so in a container
|
//! 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};
|
use std::io::{BufRead, IsTerminal};
|
||||||
|
|
||||||
@@ -63,15 +63,29 @@ fn serve() -> anyhow::Result<()> {
|
|||||||
.await
|
.await
|
||||||
.with_context(|| format!("binding {bind}"))?;
|
.with_context(|| format!("binding {bind}"))?;
|
||||||
tracing::info!("listening on {bind}");
|
tracing::info!("listening on {bind}");
|
||||||
axum::serve(listener, router(state))
|
axum::serve(listener, router(state)).with_graceful_shutdown(shutdown_signal()).await?;
|
||||||
.with_graceful_shutdown(async {
|
|
||||||
let _ = tokio::signal::ctrl_c().await;
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
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
|
/// Account management needs only the database, not the rest of the server's
|
||||||
/// configuration, so it works before OIDC (say) is set up.
|
/// configuration, so it works before OIDC (say) is set up.
|
||||||
fn open_db() -> anyhow::Result<rusqlite::Connection> {
|
fn open_db() -> anyhow::Result<rusqlite::Connection> {
|
||||||
|
|||||||
Reference in New Issue
Block a user