diff --git a/.env.template b/.env.template index 50e9922..8181941 100644 --- a/.env.template +++ b/.env.template @@ -19,7 +19,7 @@ JWT_AUDIENCE=nexus-web BOOTSTRAP_OWNER_EMAIL=*** # ── OpenClaw Integration ──────────────────────────────── -# Base URL of the OpenClaw gateway (host.docker.internal from inside container) -OPENCLAW_BASE_URL=http://host.docker.internal:18789 +# Internal Docker-DNS URL of the OpenClaw gateway +OPENCLAW_BASE_URL=http://openclaw-gateway-bao:18789 OPENCLAW_GATEWAY_TOKEN=*** OPENCLAW_GATEWAY_PASSWORD=*** diff --git a/.gitea/scripts/deploy-nexus.sh b/.gitea/scripts/deploy-nexus.sh index 0c06348..6df18da 100755 --- a/.gitea/scripts/deploy-nexus.sh +++ b/.gitea/scripts/deploy-nexus.sh @@ -60,7 +60,7 @@ JWT_KEY=${ENV_JWT_KEY} JWT_ISSUER=nexus JWT_AUDIENCE=nexus-web BOOTSTRAP_OWNER_EMAIL=vmbao62@hotmail.de -OPENCLAW_BASE_URL=http://host.docker.internal:18789 +OPENCLAW_BASE_URL=http://openclaw-gateway-bao:18789 OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN:-} OPENCLAW_GATEWAY_PASSWORD= NEXUS_VERSION=${VERSION} @@ -140,9 +140,15 @@ echo "Checking live health" retry=0 while [ "$retry" -lt 6 ]; do retry=$((retry + 1)) - if curl -fsS --max-time 10 "$BASE_URL/health" >/dev/null; then - echo "Health check passed" - break + health_body="$(curl -fsS --max-time 10 "$BASE_URL/health" 2>/dev/null || true)" + case "$health_body" in + '{"status":"Healthy"'*) + echo "Health check passed" + break + ;; + esac + if [ -n "$health_body" ]; then + echo "Health endpoint is reachable but not healthy: $health_body" >&2 fi if [ "$retry" -eq 6 ]; then echo "Health check failed" >&2 diff --git a/.gitea/workflows/rollback.yaml b/.gitea/workflows/rollback.yaml index 40adc21..8f946e4 100644 --- a/.gitea/workflows/rollback.yaml +++ b/.gitea/workflows/rollback.yaml @@ -112,9 +112,11 @@ jobs: JWT_ISSUER=nexus JWT_AUDIENCE=nexus-web BOOTSTRAP_OWNER_EMAIL=vmbao62@hotmail.de - OPENCLAW_BASE_URL=http://host.docker.internal:18789 + OPENCLAW_BASE_URL=http://openclaw-gateway-bao:18789 OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN} OPENCLAW_GATEWAY_PASSWORD= + NEXUS_VERSION=$(tr -d '[:space:]' < VERSION) + NEXUS_GIT_SHA=$(git rev-parse HEAD) EOF chmod 600 "${ENV_TMPFILE}" @@ -127,18 +129,38 @@ jobs: run: | set -euo pipefail - docker run --rm \ - -v "${{ gitea.workspace }}:/src:ro" \ + git archive --format=tar HEAD | docker run --rm -i \ -v "${DEPLOY_PATH}:/dest" \ alpine:latest \ - sh -c " - cd /src && \ - find . -mindepth 1 -maxdepth 1 \ - ! -name .git \ - -exec cp -r {} /dest/ \; && \ - DEST_OWNER=\$(stat -c '%u:%g' /dest) && \ - chown -R \"\$DEST_OWNER\" /dest - " + sh -c ' + set -eu + dest_owner="$(stat -c "%u:%g" /dest)" + mkdir -p /src-snapshot + tar -xf - -C /src-snapshot + + is_protected_path() { + case "$1" in + ./.git|./.env|./.env.*|./data|./logs|./backups|./tmp|./uploads|./storage) + return 0 + ;; + *) + return 1 + ;; + esac + } + + cd /dest + find . -mindepth 1 -maxdepth 1 | while IFS= read -r path; do + if ! is_protected_path "$path"; then rm -rf "$path"; fi + done + + cd /src-snapshot + find . -mindepth 1 -maxdepth 1 | while IFS= read -r path; do + if ! is_protected_path "$path"; then cp -a "$path" /dest/; fi + done + + chown -R "$dest_owner" /dest + ' echo "✅ Rollback code (${{ inputs.target_tag }}) synced to ${DEPLOY_PATH}" @@ -151,16 +173,18 @@ jobs: docker run --rm \ -v "${DEPLOY_PATH}:/workspace/nexus" \ - -v "/tmp:/tmp-host:ro" \ -v /var/run/docker.sock:/var/run/docker.sock \ -w /workspace/nexus \ + -i \ docker:cli \ - sh -c " - set -e - echo '🔙 Rolling back to ${{ inputs.target_tag }}' - docker compose --env-file /tmp-host/$(basename "${ENV_TMPFILE}") build --no-cache - docker compose --env-file /tmp-host/$(basename "${ENV_TMPFILE}") up -d --wait --force-recreate - " + sh -c ' + set -eu + umask 077 + cat > /tmp/nexus-rollback-env + trap '\''rm -f /tmp/nexus-rollback-env'\'' EXIT INT TERM + docker compose --env-file /tmp/nexus-rollback-env build --no-cache + docker compose --env-file /tmp/nexus-rollback-env up -d --wait --force-recreate + ' < "${ENV_TMPFILE}" echo "✅ Rollback redeploy completed" @@ -186,11 +210,14 @@ jobs: WAIT=1 while [ $RETRY -lt $MAX ]; do RETRY=$((RETRY + 1)) - if curl -sf --max-time 10 https://nexus.noveria.net/health; then - echo "" - echo "✅ Health check passed (attempt $RETRY/$MAX)" - exit 0 - fi + HEALTH_BODY=$(curl -sf --max-time 10 https://nexus.noveria.net/health || true) + case "$HEALTH_BODY" in + '{"status":"Healthy"'*) + echo "✅ Health check passed (attempt $RETRY/$MAX)" + exit 0 + ;; + esac + [ -n "$HEALTH_BODY" ] && echo "⚠️ Health endpoint is degraded: $HEALTH_BODY" echo "⏳ Attempt $RETRY/$MAX failed, waiting ${WAIT}s..." sleep $WAIT NEXT=$((WAIT + RETRY)) diff --git a/compose.yaml b/compose.yaml index 4656e67..5adfc29 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,6 +2,15 @@ name: nexus services: postgres: image: postgres:17-alpine + # WAL-Archivierung bleibt deaktiviert, bis ein verwaltetes Off-Server-Ziel + # mit Retention und Restore-Test existiert. Ein lokales Endlosarchiv ist + # kein Backup und kann bei Fehlern pg_wal ungebremst wachsen lassen. + command: + - postgres + - -c + - archive_mode=off + - -c + - archive_command= restart: always deploy: resources: @@ -50,7 +59,7 @@ services: Jwt__Audience: ${JWT_AUDIENCE:-nexus-web} Bootstrap__OwnerEmail: ${BOOTSTRAP_OWNER_EMAIL:?Set BOOTSTRAP_OWNER_EMAIL in .env} # Initial owner password is generated once at first seed and then lives only in the DB. - Integrations__OpenClaw__BaseUrl: ${OPENCLAW_BASE_URL:-http://host.docker.internal:18789} + Integrations__OpenClaw__BaseUrl: ${OPENCLAW_BASE_URL:-http://openclaw-gateway-bao:18789} Integrations__OpenClaw__Token: ${OPENCLAW_GATEWAY_TOKEN:-} Integrations__OpenClaw__Password: ${OPENCLAW_GATEWAY_PASSWORD:-} Admin__ResetToken: ${Admin__ResetToken:-} @@ -59,7 +68,7 @@ services: - host.docker.internal:host-gateway depends_on: postgres: - condition: service_started + condition: service_healthy restart: true healthcheck: test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health/live || exit 1"] @@ -106,7 +115,7 @@ services: - "127.0.0.1:18880:80" depends_on: api: - condition: service_started + condition: service_healthy restart: true healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:80/ || exit 1"] diff --git a/docs/architecture-board-first-orchestration.md b/docs/architecture-board-first-orchestration.md index f68f8eb..2116708 100644 --- a/docs/architecture-board-first-orchestration.md +++ b/docs/architecture-board-first-orchestration.md @@ -68,8 +68,8 @@ Ansatz. Das Backend fungiert bereits als sichere Schicht zwischen allen Akteuren │ │ │ ALLE Gateway-Calls → Authorization: Bearer │ └──────────────┬──────────────────────────────┬────────────────────┘ - │ host.docker.internal:18789 │ - │ (Gateway loopback/lan) │ + │ openclaw-gateway-bao:18789 │ + │ (internes Docker-DNS) │ ▼ │ ┌──────────────────────────────┐ │ │ OpenClaw Gateway Container │ │ @@ -199,7 +199,7 @@ Ebene 4: X-Agent-Id Header (Agent-Identität für Task-State-Enforcement) ``` POST /api/v1/operations/snapshot → DashboardService → OpenClawGatewayClient.InvokeToolAsync() - → POST http://host.docker.internal:18789/tools/invoke + → POST http://openclaw-gateway-bao:18789/tools/invoke Authorization: Bearer ``` @@ -220,7 +220,7 @@ POST /api/v1/operations/snapshot ### 5.2 Docker-Netzwerk & Gateway-Bind -**Aktuelles Problem:** +**Aktueller Stand (2026-07-09):** ``` compose.yaml: api: @@ -228,32 +228,27 @@ compose.yaml: - host.docker.internal:host-gateway networks: - nexus - - openclaw_default ← API-Container ist im Gateway-Netzwerk + - openclaw_default Gateway-Konfiguration: - gateway.bind: "loopback" ← Bindet nur 127.0.0.1 IM GATEWAY-CONTAINER + gateway.bind: "lan" + +Nexus-Konfiguration: + OPENCLAW_BASE_URL=http://openclaw-gateway-bao:18789 ``` **Ergebnis:** -- `host.docker.internal:18789` funktioniert, weil `extra_hosts` auf den Docker-Host zeigt -- ABER: Docker-Port-Forward (wenn vorhanden) sendet an Container-IP, nicht loopback -- Die `openclaw_default` Netzwerk-Mitgliedschaft des API-Containers wird NICHT genutzt +- Nexus erreicht das Gateway direkt über Docker-DNS im gemeinsamen `openclaw_default`-Netz. +- Der Umweg über einen nicht veröffentlichten Host-Port entfällt. +- Der produktive Aggregat-Healthcheck prüft neben PostgreSQL auch die Runtime-Verbindung. -**Empfehlung (siehe gateway-api-research.md, Abschnitt 6):** -```json5 -// openclaw.json -{ - gateway: { - bind: "lan" // war "loopback" - } -} -``` +Der frühere Pfad `host.docker.internal:18789` war auf dem VPS nicht erreichbar und ist obsolet. -Alternativ: API-Container über Gateway-Container-Namen ansprechen: +Produktive Einstellung: ```yaml -Integrations__OpenClaw__BaseUrl: http://openclaw_gateway:18789 +Integrations__OpenClaw__BaseUrl: http://openclaw-gateway-bao:18789 ``` -(Vorausgesetzt der Gateway-Container heißt `openclaw_gateway` und ist im `openclaw_default` Netzwerk) +Beide Container müssen Mitglied im `openclaw_default`-Netzwerk sein. ### 5.3 MCP-artige Integration: Bewertung diff --git a/docs/gateway-api-research.md b/docs/gateway-api-research.md index 0ff4293..813cbfb 100644 --- a/docs/gateway-api-research.md +++ b/docs/gateway-api-research.md @@ -290,30 +290,30 @@ The Nexus compose.yaml already includes the full integration infrastructure: ```yaml api: - extra_hosts: - - host.docker.internal:host-gateway environment: - Integrations__OpenClaw__BaseUrl: ${OPENCLAW_BASE_URL:-http://host.docker.internal:18789} + Integrations__OpenClaw__BaseUrl: ${OPENCLAW_BASE_URL:-http://openclaw-gateway-bao:18789} Integrations__OpenClaw__Token: ${OPENCLAW_GATEWAY_TOKEN:-} Integrations__OpenClaw__Password: ${OPENCLAW_GATEWAY_PASSWORD:-} + networks: + - nexus + - openclaw_default ``` The API container: -- Uses `host.docker.internal:18789` to reach the Gateway via the Docker host -- Has `extra_hosts` configured for `host.docker.internal` +- Uses Docker DNS (`openclaw-gateway-bao:18789`) in the shared `openclaw_default` network +- Does not depend on a published host port for the Gateway - Reads token/password from `.env` via `OPENCLAW_GATEWAY_PASSWORD` -### Known Issue: Gateway Bind = loopback +### Resolved Routing Issue (2026-07-09) -The Gateway binds to `127.0.0.1` (`gateway.bind: "loopback"`). This means it only listens inside the gateway container's loopback interface. +The old `host.docker.internal:18789` route was unreachable because no usable host port was published. The Gateway is now reached directly by its container DNS name. | Scenario | Works? | Why | |----------|--------|-----| -| Gateway with `--network host` | ✅ Yes | Process sees host's 127.0.0.1 directly | -| Gateway with `-p 18789:18789` + loopback bind | ❌ No | Port forward sends to container IP, not loopback | -| Gateway with `-p 18789:18789` + lan bind | ✅ Yes | Listens on all interfaces including container IP | +| `host.docker.internal:18789` | ❌ No | No reachable host listener on the VPS | +| `openclaw-gateway-bao:18789` in `openclaw_default` | ✅ Yes | Direct container-to-container routing via Docker DNS | -**Fix**: Change `gateway.bind` from `"loopback"` to `"lan"` (binds `0.0.0.0`): +The Gateway must listen on its container interface (`gateway.bind: "lan"`): ```json5 { @@ -325,8 +325,8 @@ The Gateway binds to `127.0.0.1` (`gateway.bind: "loopback"`). This means it onl **Test command (from Nexus API container):** ```bash -curl -s http://host.docker.internal:18789/health -# Expected: 200 if gateway bind is lan/container IP is reachable +curl -s http://openclaw-gateway-bao:18789/ +# Expected: HTTP 200 from inside nexus-api-1 ``` ### Required .env Vars for Nexus diff --git a/phases/runtime-routing.md b/phases/runtime-routing.md index 132dcf1..e6ce7eb 100644 --- a/phases/runtime-routing.md +++ b/phases/runtime-routing.md @@ -1,6 +1,6 @@ # Runtime und Routing -> Letzte Aktualisierung: 2026-06-16 +> Letzte Aktualisierung: 2026-07-09 ## Aktive Modelle (7 von 8 konfiguriert) @@ -23,6 +23,7 @@ - Einzige aktive Integration: `OpenClawRuntime` über `IAgentRuntime` - Model-Routing läuft zentral über OpenClaw Gateway (kein direct provider routing) -- API kommuniziert via `host.docker.internal:18789` (Gateway loopback — wird über `openclaw_default` Netzwerk gefixt) +- API kommuniziert über Docker-DNS via `http://openclaw-gateway-bao:18789` im gemeinsamen `openclaw_default`-Netzwerk. +- Der frühere Pfad `host.docker.internal:18789` ist auf dem VPS nicht erreichbar und wurde entfernt. - **Achtung:** `[AllowAnonymous]` auf `/tasks/board` und `/tasks/reset-stale` muss durch ApiKey-Auth ersetzt werden (siehe [Architektur-Review](../docs/architecture-board-first-orchestration.md#61-kritisch--allowanonymous-auf-board-endpunkten)) - Vollständige Architektur- und Sicherheitsanalyse: [architecture-board-first-orchestration.md](../docs/architecture-board-first-orchestration.md)