cli.sgit.ai / Research / sgit serve: container first. Options map and spike results

sgit serve: container first. Options map and spike results

Date2026-10-12 · 22:25 UTCKinddecisionTopicsserve containers dockerWritten bythe sgit-ai CLI team (Claude Code sessions), for Dinis CruzSourceteam/humans/dinis_cruz/claude-code-web/10/12/options__sgit-serve-container-and-static.md in SGit-AI__CLI at 5597526. Published here as written; the CLI repository is where the work happened.

Date: 2026-10-12 · Status: options and plan, no sgit code yet · Supersedes the "native server" plan in research__sgit-serve-local-vault-server.md (same folder). The API inventory and browser notes there still apply.

Decision (Dinis): run a working vault locally from the SG/Send container image. It has the vault UI and the API in one image and always tracks the latest SG/Send. sgit gets native support for it:

The current read-only server stays as a separate, clearly named mode.

Update, same day: decisions taken and phase 1 built.

  1. Image latest, with the digest that ran recorded.
  2. sgit serve install installs, after showing what and asking.
  3. Both Apple container and Podman are supported on macOS.
  4. sgit vault serve moved to sgit serve --static.
  5. The brief to SG/Send is in this folder.

The Apple container adapter is written to its documented CLI but not yet run on a Mac.

1. Two modes

ModeCommandWhat it isWritesNeeds
container (default)sgit serveThe SG/Send image (diniscruz/sg-send-vault): vault UI + full API + disk store, on localhostyes. A real local vault server: the UI edits, sgit pushes and pullsa container runtime, or the Python engine (§4)
staticsgit serve --static (today's sgit vault serve)A stdlib read-only file server for a published vault: the static loader plus the encrypted store, no APInonothing

2. What I verified today (spike in this cloud container)

All of it was run here, x86_64 Linux, Docker 29.6 / containerd 2.2. The screenshot spike__local-container-ui-open-vault.png is next to this file.

#CheckResult
1docker pull diniscruz/sg-send-vault:latestabout 10 s; 400 MB; a multi-arch index (amd64 + arm64); server v0.34.0, UI v0.2.3; 31 tags (v0.27.70 … v0.34.0)
2docker run -p 8080:8080 -v <dir>:/data/api/info/health ok; UI / 200
3sgit init / commit / push / clone --base-url http://127.0.0.1:8080 (repo code)works: 12 objects on disk at <dir>/vault/iv/ivh5cbkj/{manifest.json,bare/…}; the fresh clone has the files
4The image runs as uid 10001 (its README still says root)a host folder owned by the user is not writable: Permission denied: '/data/vault'. Fix: --user $(id -u):$(id -g), which sgit must always pass
5sgit refuses to push with no token ("no access token configured") even though the server has no authpassing any --token works. sgit fix: a server that needs no token is a normal case
6Headless Chromium → http://localhost:8080/en-gb/vault/, key typed into the formthe vault opens and decrypts: docs/, a.md ("hello local"). Same origin, plain http on localhost, Web Crypto fine
7#<vault-key> in the URLdid not auto-open (the form did)
8The UI loads sg-layout.js and sg-llm-events.js from dev.tools.sgraph.ai at runtimenot self-contained, despite "no CDN dependency" in its README: offline or air-gapped use will break
9The UI shows "Read-only" until an access key is enteredexpected; sgit serve should print the access-key hint, or configure the container so the UI is writable
10containerd only (ctr run, no Docker API)works (image imported, push lands on disk)
11Anonymous ctr pull from Docker Hub429 Too Many Requests; Docker Hub's anonymous limit is 100 pulls per 6 h per IP
12No container at all: the image's own server code (sgraph_ai_app_send__docker.app) under a Python 3.12 venv, with the UI folder copied out of the imageworks: health, UI, and an sgit push to a disk store
13CORS for the hosted UI (Origin: https://vault.sgraph.ai)preflight 200 with Access-Control-Allow-Origin: * … but with Access-Control-Request-Private-Network: true → 400 "Disallowed CORS private-network". Starlette's allow_private_network is off, so Chrome's private-network preflight from vault.sgraph.ai to localhost fails
14A server error responsecontains the full Python stack trace (an information leak; fine on a laptop, not on a shared host)

3. Runtimes: what exists and how sgit would drive each

"docker-compatible" means it accepts run -d --name -p -v --user -e, ps, stop, logs and pull with the same flags, so one adapter covers all of them. Rows marked † come from my knowledge rather than today's run; check each against its current docs when building the adapter.

RuntimeOSCLIInstall (what sgit serve install would do or print)Notes
Docker EngineLinuxdockerdistro package / get.docker.comthe reference; verified here
Docker DesktopmacOS, Windowsdockerthe vendor installerLinux VM under the hood; paid licence for larger companies†
PodmanLinux, macOS, Windowspodman (docker-compatible)brew install podman + podman machine init/start on mac/Windows; distro package on Linuxdaemonless, rootless by default†; with --userns=keep-id the uid problem (#4) goes away; no Docker licence
Apple containermacOS 26 on Apple silicon (macOS 15 runs with reduced networking)container (its own CLI, docker-like)signed .pkg from GitHub apple/container releases, then container system startone lightweight VM per container, runs OCI images, no Docker. Its own adapter; flags to confirm against container run --help (ports, mounts)
ColimamacOS, Linuxgives a docker (or nerdctl) contextbrew install colima docker && colima startfree, Lima VM; then the docker adapter†
OrbStackmacOSdockervendor appfast; commercial licence for business use†
Rancher DesktopmacOS, Windows, Linuxdocker or nerdctlvendor installerfree (SUSE)†
FinchmacOS, Windows, Linuxfinch (nerdctl-based, docker-like)AWS installer / brewfree (AWS)†
containerd + nerdctlLinuxnerdctl (docker-compatible)distro or release tarballlean Linux servers; raw ctr works too (verified, #10)
WSL2Windowsany of the above inside WSLwsl --install + one of the aboveWindows without Docker Desktop
Python engine (no container)anywhere with uvuv run --python 3.12 --with sgraph-ai-app-send …uv (one static binary); uv fetches Python 3.12 itselfthe same server code as the image, verified (#12); needs the built UI folder (§5)

Detection order: a runtime already running wins. Otherwise the order is:

  1. docker (context answers)
  2. podman
  3. container (Apple)
  4. nerdctl
  5. finch
  6. the Python engine if uv is found
  7. --static as the last resort, with a message saying what the user is missing

sgit serve --runtime <name> overrides the choice. sgit serve runtimes lists what it found, and for anything missing, the one install command per OS.

Installing:

4. What sgit serve does (container mode)

sgit serve                     # inside a clone
  1. pick a runtime (§3); pull the image, pinned by digest and recorded (latest by default; --tag vX.Y.Z)
  2. data dir: <clone>/.sg_vault/serve/ (or --data DIR, interchangeable with any sg-send-vault /data)
  3. run: --name sgit-serve-<vault_id> --user <uid>:<gid> -p 127.0.0.1:<port>:8080 -v <data>:/data
          (-p on 127.0.0.1 only: a plain `-p 8080:8080` publishes on every interface)
  4. wait for /api/info/health
  5. seed: push this clone into it as a remote, `local` → http://127.0.0.1:<port>
           (the existing push code: no coupling to the server's disk layout)
  6. print the UI URL, the vault-key and access-key hints; --open launches the browser
sgit serve status | logs | stop | update     # update = pull the newer image, restart, same data dir
sgit serve --static                          # today's read-only published-folder server

Then:

The server owns its own store. The clone's .sg_vault/bare/ is never mounted, because the UI writing the clone's accepted refs would undo what 0.21 fixed.

Python engine: steps 1 and 3 become uv run --python 3.12 --with sgraph-ai-app-send==<v> python -m … --port <p>, with SEND__STORAGE_MODE=disk, SEND__DISK_PATH=<data> and SEND__VAULT_STATIC_DIR=<ui dir>. Everything else is the same.

TLS: only for the LAN or Safari cases (see the research note §5–6).

5. Asks of the SG/Send team (from the spike)

  1. Vendor the dev.tools.sgraph.ai scripts into the image (#8). Today the "self-contained" UI needs the internet.
  2. allow_private_network=True in the CORS middleware (#13). Without it, the hosted UI cannot reach a local server from Chrome.
  3. No stack traces in error responses (#14), or only behind a debug flag.
  4. README: the image runs as uid 10001, so document --user or chown (#4).
  5. Also publish to GHCR or ECR Public (#11). sgit can then fall back when Docker Hub rate-limits.
  6. Publish the built vault UI (/app/static_vault) as a versioned release asset. That is what the Python engine needs; today it can only be copied out of the image.
  7. Optionally, let #<vault-key> (or a ?key= handoff) auto-open on /en-gb/vault/ (#7), so sgit serve --open lands inside the vault.

6. Changes in sgit

ChangeSize
Container__Runtime (detect, pull, run, ps, stop, logs, health wait) with adapters: docker-compatible (docker, podman, nerdctl, finch), Apple container, Python engine (uv)~350 lines
CLI__Serve: container mode (seed via push to a local remote, banner, --open), status/logs/stop/update/runtimes/install, --static = today's server; sgit vault serve points to it~250 lines
A token is optional when the server has no auth (#5)small
TLS cert generation + mount~100 lines
Tests:
• unit: each adapter's command lines, against a fake runtime binary on PATH;
• integration: real Docker in CI (the daemon starts in this kind of environment, as shown today); clone, push and pull against the image; a Playwright check that the UI opens the vault
~1 day

Estimate:

PhaseWorkEstimate
1Docker-compatible adapter + serve, stop, status, logs + seeding2 days
2Apple container adapter + Python engine1–1.5 days; the Apple adapter needs a Mac to test on
3runtimes/install + TLS1 day
4Docs, threat model rows½ day

Total: about 5 days. Phase 1 alone gives you "clone a vault, sgit serve, edit it in the browser".

7. Decisions for you

  1. Image tag: default to latest (always current, as you suggested) and record the digest in use, or pin per sgit release for reproducibility? My suggestion: latest, plus --tag and a recorded digest.
  2. Installing runtimes: print the command only, or run it after confirmation?
  3. Apple container vs Podman as the macOS recommendation: Apple's needs macOS 26 on Apple silicon; Podman runs everywhere.
  4. --static naming: keep sgit vault serve as an alias, or move it fully?
  5. Should I send the §5 asks to the SG/Send team as a brief?

Sources

This page as markdown · its source, admin/content/articles/sgit-serve-container-first.md