fix(docker): model downloads failed in containers (curl exit 23, glibc mismatch)

- pin builder and runtime to the same Debian release (trixie): the floating
  rust:1-slim builder drifted to a newer glibc than the bookworm runtime, so
  freshly built images crashed on start with 'GLIBC_2.39 not found'
- install curl in the runtime image — ready-.cmf imports download with it
- keep all gateway state under /app/data (CORTIQ_GATEWAY_HOME) and run as
  root so bind-mounted host folders on Docker Desktop are writable; pass
  --user 10001 to harden when not bind-mounting
- persist /app/data via a cortiq-data volume in docker-compose
- fail imports immediately with the path and reason when the models dir
  cannot be created, and annotate curl exit codes 23/22 with readable hints
- exclude local models/*.cmf from the docker build context
This commit is contained in:
infosave2007
2026-07-25 12:01:38 +03:00
parent 5d8455b4a0
commit 45def5c6b6
5 changed files with 40 additions and 9 deletions
+2
View File
@@ -1,4 +1,6 @@
target
models
*.cmf
.git
.github
docs
+13 -5
View File
@@ -1,5 +1,7 @@
# ---- build stage ------------------------------------------------------------
FROM rust:1-slim AS builder
# pin builder and runtime to the same Debian release — a floating `rust:1-slim`
# drifts to a newer glibc than the runtime image and the binary refuses to start
FROM rust:1-slim-trixie AS builder
WORKDIR /app
# the admin SPA (web/) is embedded at compile time via rust-embed, so it must be present
COPY Cargo.toml Cargo.lock ./
@@ -8,18 +10,24 @@ COPY web ./web
RUN cargo build --release
# ---- runtime stage ----------------------------------------------------------
FROM debian:bookworm-slim
FROM debian:trixie-slim
# curl is required at runtime — model imports download ready .cmf files with it
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* \
&& useradd -r -u 10001 cortiq \
&& mkdir -p /app/config
&& mkdir -p /app/config /app/data/models
COPY --from=builder /app/target/release/cortiq-gateway /usr/local/bin/cortiq-gateway
# ship the example as the default config so `docker run` works out of the box
COPY config/gateway.example.toml /app/config/gateway.toml
RUN chown -R 10001:10001 /app
WORKDIR /app
USER cortiq
# all gateway state (downloaded models, stats, secrets) lives under /app/data —
# mount a volume there to keep it across container recreates
ENV CORTIQ_GATEWAY_HOME=/app/data
# runs as root so writes to bind-mounted host folders (Docker Desktop on
# Windows/macOS) don't fail with permission errors (curl exit code 23);
# add `--user 10001` to docker run to harden when not using host bind mounts
EXPOSE 9000
ENTRYPOINT ["cortiq-gateway"]
CMD ["--config", "/app/config/gateway.toml"]
+6 -2
View File
@@ -54,7 +54,8 @@ One OpenAI-compatible endpoint → automatic model selection from your pool
```bash
# Docker (GitHub Container Registry)
docker run -p 9000:9000 -e CORTIQ_ADMIN_TOKEN=secret \
-v "$PWD/config:/app/config" ghcr.io/infosave2007/cortiq-gateway:latest
-v "$PWD/config:/app/config" -v cortiq-data:/app/data \
ghcr.io/infosave2007/cortiq-gateway:latest
# → admin console at http://localhost:9000/admin
# from crates.io
@@ -67,7 +68,10 @@ cargo build --release # ./target/release/cortiq-gateway
```
The image ships a default config and runs out of the box; mount `/app/config` (with a
`gateway.toml`) to use your own. Tags: `:latest`, `:X.Y.Z`, `:edge` (linux/amd64 + arm64).
`gateway.toml`) to use your own. Downloaded models and stats live in `/app/data` — mount
a volume there so they survive container recreates (a host folder works too, e.g.
`-v "$PWD/models:/app/data/models"`). Tags: `:latest`, `:X.Y.Z`, `:edge`
(linux/amd64 + arm64).
**Docker Compose** — gateway + a local Ollama model in one command:
+5
View File
@@ -17,6 +17,10 @@ services:
CORTIQ_ROUTER_KEY: ${CORTIQ_ROUTER_KEY:-}
volumes:
- ./config/gateway.compose.toml:/app/config/gateway.toml:ro
# downloaded .cmf models + stats persist here across recreates; swap the
# named volume for a host folder (e.g. ./models:/app/data/models) if you
# want the files visible next to the compose file
- cortiq-data:/app/data
depends_on:
- ollama
restart: unless-stopped
@@ -30,4 +34,5 @@ services:
restart: unless-stopped
volumes:
cortiq-data:
ollama:
+14 -2
View File
@@ -406,7 +406,13 @@ async fn download_cmf_repo(
}
Ok(status) => {
cleanup_output(&output_abs);
store.push_line(&id, format!("✗ download failed: exit code {:?}", status.code()));
// curl 23 = write error: almost always a non-writable models dir
let hint = match status.code() {
Some(23) => " (write error — is the models dir writable?)",
Some(22) => " (HTTP error — repo/file not found or auth required)",
_ => "",
};
store.push_line(&id, format!("✗ download failed: exit code {:?}{hint}", status.code()));
store.finish(&id, false);
}
Err(e) => {
@@ -471,7 +477,13 @@ pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Resu
}
}
}
let _ = std::fs::create_dir_all(&cfg.models_dir);
std::fs::create_dir_all(&cfg.models_dir).map_err(|e| {
format!(
"cannot create models dir {}: {e} — check that the gateway has write \
access (in Docker, mount a writable volume at the models path)",
cfg.models_dir
)
})?;
let base = if p.name.trim().is_empty() {
sanitize(p.repo.rsplit('/').next().unwrap_or(&p.repo))
} else {