From 5d5072e019bd8834a702be6e9e8822d7ec7495c9 Mon Sep 17 00:00:00 2001 From: dvcdsys Date: Mon, 4 May 2026 16:11:42 +0100 Subject: [PATCH] fix(server): align CPU image port + /data ownership with CUDA image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two latent bugs in the v0.5.0 CPU image surfaced during post-release smoke testing of dvcdsys/code-index:v0.5.0 on macOS arm64: 1) Port mismatch — image baked ENV CIX_PORT=8001 (Python parallel-PoC carry-over) but docker-compose.yml mapped 21847:21847. A fresh `docker compose up -d` from the published files left the host port pointing at a non-listening container port; `curl` saw connection reset. The Python backend was archived 2026-04, so the rationale for 8001 is dead. CUDA image already uses 21847; CPU now matches. 2) /data ownership — distroless USER nonroot:nonroot (uid 65532) but the Dockerfile declared `VOLUME ["/data"]` without pre-creating the directory with nonroot ownership. A fresh Docker named volume inherited root ownership from the daemon and the runtime user got `mkdir /data/sqlite: permission denied` on first boot. CUDA image pre-creates /data via COPY --chown=1001:1001; CPU now does the same with --chown=65532:65532. Changes: - server/Dockerfile: ENV CIX_PORT=21847, EXPOSE 21847; mkdir /out/data in builder stage; COPY --chown=65532:65532 /out/data /data before VOLUME declaration. - server/internal/config/config.go: getenvInt default 8001 → 21847. Doc comment rewritten — "Python parallel rollout" rationale is dead. - server/internal/config/config_test.go: TestLoadDefaults expects 21847. - server/README.md: stale "phase 1 / Python parallel" preamble replaced; port table cell updated; quick-start docker run example uses 21847 and shows the now-required bootstrap admin envs. - docker-compose.yml + docker-compose.cuda.yml: explicit CIX_PORT=${CIX_PORT:-21847} as defense in depth against future Dockerfile regressions or third-party forks. - docker-compose.yml: bind-mount uid comment updated 1001 → 65532 (the CPU image's nonroot uid; CUDA's 1001 only applies to the CUDA compose file). Local verification: - `make` build green; full Go test suite green (one fixture updated). - `docker buildx build` of the patched CPU Dockerfile produces an arm64 image. `docker run` with a NAMED VOLUME (no --user override) and the DEFAULT port (no CIX_PORT env) now boots cleanly: /health → 200 /api/v1/auth/bootstrap-status → 200 /dashboard/ → 200 /api/v1/admin/users → 200 (admin session) Co-Authored-By: Claude Opus 4.7 --- docker-compose.cuda.yml | 4 ++++ docker-compose.yml | 17 ++++++++++++----- server/Dockerfile | 24 +++++++++++++++++++++--- server/README.md | 26 +++++++++++++++----------- server/internal/config/config.go | 10 ++++++---- server/internal/config/config_test.go | 4 ++-- 6 files changed, 60 insertions(+), 25 deletions(-) diff --git a/docker-compose.cuda.yml b/docker-compose.cuda.yml index 628a25e..a40032a 100644 --- a/docker-compose.cuda.yml +++ b/docker-compose.cuda.yml @@ -7,6 +7,10 @@ services: - "${PORT:-21847}:21847" environment: - CIX_API_KEY=${CIX_API_KEY} + # Defense in depth — the image already defaults to 21847 but + # pinning it here keeps the host:container port mapping honest + # if a third-party fork or custom build sets a different default. + - CIX_PORT=${CIX_PORT:-21847} - CIX_EMBEDDING_MODEL=${CIX_EMBEDDING_MODEL:-awhiteside/CodeRankEmbed-Q8_0-GGUF} - CIX_CHROMA_PERSIST_DIR=/data/chroma - CIX_SQLITE_PATH=/data/sqlite/projects.db diff --git a/docker-compose.yml b/docker-compose.yml index f20a807..e758c30 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,15 +7,20 @@ services: - "${PORT:-21847}:21847" environment: - CIX_API_KEY=${CIX_API_KEY} + # Defense in depth — the image already defaults to 21847 (since + # v0.5.1) but pinning it here keeps the host:container port mapping + # honest if a third-party fork or custom build sets a different + # default. + - CIX_PORT=${CIX_PORT:-21847} - CIX_EMBEDDING_MODEL=${CIX_EMBEDDING_MODEL:-awhiteside/CodeRankEmbed-Q8_0-GGUF} - CIX_CHROMA_PERSIST_DIR=/data/chroma - CIX_SQLITE_PATH=/data/sqlite/projects.db - CIX_MAX_FILE_SIZE=${CIX_MAX_FILE_SIZE:-524288} - CIX_EXCLUDED_DIRS=${CIX_EXCLUDED_DIRS:-node_modules,.git,.venv,__pycache__,dist,build,.next,.cache,.DS_Store} # GGUF cache lives on the named volume below — survives `docker compose - # down` (without -v) and is owned by the image's 1001:1001 user, so the - # cix-server process can always write to it regardless of host - # bind-mount permissions. + # down` (without -v) and is owned by the image's 65532:65532 (nonroot) + # user on the CPU image, so the cix-server process can always write to + # it regardless of host bind-mount permissions. - CIX_GGUF_CACHE_DIR=/data/models - CIX_LLAMA_BIN_DIR=/app - CIX_LLAMA_STARTUP_TIMEOUT=120 @@ -42,8 +47,10 @@ services: - CIX_BOOTSTRAP_GGUF_PATH=${CIX_BOOTSTRAP_GGUF_PATH:-} volumes: # Operator-managed bind for sqlite + chroma so backups and inspection - # are one `cd` away on the host. Make sure the directory is owned by - # 1001:1001 OR use `user: "0:0"` — see CLAUDE.md. + # are one `cd` away on the host. The CPU image runs as + # nonroot:nonroot (uid 65532) — chown your bind directory to + # 65532:65532 OR add `user: "0:0"` to fall back to root. See + # doc/SECURITY_DEPLOYMENT.md. - ${HOME}/.cix/data:/data # Docker-managed named volume layered ON TOP of /data/models. This # isolates the GGUF cache from host-side bind permission issues and diff --git a/server/Dockerfile b/server/Dockerfile index 989864f..d8cf02c 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -47,13 +47,24 @@ RUN CGO_ENABLED=0 GOOS=linux go build \ -o /out/cix-server \ ./cmd/cix-server +# Pre-create an empty /data tree so the final stage can COPY it in with +# nonroot (uid 65532) ownership. Without this, a fresh Docker named volume +# initialises root-owned and the distroless nonroot uid in the runtime +# stage cannot `mkdir /data/sqlite` on first boot. +RUN mkdir -p /out/data + FROM gcr.io/distroless/static-debian12:nonroot WORKDIR / COPY --from=builder /out/cix-server /cix-server -# Default port; override with CIX_PORT at runtime. -ENV CIX_PORT=8001 -EXPOSE 8001 +# Default port — matches CIX_PORT in docker-compose.yml port mapping +# (21847:21847) and the CUDA image (Dockerfile.cuda). The earlier 8001 +# default was a Python-FastAPI compat carry-over from the migration era; +# the Python backend was archived 2026-04, so the parity is no longer +# meaningful and the mismatch caused fresh `docker compose up -d` runs +# to leave the host port pointing at a non-listening container port. +ENV CIX_PORT=21847 +EXPOSE 21847 # Container data layout — pin paths under /data so the named volume in # portainer-stack.yml / docker-compose receives both SQLite and chromem-go @@ -61,6 +72,13 @@ EXPOSE 8001 # inappropriate inside distroless where there is no usable HOME. ENV CIX_SQLITE_PATH=/data/sqlite/projects.db ENV CIX_CHROMA_PERSIST_DIR=/data/chroma + +# Pre-owned /data directory (distroless has no shell to mkdir+chown). +# A fresh Docker named volume mounted onto /data inherits the 65532:65532 +# ownership baked here, so the nonroot runtime user can write the sqlite +# and chroma trees on first boot. Bind mounts still need host-side chown +# to 65532:65532 — see doc/SECURITY_DEPLOYMENT.md. +COPY --from=builder --chown=65532:65532 /out/data /data VOLUME ["/data"] USER nonroot:nonroot diff --git a/server/README.md b/server/README.md index 521eeb5..2180d3d 100644 --- a/server/README.md +++ b/server/README.md @@ -1,7 +1,8 @@ -# api-go-poc — cix-server (Go) +# cix-server (Go) -Phase 1 scaffold of the Go rewrite of `api/` (Python/FastAPI). Runs in parallel -to Python during the PoC — default port is **8001** (Python uses 21847). +The Go HTTP server backing cix's indexing + dashboard. Default port is +**21847** (was 8001 during the Python parallel-rollout era; the Python +backend was archived 2026-04 and the parity is no longer meaningful). ## Layout @@ -16,24 +17,27 @@ Dockerfile CPU multi-stage, distroless runtime ## Build / run / test ```bash -cd api-go-poc +cd server go build ./... go vet ./... go test ./... -# Local run (binds :8001 by default) -CIX_SQLITE_PATH=/tmp/cix-phase1.db ./cix-server +# Local run (binds :21847 by default) +CIX_SQLITE_PATH=/tmp/cix.db ./cix-server # Or with version injected: -go build -ldflags "-X main.version=0.2.0-go" -o cix-server ./cmd/cix-server +go build -ldflags "-X main.version=v0.5.1" -o cix-server ./cmd/cix-server ``` ## Docker ```bash -docker build -t cix-server-go:phase1 --build-arg VERSION=0.2.0-go . -docker run --rm -p 8001:8001 \ +docker build -t cix-server-go:dev --build-arg VERSION=v0.5.1 . +docker run --rm -p 21847:21847 \ + -e CIX_API_KEY=cix_ \ + -e CIX_BOOTSTRAP_ADMIN_EMAIL=admin@example.com \ + -e CIX_BOOTSTRAP_ADMIN_PASSWORD= \ -v cix-data:/data \ - cix-server-go:phase1 + cix-server-go:dev ``` ## Environment variables @@ -43,7 +47,7 @@ All are optional; defaults match `api/app/config.py` except `CIX_PORT`. | Var | Default | Notes | |---|---|---| | `CIX_API_KEY` | `""` | Warned at startup if empty; enforced from Phase 2 | -| `CIX_PORT` | `8001` | Python uses 21847 — different to allow parallel run | +| `CIX_PORT` | `21847` | Listen port. Both Docker images bake this in. | | `CIX_EMBEDDING_MODEL` | `awhiteside/CodeRankEmbed-Q8_0-GGUF` | | | `CIX_CHROMA_PERSIST_DIR` | `/data/chroma` | Name kept for compat; backend changes in Phase 4 | | `CIX_SQLITE_PATH` | `/data/sqlite/projects.db` | Suffixed with model-safe name on open | diff --git a/server/internal/config/config.go b/server/internal/config/config.go index 951d3fb..541a1ce 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -12,9 +12,11 @@ import ( "strings" ) -// Config holds all runtime settings. Defaults match api/app/config.py except -// for Port, which is 8001 by default so the Go server does not collide with -// the Python server (21847) during parallel PoC rollout. +// Config holds all runtime settings. Port defaults to 21847 — the same +// value the Docker images bake into ENV CIX_PORT and the same the +// docker-compose templates map on the host side. The earlier 8001 +// default was a Python-FastAPI parallel-rollout carry-over; the Python +// backend was archived 2026-04 and the parity is no longer meaningful. type Config struct { APIKey string // AuthDisabled, when true, makes the server skip the API-key check on @@ -122,7 +124,7 @@ func Load() (*Config, error) { } c.AuthDisabled = authOff - port, err := getenvInt("CIX_PORT", 8001) + port, err := getenvInt("CIX_PORT", 21847) if err != nil { return nil, err } diff --git a/server/internal/config/config_test.go b/server/internal/config/config_test.go index bf4ff9d..c1e2310 100644 --- a/server/internal/config/config_test.go +++ b/server/internal/config/config_test.go @@ -15,8 +15,8 @@ func TestLoadDefaults(t *testing.T) { if err != nil { t.Fatalf("Load: %v", err) } - if c.Port != 8001 { - t.Errorf("Port default = %d, want 8001", c.Port) + if c.Port != 21847 { + t.Errorf("Port default = %d, want 21847", c.Port) } if c.EmbeddingModel != "awhiteside/CodeRankEmbed-Q8_0-GGUF" { t.Errorf("EmbeddingModel default = %q", c.EmbeddingModel)