From b3bb7ce43d710f03985996941e709eb30809702c Mon Sep 17 00:00:00 2001 From: janosmurai Date: Sun, 18 Oct 2026 19:56:30 +0200 Subject: [PATCH 1/2] feat(compose): pass the shaide server URL to the webapp (SHD-1088) Signed-off-by: janosmurai --- compose.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compose.yaml b/compose.yaml index 6500bbc..b807f57 100644 --- a/compose.yaml +++ b/compose.yaml @@ -63,6 +63,12 @@ services: ports: # Host port 3001 is where a natively running shaide server expects it. - "3001:8787" + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + # The shaide server the webapp signs users in to, resolved from inside + # this container. Same address as for the control panel below. + SHAIDE_SERVER_URL: http://${SHAIDE_SERVER_FQDN:-shaide_server}:${SHAIDE_SERVER_PORT:-8080} vectordb: image: qdrant/qdrant From b406209d20d913db70a33c86d1810896d68050e4 Mon Sep 17 00:00:00 2001 From: janosmurai Date: Mon, 19 Oct 2026 15:15:51 +0200 Subject: [PATCH 2/2] refactor(compose): reach the server at http://shaide-server like on k8s (SHD-1088) Signed-off-by: janosmurai --- README.md | 7 ++++++- compose.kc.yaml | 8 +++----- compose.yaml | 19 +++++++++++-------- justfile | 3 ++- scripts/kc/README.md | 2 +- scripts/kc/notifications.Caddyfile | 2 +- test/kc-compose/test_contract.py | 16 +++++++++------- test/platform-discovery/README.md | 6 +++--- test/platform-discovery/compose.yaml | 11 ++++++----- test/platform-discovery/smoke.py | 2 +- 10 files changed, 43 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 0fe1d32..a25b851 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ explicitly; ordinary startup does not require KC images or credentials. ```sh # Everything the shaide server depends on, to run the server natively -SHAIDE_SERVER_FQDN=host.docker.internal docker compose up -d +SHAIDE_SERVER_FQDN=host.docker.internal SHAIDE_SERVER_PORT=8080 docker compose up -d cargo run # Every service in containers, the shaide server included @@ -264,6 +264,11 @@ gateway (`mcp` profile) are optional: pass the profile names to the recipes, e.g. `just dev app mcp`, or `--profile app` to compose. When running the containerized server with the webapp, also set `WEBAPP_URL=http://webapp:8787`. +As on k8s, the containers reach the containerized server at +`http://shaide-server` (port 80). On the host it is published at +`SHAIDE_SERVER_HOST_PORT` (default `8080`). `SHAIDE_SERVER_FQDN` and +`SHAIDE_SERVER_PORT` point the containers at a natively running server instead. + The containerized server is built from the `dev` Docker target, which adds `azure-cli`. It keeps its data in `~/.config/axem-docker`, separate from the native server's `~/.config/axem`, and reads a copy of `~/.azure` (create the diff --git a/compose.kc.yaml b/compose.kc.yaml index 7fb14cb..ccf5a3e 100644 --- a/compose.kc.yaml +++ b/compose.kc.yaml @@ -2,11 +2,11 @@ # Requires Compose >= 2.24.4 (!override) and operator-supplied local/private images. # No private code, image builds, account registration or storage credentials in server. services: - shaide_server: + shaide-server: build: !reset null pull_policy: never ports: !override - - "127.0.0.1:${SHAIDE_SERVER_PORT:-8080}:${SHAIDE_SERVER_PORT:-8080}" + - "127.0.0.1:${SHAIDE_SERVER_HOST_PORT:-8080}:80" environment: ADMIN_PASSWORD: !reset null JWT_SECRET: !reset null @@ -71,7 +71,7 @@ services: S3_FQDN: kc-rustfs S3_PORT: "9000" VECTOR_DB_URL: http://kc-qdrant:6334 - SHAIDE_SERVER_URL: http://shaide_server:${SHAIDE_SERVER_PORT:-8080} + SHAIDE_SERVER_URL: http://shaide-server KC_SHAIDE_USERNAME: ${KC_SHAIDE_USERNAME:?dedicated role=user service account required} RUSTFS_WEBHOOK_ARN: arn:rustfs:sqs:eu-central-1:shaide:webhook KC_CONTEXT_ISSUER: shaide-local @@ -213,8 +213,6 @@ services: pull_policy: never user: "${KC_LOCAL_UID:?operator uid required}:${KC_LOCAL_GID:?operator gid required}" command: [caddy, run, --config, /etc/caddy/Caddyfile, --adapter, caddyfile] - environment: - SHAIDE_SERVER_PORT: ${SHAIDE_SERVER_PORT:-8080} volumes: - ./scripts/kc/notifications.Caddyfile:/etc/caddy/Caddyfile:ro networks: [kc_notify_storage, kc_notify_server] diff --git a/compose.yaml b/compose.yaml index b807f57..462f42c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -6,12 +6,15 @@ name: shaide-local-infra # - `docker compose --profile server up -d` also runs the server (`just stack`). # Optional services: `--profile app` (webapp) and `--profile mcp` (MCP gateway). # +# Like on k8s, the other containers reach the server at http://shaide-server +# (port 80). The server is published on the host at SHAIDE_SERVER_HOST_PORT. +# # Images default to a local build of this repo and the released images of the # other components. Override them with SHAIDE_SERVER_IMAGE, SHAIDE_WEBAPP_IMAGE # and SHAIDE_CONTROL_PANEL_IMAGE, e.g. SHAIDE_WEBAPP_IMAGE=shaide_webapp:latest. services: - shaide_server: + shaide-server: profiles: [server] build: context: . @@ -21,13 +24,13 @@ services: extra_hosts: - "host.docker.internal:host-gateway" ports: - - "${SHAIDE_SERVER_PORT:-8080}:${SHAIDE_SERVER_PORT:-8080}" + - "${SHAIDE_SERVER_HOST_PORT:-8080}:80" environment: # Bootstraps the initial `admin` user. JWT_SECRET signs user JWTs and # must be at least 32 bytes -- config.rs asserts on it at startup. ADMIN_PASSWORD: admin JWT_SECRET: local-development-jwt-secret-change-me - PORT: ${SHAIDE_SERVER_PORT:-8080} + PORT: "80" SHAIDE_SERVER_UI_FQDN: control-panel SHAIDE_SERVER_UI_PORT: 3000 # Passed through only when set, since the `/app` proxy is optional. Use @@ -68,7 +71,7 @@ services: environment: # The shaide server the webapp signs users in to, resolved from inside # this container. Same address as for the control panel below. - SHAIDE_SERVER_URL: http://${SHAIDE_SERVER_FQDN:-shaide_server}:${SHAIDE_SERVER_PORT:-8080} + SHAIDE_SERVER_URL: http://${SHAIDE_SERVER_FQDN:-shaide-server}${SHAIDE_SERVER_PORT:+:${SHAIDE_SERVER_PORT}} vectordb: image: qdrant/qdrant @@ -83,10 +86,10 @@ services: extra_hosts: - "host.docker.internal:host-gateway" environment: - # `shaide_server` for the containerized server; `just dev` sets it to - # `host.docker.internal` to reach the native one. - SHAIDE_SERVER_FQDN: ${SHAIDE_SERVER_FQDN:-shaide_server} - SHAIDE_SERVER_PORT: ${SHAIDE_SERVER_PORT:-8080} + # `shaide-server:80` for the containerized server; `just dev` sets + # `host.docker.internal:8080` to reach the native one. + SHAIDE_SERVER_FQDN: ${SHAIDE_SERVER_FQDN:-shaide-server} + SHAIDE_SERVER_PORT: ${SHAIDE_SERVER_PORT:-80} # HMAC key for the signed session cookie. Must be at least 32 characters # or the Control Panel refuses to sign any session and every login fails. SESSION_SECRET: ${SESSION_SECRET:-local-development-session-secret-change-me} diff --git a/justfile b/justfile index 37166da..33a0661 100644 --- a/justfile +++ b/justfile @@ -15,9 +15,10 @@ check: # Start what a natively running shaide server needs; optional profiles: app, mcp [env("SHAIDE_SERVER_FQDN", "host.docker.internal")] +[env("SHAIDE_SERVER_PORT", "8080")] services-up *profiles: # Free the server port in case the containerized server is still running. - docker compose --profile server stop shaide_server + docker compose --profile server stop shaide-server docker compose {{ prepend("--profile ", profiles) }} up -d --remove-orphans # Stop every compose service, including the optional ones diff --git a/scripts/kc/README.md b/scripts/kc/README.md index 63d117d..222662c 100644 --- a/scripts/kc/README.md +++ b/scripts/kc/README.md @@ -51,7 +51,7 @@ shell function ensures every command uses the same project, overlay and settings ```sh export KC_CONFIG='/config.env' -export SHAIDE_SERVER_PORT=18081 # choose an unused port; do not restart another stack +export SHAIDE_SERVER_HOST_PORT=18081 # choose an unused port; do not restart another stack kc_compose() { docker compose --env-file "$KC_CONFIG" -p shaide-kc-local \ -f compose.yaml -f compose.kc.yaml --profile server "$@" diff --git a/scripts/kc/notifications.Caddyfile b/scripts/kc/notifications.Caddyfile index 5bc6b2c..bbef767 100644 --- a/scripts/kc/notifications.Caddyfile +++ b/scripts/kc/notifications.Caddyfile @@ -9,7 +9,7 @@ } handle @event { # Fixed destination; incoming bearer token is authenticated by shaide. - reverse_proxy shaide_server:{$SHAIDE_SERVER_PORT:8080} { + reverse_proxy shaide-server:80 { transport http { dial_timeout 5s response_header_timeout 120s diff --git a/test/kc-compose/test_contract.py b/test/kc-compose/test_contract.py index a1bc912..a51df69 100644 --- a/test/kc-compose/test_contract.py +++ b/test/kc-compose/test_contract.py @@ -118,18 +118,18 @@ def test_missing_optional_settings_fail_only_overlay(self): def test_only_server_has_host_ports(self): m = self.model() for name, svc in m['services'].items(): - if name != 'shaide_server': self.assertFalse(svc.get('ports'), name) - self.assertEqual(m['services']['shaide_server']['ports'][0]['host_ip'], '127.0.0.1') + if name != 'shaide-server': self.assertFalse(svc.get('ports'), name) + self.assertEqual(m['services']['shaide-server']['ports'][0]['host_ip'], '127.0.0.1') def test_storage_and_vectors_are_isolated_from_server_and_mcp(self): m = self.model(); s = m['services'] for backend in ['kc-rustfs', 'kc-qdrant']: - for facade in ['shaide_server', 'kc-mcp']: + for facade in ['shaide-server', 'kc-mcp']: self.assertTrue(set(s[backend]['networks']).isdisjoint(s[facade]['networks'])) self.assertNotIn('default', s['knowledge-center']['networks']) for name, net in m['networks'].items(): if name != 'default': self.assertTrue(net['internal'], name) def test_server_has_no_storage_credentials_or_mounts(self): - m = self.model(); s = m['services']['shaide_server'] + m = self.model(); s = m['services']['shaide-server'] self.assertFalse(any(k.startswith(('S3_', 'RUSTFS_', 'AWS_')) and k != 'S3_WEBHOOK_TOKEN' for k in s['environment'])) self.assertFalse(any('storage' in x['source'] for x in s['secrets'])) def test_compose_output_contains_no_secret_values(self): @@ -138,17 +138,19 @@ def test_compose_output_contains_no_secret_values(self): self.assertNotIn((self.dest / n).read_text().strip(), output) def test_named_state_and_no_build_contract(self): m = self.model(); s = m['services'] - for service in ['shaide_server', 'kc-rustfs', 'kc-qdrant']: + for service in ['shaide-server', 'kc-rustfs', 'kc-qdrant']: self.assertTrue(any(v['type'] == 'volume' for v in s[service]['volumes'])) for name in ['knowledge-center', 'kc-mcp', 'kc-rustfs']: self.assertNotIn('build', s[name]); self.assertEqual(s[name]['pull_policy'], 'never') - self.assertEqual(s['shaide_server']['environment']['VECTOR_DB_URL'], 'http://vectordb:6334') + self.assertEqual(s['shaide-server']['environment']['VECTOR_DB_URL'], 'http://vectordb:6334') def test_signed_context_and_notification_trust(self): - s = self.model()['services']; env = s['shaide_server']['environment'] + s = self.model()['services']; env = s['shaide-server']['environment'] targets = json.loads(env['MCP_COLLECTION_CONTEXT_TARGETS']) self.assertEqual(targets[0]['url'], 'http://kc-mcp:8080') self.assertEqual(env['KNOWLEDGE_CENTER_URL'], 'http://knowledge-center:8080') self.assertIn('kc-notifications:8080/v1/object-storage/event', s['kc-rustfs']['environment']['RUSTFS_NOTIFY_WEBHOOK_ENDPOINT_SHAIDE']) self.assertEqual(s['knowledge-center']['environment']['RUSTFS_WEBHOOK_ARN'], 'arn:rustfs:sqs:eu-central-1:shaide:webhook') + self.assertEqual(s['knowledge-center']['environment']['SHAIDE_SERVER_URL'], 'http://shaide-server') + self.assertEqual(s['shaide-server']['ports'][0]['target'], 80) if __name__ == '__main__': unittest.main() diff --git a/test/platform-discovery/README.md b/test/platform-discovery/README.md index 42d8727..acbe96b 100644 --- a/test/platform-discovery/README.md +++ b/test/platform-discovery/README.md @@ -63,7 +63,7 @@ For example: ```sh printf '%s\n' '{"scenario":"missing-metadata"}' > fixtures/scenario.json -docker compose restart shaide_server +docker compose restart shaide-server ``` Then click **Refresh available models**. Restarting clears the server's @@ -77,7 +77,7 @@ The fixture definitions and network aliases are in `mock.py` and `compose.yaml`. ```sh python3 smoke.py docker compose ps -docker compose logs --tail=40 platform-mock shaide_server control-panel +docker compose logs --tail=40 platform-mock shaide-server control-panel docker compose down ``` @@ -90,5 +90,5 @@ removes the models it added, and leaves an ordinary test user in the isolated database with a one-day expiry. `down` preserves data for the next start. The mock's self-signed certificate is regenerated at mock startup. If you -restart or recreate `platform-mock` alone, also restart `shaide_server` so it +restart or recreate `platform-mock` alone, also restart `shaide-server` so it reloads the CA. None of these commands affect the existing Compose project. diff --git a/test/platform-discovery/compose.yaml b/test/platform-discovery/compose.yaml index 14be4cc..287b470 100644 --- a/test/platform-discovery/compose.yaml +++ b/test/platform-discovery/compose.yaml @@ -20,14 +20,15 @@ services: timeout: 2s retries: 20 - shaide_server: + shaide-server: init: true build: context: ../.. target: runtime image: shaide-server:discovery-demo - ports: ["127.0.0.1:18080:8080"] + ports: ["127.0.0.1:18080:80"] environment: + PORT: "80" ADMIN_PASSWORD: admin JWT_SECRET: discovery-demo-only-jwt-secret-change-me shaide_ROOT: /data @@ -51,10 +52,10 @@ services: image: shaide-control-panel:discovery-demo ports: ["127.0.0.1:13000:3000"] environment: - SHAIDE_SERVER_FQDN: shaide_server - SHAIDE_SERVER_PORT: "8080" + SHAIDE_SERVER_FQDN: shaide-server + SHAIDE_SERVER_PORT: "80" SESSION_SECRET: discovery-demo-only-session-secret-change-me - depends_on: [shaide_server] + depends_on: [shaide-server] vectordb: image: qdrant/qdrant:latest diff --git a/test/platform-discovery/smoke.py b/test/platform-discovery/smoke.py index a9c72fa..440dff3 100644 --- a/test/platform-discovery/smoke.py +++ b/test/platform-discovery/smoke.py @@ -40,7 +40,7 @@ def ok(url, **kwargs): def scenario(name): (DEMO / "fixtures/scenario.json").write_text(json.dumps({"scenario": name}) + "\n") - subprocess.run(COMPOSE + ["restart", "shaide_server"], check=True, stdout=subprocess.DEVNULL) + subprocess.run(COMPOSE + ["restart", "shaide-server"], check=True, stdout=subprocess.DEVNULL) for _ in range(60): try: if request(SERVER + "/v1/platform-models", session=ANON)[0] == 401: