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)