diff --git a/docker-compose.dev-sx-host.yml b/docker-compose.dev-sx-host.yml new file mode 100644 index 00000000..8a63156e --- /dev/null +++ b/docker-compose.dev-sx-host.yml @@ -0,0 +1,37 @@ +# host-on-sx live service — the SX web host (lib/host) served by the native +# http-listen server via lib/host/serve.sh. Joins the sx-dev project + externalnet +# so Caddy can reverse_proxy a subdomain to it (blog.rose-ash.com). Isolated from +# the sx_docs server: separate container, separate port. +# +# Usage: +# docker compose -p sx-dev -f docker-compose.dev-sx-host.yml up -d sx_host +# docker compose -p sx-dev -f docker-compose.dev-sx-host.yml logs -f sx_host +# docker compose -p sx-dev -f docker-compose.dev-sx-host.yml down + +services: + sx_host: + image: registry.rose-ash.com:5000/sx_docs:latest + container_name: sx-dev-sx_host-1 + entrypoint: ["bash", "/app/lib/host/serve.sh"] + working_dir: /app + environment: + SX_PROJECT_DIR: /app + SX_SERVER: /app/bin/sx_server + HOST_PORT: "8000" + # Bind all interfaces so Caddy (on externalnet) can reach it. + SX_HTTP_HOST: "0.0.0.0" + OCAMLRUNPARAM: "b" + volumes: + # SX source (hot-reload on container restart) + - ./spec:/app/spec:ro + - ./lib:/app/lib:ro + # OCaml server binary — this worktree's build (has the SX_HTTP_HOST bind fix) + - ./hosts/ocaml/_build/default/bin/sx_server.exe:/app/bin/sx_server:ro + networks: + - externalnet + - default + restart: unless-stopped + +networks: + externalnet: + external: true diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index e95fcba3..006ab145 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -745,8 +745,15 @@ let setup_evaluator_bridge env = | _ -> raise (Eval_error "http-listen: (port handler)") in let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in Unix.setsockopt sock Unix.SO_REUSEADDR true; + (* Bind host: loopback by default (safe for tests + local runs); set + SX_HTTP_HOST=0.0.0.0 to expose on the network (container/Caddy). *) + let bind_addr = + match Sys.getenv_opt "SX_HTTP_HOST" with + | Some h -> (try Unix.inet_addr_of_string h + with _ -> Unix.inet_addr_loopback) + | None -> Unix.inet_addr_loopback in Unix.bind sock - (Unix.ADDR_INET (Unix.inet_addr_loopback, port)); + (Unix.ADDR_INET (bind_addr, port)); Unix.listen sock 64; (* SX runtime is shared across threads — serialize handler calls. *) let mtx = Mutex.create () in diff --git a/lib/host/serve.sh b/lib/host/serve.sh index d19b76a5..6f79032f 100755 --- a/lib/host/serve.sh +++ b/lib/host/serve.sh @@ -13,7 +13,9 @@ # exactly what the suites verify. set -uo pipefail -cd "$(git rev-parse --show-toplevel)" +# Project root: SX_PROJECT_DIR in containers (set to /app by the compose stack), +# else the git toplevel for local runs. +cd "${SX_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo .)}" SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}" if [ ! -x "$SX_SERVER" ]; then diff --git a/plans/host-on-sx.md b/plans/host-on-sx.md index 42cdc24b..adc1118a 100644 --- a/plans/host-on-sx.md +++ b/plans/host-on-sx.md @@ -133,8 +133,18 @@ lib/host/sxtp.sx subsystem APIs (feed/search/commerce/… - [x] native `http-listen` ↔ Dream-app bridge (`lib/host/server.sx`: `host/native-handler`/`host/serve`) + `lib/host/serve.sh` launcher. Serves real HTTP on a host port — verified live (health/feed/relations reads + 404). -- [ ] promote into the docker stack + a Caddy subdomain (NOT `rose-ash.com` — that - is the legacy public site, untouched). Scope now includes `hosts/` + Caddy. +- [x] promote into the docker stack + a Caddy subdomain — **LIVE at + `https://blog.rose-ash.com`** (reusing a down Quart subdomain). New compose + service `sx_host` (`docker-compose.dev-sx-host.yml`, container + `sx-dev-sx_host-1`) runs `serve.sh` on `externalnet`; Caddy reverse-proxies + `blog.rose-ash.com` → `sx-dev-sx_host-1:8000`. Required a `hosts/` fix: + `http-listen` bound `inet_addr_loopback` only — added `SX_HTTP_HOST` env + (default loopback; stack sets `0.0.0.0`) in `sx_server.ml`, rebuilt this + worktree's binary. Verified: `/health`, `/feed`, relations reads serve real + JSON through Cloudflare→Caddy; `/` 404 (no root route yet). `rose-ash.com` + untouched. CAVEAT: `/root/caddy/Caddyfile` is an inode-pinned bind mount — + edited file shows a NEW inode the container can't see; loaded live via + `caddy reload` from a non-bind path. A caddy restart reconciles the bind. - [ ] proxy-to-Quart fallback for un-migrated paths (strangler requirement before a real subdomain fronts users). - [ ] internal-HMAC middleware on `/internal/*` (service-to-service auth; protocol