From 21d4ae3d2e7995c311b6cda4cd2e0fc3f9dca657 Mon Sep 17 00:00:00 2001 From: RomeoCavazza Date: Thu, 25 Jun 2026 18:16:13 +0200 Subject: [PATCH] feat(compose): add production client_web service --- .dockerignore | 2 ++ README.md | 12 +++++-- .../src-tauri/capabilities/default.json | 2 +- client-web/Dockerfile | 36 +++++++++++++++++++ client-web/next.config.mjs | 9 ++++- docker-compose.yml | 32 ++++++++++++++++- 6 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 client-web/Dockerfile diff --git a/.dockerignore b/.dockerignore index 9315c8c..fe4d0cc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,4 +4,6 @@ .git/ **/target/ **/.env +**/.env.* **/node_modules/ +**/.next/ diff --git a/README.md b/README.md index 97fd7f2..47017c1 100644 --- a/README.md +++ b/README.md @@ -115,15 +115,23 @@ cd opswarden cp .env.example .env # adjust OPSWARDEN_KICKOFF_TOKEN and DATABASE_URL if needed -# 3. Run everything (server + database) +# 3. Run everything (database + server + web UI) docker compose up --build ``` -Check the server responds: +Compose brings up `db`, the `server` on `:8080`, and the production `client_web` +on **`:8081`** (the Next.js UI, also the URL-mode target the desktop build loads). +`client_web` proxies `/api/*` to the server over the compose network; the browser +reaches the WebSocket directly on the server's published `:8080`. If host port +`8081` is already in use, run `CLIENT_WEB_PORT=8091 docker compose up --build`; +the container still listens on `:8081` internally. + +Check the services respond: ```bash curl http://localhost:8080/health # -> {"status":"ok"} curl http://localhost:8080/about.json # -> service catalog + SHA-256 token +curl http://localhost:8081/en # -> 200, the web UI (FR at /fr) ``` ### The project at a glance diff --git a/client-desktop/src-tauri/capabilities/default.json b/client-desktop/src-tauri/capabilities/default.json index dbe6d1f..69a9820 100644 --- a/client-desktop/src-tauri/capabilities/default.json +++ b/client-desktop/src-tauri/capabilities/default.json @@ -4,7 +4,7 @@ "description": "Default capability for the OpsWarden desktop shell: core APIs + native notifications on the main window.", "windows": ["main"], "remote": { - "urls": ["http://localhost:4242"] + "urls": ["http://localhost:4242", "http://localhost:8081"] }, "permissions": ["core:default", "notification:default"] } diff --git a/client-web/Dockerfile b/client-web/Dockerfile new file mode 100644 index 0000000..503c729 --- /dev/null +++ b/client-web/Dockerfile @@ -0,0 +1,36 @@ +# --- client-web/Dockerfile --- +# +# Production web surface for OpsWarden (Next.js), served with `next start` on +# :8081 — the URL-mode target the Tauri desktop build loads. Built from the +# monorepo root context (single root package-lock + npm workspaces). `/api/*` is +# proxied to the server via OPSWARDEN_API_ORIGIN (Compose: http://server:8080), +# resolved at server start; the browser reaches the WebSocket directly on the +# server's published :8080 (NEXT_PUBLIC_WS_URL, baked at build time). + +# ---- build ---- +FROM node:20-bookworm-slim AS builder +WORKDIR /app +# Manifests first so the `npm ci` layer caches on lockfile changes only. +COPY package.json package-lock.json ./ +COPY client-web/package.json ./client-web/package.json +COPY client-desktop/package.json ./client-desktop/package.json +RUN npm ci +# App source + build. Both the client `NEXT_PUBLIC_*` env and the `/api/*` rewrite +# destination are baked into the build, so they are set here as build args. +COPY client-web ./client-web +ARG NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws +ARG OPSWARDEN_API_ORIGIN=http://localhost:8080 +ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL} +ENV OPSWARDEN_API_ORIGIN=${OPSWARDEN_API_ORIGIN} +RUN npm run build --workspace client-web + +# ---- runtime ---- +FROM node:20-bookworm-slim AS runner +WORKDIR /app/client-web +ENV NODE_ENV=production +# Installed deps + the built app. next.config.mjs is read at boot, so +# OPSWARDEN_API_ORIGIN is resolved at runtime from the container env. +COPY --from=builder /app/node_modules /app/node_modules +COPY --from=builder /app/client-web /app/client-web +EXPOSE 8081 +CMD ["/app/node_modules/.bin/next", "start", "-p", "8081"] diff --git a/client-web/next.config.mjs b/client-web/next.config.mjs index bd85c4f..a00eed2 100644 --- a/client-web/next.config.mjs +++ b/client-web/next.config.mjs @@ -5,6 +5,13 @@ import { fileURLToPath } from "node:url"; const withNextIntl = createNextIntlPlugin("./i18n/request.ts"); const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +// Origin the Next server proxies `/api/*` to. Next *bakes* rewrite destinations +// into the build, so this is read at build time: the Compose image is built with +// `OPSWARDEN_API_ORIGIN=http://server:8080` (internal network), while `next dev` +// re-evaluates the config per run and falls back to the host server on :8080. +// The WebSocket is a separate, browser-direct concern (NEXT_PUBLIC_WS_URL). +const apiOrigin = process.env.OPSWARDEN_API_ORIGIN || "http://localhost:8080"; + /** @type {import('next').NextConfig} */ const nextConfig = { turbopack: { @@ -14,7 +21,7 @@ const nextConfig = { return [ { source: "/api/:path*", - destination: "http://localhost:8080/api/:path*", + destination: `${apiOrigin}/api/:path*`, }, ]; }, diff --git a/docker-compose.yml b/docker-compose.yml index 2fb230c..0dd2b90 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,9 @@ services: GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET:-} OPSWARDEN_AUTOMATION_TEAM_ID: ${OPSWARDEN_AUTOMATION_TEAM_ID:-} OPSWARDEN_AUTOMATION_NOTIFY_URL: ${OPSWARDEN_AUTOMATION_NOTIFY_URL:-} - OPSWARDEN_WEB_ORIGIN: ${OPSWARDEN_WEB_ORIGIN:-http://localhost:4242} + # The browser-facing web origin in Compose is the client_web service on + # :8081 (used for the OAuth post-login redirect). Dev runs keep :4242. + OPSWARDEN_WEB_ORIGIN: ${OPSWARDEN_WEB_ORIGIN:-http://localhost:8081} # Google OAuth (optional; blank => Google sign-in disabled). GOOGLE_OAUTH_CLIENT_ID: ${GOOGLE_OAUTH_CLIENT_ID:-} GOOGLE_OAUTH_CLIENT_SECRET: ${GOOGLE_OAUTH_CLIENT_SECRET:-} @@ -51,5 +53,33 @@ services: timeout: 3s retries: 10 + # Production web surface (Next.js) — the URL-mode target the desktop build + # loads. Serves the UI on :8081 and proxies /api/* to the server over the + # compose network; the browser hits the server's published :8080 for the WS. + client_web: + build: + context: . + dockerfile: client-web/Dockerfile + args: + # Both are baked into the Next build: the browser hits the server's + # published :8080 for the WebSocket; the Next server proxies /api/* to + # the server over the compose network. + NEXT_PUBLIC_WS_URL: ${NEXT_PUBLIC_WS_URL:-ws://localhost:8080/ws} + OPSWARDEN_API_ORIGIN: http://server:8080 + ports: + - "${CLIENT_WEB_PORT:-8081}:8081" + depends_on: + server: + condition: service_healthy + healthcheck: + test: + [ + "CMD-SHELL", + 'node -e "require(''http'').get(''http://localhost:8081/en'',r=>process.exit(r.statusCode<500?0:1)).on(''error'',()=>process.exit(1))"', + ] + interval: 10s + timeout: 5s + retries: 12 + volumes: db_data: