From 1e3b90723635677a1b6207b764a841da79176296 Mon Sep 17 00:00:00 2001 From: hwcopeland Date: Thu, 26 Feb 2026 09:24:00 -0600 Subject: [PATCH 1/2] feat(docker): add container support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Multi-stage Dockerfile: node:20-alpine builder + nginx/node runner - Builder compiles sebuf RPC handlers (api/[domain]/v1/[rpc].ts → .js) so the local API sidecar can discover and load them at runtime - Runner stage pinned to linux/amd64 to avoid QEMU during npm build - docker/nginx.conf: serves Vite SPA, proxies /api/* to Node sidecar - docker/entrypoint.sh: starts local-api-server.mjs sidecar then nginx - deploy/k8s/worldmonitor.yaml: Deployment + Service + HTTPRoute + Secret template with generic placeholders Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 106 +++++++++++++++++++++++++++ deploy/k8s/worldmonitor.yaml | 135 +++++++++++++++++++++++++++++++++++ docker/entrypoint.sh | 26 +++++++ docker/nginx.conf | 42 +++++++++++ 4 files changed, 309 insertions(+) create mode 100644 Dockerfile create mode 100644 deploy/k8s/worldmonitor.yaml create mode 100644 docker/entrypoint.sh create mode 100644 docker/nginx.conf diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..7ec4bbbb4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,106 @@ +# ============================================================ +# World Monitor — Docker Build +# https://github.com/koala73/worldmonitor +# +# Architecture: +# Stage 1 (builder) — installs deps and builds the Vite frontend +# Stage 2 (runner) — serves the static build with nginx, +# plus a Node.js sidecar that mirrors the +# 60+ Vercel Edge Functions locally +# ============================================================ + +# ── Stage 1: Build ────────────────────────────────────────── +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install dependencies first (better layer caching) +COPY package*.json ./ +RUN npm ci --ignore-scripts + +# Copy source +COPY . . + +# Build-time environment variables. +# Override these at build time with --build-arg: +# docker build --build-arg VITE_VARIANT=world . +# +# VITE_VARIANT options: world | tech | finance | happy +ARG VITE_VARIANT=world +ARG VITE_MAPTILER_KEY="" +ARG VITE_MAPBOX_TOKEN="" +ARG VITE_POSTHOG_KEY="" + +ENV VITE_VARIANT=${VITE_VARIANT} +ENV VITE_MAPTILER_KEY=${VITE_MAPTILER_KEY} +ENV VITE_MAPBOX_TOKEN=${VITE_MAPBOX_TOKEN} +ENV VITE_POSTHOG_KEY=${VITE_POSTHOG_KEY} + +# Compile the sebuf RPC handlers (api/[domain]/v1/[rpc].ts → .js) +# Required so the local API sidecar can dynamically load them at runtime +RUN npm run build:sidecar-sebuf + +# Build the Vite SPA +RUN npm run build + +# ── Stage 2: Runtime ───────────────────────────────────────── +# Pin to amd64 so the image runs on x86_64 cluster nodes regardless of +# the build host platform (avoids QEMU emulation for the npm build stage). +FROM --platform=linux/amd64 node:20-alpine AS runner + +WORKDIR /app + +# Install nginx to serve the static frontend +RUN apk add --no-cache nginx + +# Copy the built frontend +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy the API handlers and their dependencies +COPY --from=builder /app/api ./api +COPY --from=builder /app/server ./server +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json + +# Copy the local API sidecar (repurposed from Tauri desktop sidecar) +COPY --from=builder /app/src-tauri/sidecar/local-api-server.mjs ./sidecar/local-api-server.mjs + +# Copy nginx config +COPY docker/nginx.conf /etc/nginx/http.d/default.conf + +# Copy the entrypoint script +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Runtime secrets — pass these via `docker run -e` or a .env file. +# The dashboard works without most keys; missing panels simply won't appear. +# See .env.example in the repo for full descriptions and registration links. +ENV NODE_ENV=production +# Port the Node.js API sidecar listens on (must match nginx proxy_pass upstream) +ENV LOCAL_API_PORT=3001 + +# Runtime API keys (all optional — dashboard degrades gracefully) +# See .env.example for full descriptions and registration links. +ENV GROQ_API_KEY="" +ENV OPENROUTER_API_KEY="" +ENV UPSTASH_REDIS_REST_URL="" +ENV UPSTASH_REDIS_REST_TOKEN="" +ENV FINNHUB_API_KEY="" +ENV EIA_API_KEY="" +ENV FRED_API_KEY="" +ENV WINGBITS_API_KEY="" +ENV ACLED_ACCESS_TOKEN="" +ENV CLOUDFLARE_API_TOKEN="" +ENV NASA_FIRMS_API_KEY="" +ENV AISSTREAM_API_KEY="" +ENV OPENSKY_CLIENT_ID="" +ENV OPENSKY_CLIENT_SECRET="" +ENV WS_RELAY_URL="" +ENV RELAY_SHARED_SECRET="" + +# Expose ports: +# 80 — nginx (frontend + /api proxy) +# 3001 — Node.js API sidecar (internal; proxied by nginx) +EXPOSE 80 3001 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/deploy/k8s/worldmonitor.yaml b/deploy/k8s/worldmonitor.yaml new file mode 100644 index 000000000..b24071586 --- /dev/null +++ b/deploy/k8s/worldmonitor.yaml @@ -0,0 +1,135 @@ +# ============================================================ +# World Monitor — Kubernetes Manifests (example / template) +# +# Fill in all values for your cluster before applying. +# +# Deploy: +# kubectl apply -f deploy/k8s/worldmonitor.yaml +# +# Build & push image first: +# docker buildx build --provenance=false --sbom=false \ +# -t /worldmonitor:latest --load . +# skopeo copy \ +# 'docker-daemon:/worldmonitor:latest' \ +# 'docker:///worldmonitor:latest' +# ============================================================ +--- +# Secret — API keys injected at runtime. +# Populate values before applying, or replace with an ExternalSecret/SealedSecret. +# All keys are optional; missing ones degrade specific panels gracefully. +# See .env.example for full descriptions and registration links. +apiVersion: v1 +kind: Secret +metadata: + name: worldmonitor-env + namespace: +type: Opaque +stringData: + GROQ_API_KEY: "" + OPENROUTER_API_KEY: "" + UPSTASH_REDIS_REST_URL: "" + UPSTASH_REDIS_REST_TOKEN: "" + FINNHUB_API_KEY: "" + EIA_API_KEY: "" + FRED_API_KEY: "" + WINGBITS_API_KEY: "" + ACLED_ACCESS_TOKEN: "" + CLOUDFLARE_API_TOKEN: "" + NASA_FIRMS_API_KEY: "" + AISSTREAM_API_KEY: "" + OPENSKY_CLIENT_ID: "" + OPENSKY_CLIENT_SECRET: "" + WS_RELAY_URL: "" + RELAY_SHARED_SECRET: "" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: worldmonitor + namespace: + labels: + app: worldmonitor +spec: + replicas: 1 + selector: + matchLabels: + app: worldmonitor + template: + metadata: + labels: + app: worldmonitor + spec: + containers: + - name: worldmonitor + image: /worldmonitor:latest + imagePullPolicy: Always + ports: + - name: http + containerPort: 80 + protocol: TCP + env: + - name: NODE_ENV + value: "production" + - name: LOCAL_API_PORT + value: "3001" + # Site variant: world | tech | finance | happy + - name: VITE_VARIANT + value: "world" + envFrom: + - secretRef: + name: worldmonitor-env + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 15 + periodSeconds: 30 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + # Remove if your registry is public or credentials are handled differently + imagePullSecrets: + - name: +--- +apiVersion: v1 +kind: Service +metadata: + name: worldmonitor + namespace: +spec: + selector: + app: worldmonitor + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP +--- +# HTTPRoute — requires Gateway API CRDs and a compatible gateway controller. +# Remove or replace with an Ingress resource if not using Gateway API. +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: worldmonitor + namespace: +spec: + parentRefs: + - name: + namespace: + sectionName: https + hostnames: + - "" + rules: + - backendRefs: + - name: worldmonitor + port: 80 diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 000000000..0d407ae7d --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# World Monitor — container entrypoint +# Starts the Node.js API sidecar, then nginx in the foreground. + +set -e + +API_PORT="${LOCAL_API_PORT:-3001}" + +echo "==> Starting World Monitor API sidecar on port ${API_PORT}..." + +# The API sidecar: prefer the repurposed Tauri local-api-server (primary), +# then fall back to a compiled server/index.js (legacy), then warn. +if [ -f /app/sidecar/local-api-server.mjs ]; then + LOCAL_API_PORT="${API_PORT}" node /app/sidecar/local-api-server.mjs & +elif [ -f /app/server/index.js ]; then + node /app/server/index.js & +elif [ -f /app/server/index.ts ]; then + # Dev image fallback only — ts-node is not installed in production images + npx ts-node /app/server/index.ts & +else + echo " [warn] No API sidecar found — API calls will fall back to worldmonitor.app (cloud)." +fi + +echo "==> Starting nginx..." +# Remove default PID file location issue on alpine +nginx -g "daemon off;" diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 000000000..c056dabec --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,42 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; + gzip_min_length 1000; + + # Cache static assets with content hashes (Vite output) + location ~* \.(js|css|woff2?|ttf|otf|svg|png|jpg|webp|ico|wasm)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # Proxy /api/* to the Node.js sidecar + location /api/ { + proxy_pass http://127.0.0.1:3001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Connection ""; + proxy_read_timeout 30s; + } + + # Proxy /ingest (PostHog analytics relay) + location /ingest/ { + proxy_pass https://us.i.posthog.com/; + proxy_http_version 1.1; + proxy_set_header Host us.i.posthog.com; + proxy_ssl_server_name on; + } + + # SPA fallback — all other routes serve index.html + location / { + try_files $uri $uri/ /index.html; + } +} From 624171d7d483684b3143b3baf6c32a0d6dae745a Mon Sep 17 00:00:00 2001 From: Hampton Copeland <57115868+hwcopeland@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:00:42 -0600 Subject: [PATCH 2/2] Update deploy/k8s/worldmonitor.yaml API readiness probe Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/k8s/worldmonitor.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/k8s/worldmonitor.yaml b/deploy/k8s/worldmonitor.yaml index b24071586..079f45160 100644 --- a/deploy/k8s/worldmonitor.yaml +++ b/deploy/k8s/worldmonitor.yaml @@ -87,13 +87,13 @@ spec: cpu: "500m" livenessProbe: httpGet: - path: / + path: /api/local-service-status port: http initialDelaySeconds: 15 periodSeconds: 30 readinessProbe: httpGet: - path: / + path: /api/local-service-status port: http initialDelaySeconds: 5 periodSeconds: 10