diff --git a/.dockerignore b/.dockerignore index 6fe18bc..cb8f2b4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,6 @@ target +models +*.cmf .git .github docs diff --git a/Dockerfile b/Dockerfile index f3fbf3a..35d2e2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index 475439d..f801fae 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/docker-compose.yml b/docker-compose.yml index 82e60ae..e040d62 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/src/import.rs b/src/import.rs index fb97cf4..0317c47 100644 --- a/src/import.rs +++ b/src/import.rs @@ -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, 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 {