diff --git a/README.md b/README.md index 8046b62..4082381 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,23 @@ answers `ok` for probes. Keep `MTGC_OIDC_CLIENT_SECRET` in a Secret. Run the account commands with `kubectl exec -it deploy/ -- mtgc-server user add `. +A freshly provisioned volume is usually owned by root, which the server +(UID/GID 10001) can't write to — it fails at startup with "`/data` isn't +writable". Have Kubernetes hand the volume to its group in the pod spec: + +```yaml +spec: + securityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + fsGroup: 10001 +``` + +A few volume types ignore `fsGroup` (`hostPath`, and some NFS and local-path +provisioners). For those, `chown 10001:10001` the backing directory once, or +add an init container that does it. + To build the image yourself and push it to your own registry: ```bash diff --git a/crates/mtgc-server/Dockerfile b/crates/mtgc-server/Dockerfile index d1f0c93..d313add 100644 --- a/crates/mtgc-server/Dockerfile +++ b/crates/mtgc-server/Dockerfile @@ -13,10 +13,13 @@ FROM debian:bookworm-slim RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* \ - && useradd --system --uid 10001 --home /data mtgc \ - && mkdir /data && chown mtgc /data + && groupadd --system --gid 10001 mtgc \ + && useradd --system --uid 10001 --gid 10001 --home /data mtgc \ + && mkdir /data && chown 10001:10001 /data COPY --from=build /mtgc-server /usr/local/bin/mtgc-server -USER mtgc +# Numeric, so Kubernetes can verify `runAsNonRoot` (it can't resolve names), +# and with a fixed group so `fsGroup: 10001` grants a mounted volume to it. +USER 10001:10001 VOLUME /data ENV MTGC_DB=/data/mtgc-server.db \ MTGC_BIND=0.0.0.0:8080 diff --git a/crates/mtgc-server/src/main.rs b/crates/mtgc-server/src/main.rs index 9308f64..1092a1e 100644 --- a/crates/mtgc-server/src/main.rs +++ b/crates/mtgc-server/src/main.rs @@ -46,8 +46,7 @@ fn serve() -> anyhow::Result<()> { .init(); let config = Config::from_env()?; - let conn = db::open(&config.db_path) - .with_context(|| format!("opening {}", config.db_path.display()))?; + let conn = open_at(&config.db_path)?; let bind = config.bind; tracing::info!( public_url = %config.public_url, @@ -90,7 +89,28 @@ async fn shutdown_signal() { /// configuration, so it works before OIDC (say) is set up. fn open_db() -> anyhow::Result { let path = std::env::var("MTGC_DB").unwrap_or_else(|_| "mtgc-server.db".into()); - db::open(std::path::Path::new(&path)).with_context(|| format!("opening {path}")) + open_at(std::path::Path::new(&path)) +} + +/// Open the database, explaining the usual reason it can't be: a mounted +/// volume the server's user can't write to. SQLite's own message ("unable to +/// open database file") doesn't say so. +fn open_at(path: &std::path::Path) -> anyhow::Result { + db::open(path).map_err(|e| { + let dir = path.parent().filter(|d| !d.as_os_str().is_empty()).unwrap_or(std::path::Path::new(".")); + let probe = dir.join(".mtgc-write-test"); + let writable = std::fs::write(&probe, b"").and_then(|_| std::fs::remove_file(&probe)); + let err = anyhow::Error::new(e).context(format!("opening {}", path.display())); + match writable { + Ok(()) => err, + Err(w) => err.context(format!( + "{} isn't writable by this process ({w}). In the container image the server \ + runs as UID/GID 10001: on Kubernetes, set the pod's securityContext.fsGroup \ + to 10001; for a bind mount, `chown 10001:10001` the host directory", + dir.display() + )), + } + }) } fn user(args: &[String]) -> anyhow::Result<()> {