From fd9fa2ef69cbdf126c271954e34ea3166c67b99b Mon Sep 17 00:00:00 2001 From: BrewingCoder Date: Sat, 9 May 2026 10:19:57 -0400 Subject: [PATCH] infra+dotnet: fold frontend nginx into backend wwwroot (HOL-20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the dedicated nginx frontend container — the SPA bundle now ships inside the backend image at /app/wwwroot and Kestrel serves it via UseStaticFiles + MapFallbackToFile (so React Router still owns SPA routes). With this change the hobby stack hits its 3-container LEAN target: backend, postgres, clickhouse. The runtime URL substitution from frontend-entrypoint.py is gone too — URLs are now baked at backend image build time via REACT_APP_* build args (defaults all collapse to http://localhost:8082 since backend is the only HTTP surface). For custom-domain deployments, override the build args and rebuild. Removed: frontend.Dockerfile, frontend-entrypoint.py, nginx.conf, compose.dev-frontend.yml. README + .env.example updated. appsettings also drops the leftover Redis/Kafka sections (HOL-22/HOL-23 cleanup). Closes HOL-20. Co-Authored-By: Claude Opus 4.7 --- infra/docker/.env.example | 16 ++- infra/docker/README.md | 55 +++++----- infra/docker/backend-dotnet.Dockerfile | 85 ++++++++++++++- infra/docker/compose.dev-frontend.yml | 36 ------- infra/docker/compose.hobby-dotnet.yml | 25 ++--- infra/docker/frontend-entrypoint.py | 58 ---------- infra/docker/frontend.Dockerfile | 108 ------------------- infra/docker/nginx.conf | 41 ------- src/dotnet/src/HoldFast.Api/Program.cs | 21 ++++ src/dotnet/src/HoldFast.Api/appsettings.json | 8 +- 10 files changed, 145 insertions(+), 308 deletions(-) delete mode 100644 infra/docker/compose.dev-frontend.yml delete mode 100644 infra/docker/frontend-entrypoint.py delete mode 100644 infra/docker/frontend.Dockerfile delete mode 100644 infra/docker/nginx.conf diff --git a/infra/docker/.env.example b/infra/docker/.env.example index 944a1790..0f6fff95 100644 --- a/infra/docker/.env.example +++ b/infra/docker/.env.example @@ -13,10 +13,11 @@ COMPOSE_FILE=compose.yml COMPOSE_PATH_SEPARATOR=: # ── Container images ──────────────────────────────────────────────── -# Backend / frontend default to locally-built holdfast-* images. Leave -# unset (or set to empty) to let compose use the in-tree build. -# BACKEND_IMAGE_NAME= -# FRONTEND_IMAGE_NAME= +# Backend image defaults to a locally-built holdfast-backend-dotnet:latest. +# Leave unset (or set to empty) to let compose use the in-tree build. +# HOL-20: the frontend nginx image was retired — the SPA bundle now ships +# inside the backend image at /app/wwwroot and is served by Kestrel. +# BACKEND_DOTNET_IMAGE_NAME= # Infra dependencies — pinned for reproducibility. CLICKHOUSE_IMAGE_NAME=clickhouse/clickhouse-server:24.3.15.72-alpine @@ -45,9 +46,14 @@ PSQL_USER=postgres # Removed in HOL-22 — backend uses IMemoryCache + Postgres instead. # ── Frontend / API URLs ───────────────────────────────────────────── +# HOL-20: backend serves the SPA bundle, so all URLs collapse to :8082. +# These values are baked into the SPA bundle at backend image build time +# (see ARG REACT_APP_* in backend-dotnet.Dockerfile). To deploy behind a +# reverse proxy / custom domain, override the build args at image build +# time and rebuild the backend image — runtime substitution was dropped. REACT_APP_DISABLE_ANALYTICS=false REACT_APP_FRONTEND_ORG=1 -REACT_APP_FRONTEND_URI=http://localhost:3000 +REACT_APP_FRONTEND_URI=http://localhost:8082 REACT_APP_IN_DOCKER=true REACT_APP_OTLP_ENDPOINT=http://localhost:8082/otel REACT_APP_PRIVATE_GRAPH_URI=http://localhost:8082/private diff --git a/infra/docker/README.md b/infra/docker/README.md index 881129bc..8ffb94ba 100644 --- a/infra/docker/README.md +++ b/infra/docker/README.md @@ -1,12 +1,13 @@ # HoldFast — Local Docker Stack Quickstart for running HoldFast end-to-end on a single machine. Works on Linux, -macOS, and Windows (via Docker Desktop). The stack runs as 8 services in a -single `docker compose` project: backend, frontend, collector, -postgres, clickhouse, kafka, zookeeper, redis. +macOS, and Windows (via Docker Desktop). The hobby stack runs as **3 services** +in a single `docker compose` project: `backend`, `postgres`, `clickhouse`. -(See [HOL-17](https://yt.brewingcoder.com/issue/HOL-17) for the in-flight -work to reduce this further.) +The backend image bundles the SPA frontend, embeds an OTLP receiver, and uses +an in-process message bus, so kafka / zookeeper / redis / collector / nginx +have all been retired (see [HOL-17](https://yt.brewingcoder.com/issue/HOL-17) +and its subtasks). > Production deployments should use a Helm chart (TODO) or a managed-service > compose. This compose file is for development and demos only — secrets are @@ -15,8 +16,8 @@ work to reduce this further.) ## Prerequisites - Docker 24+ with Docker Compose v2 -- ~6 GB free RAM (kafka + clickhouse + jvm overhead) -- Ports free on the host: 3000, 5432, 6379, 8082, 8123, 8889, 9000, 9092, 4317-4319 +- ~3 GB free RAM (clickhouse is the dominant consumer; tuned by HOL-18) +- Ports free on the host: 5432 (postgres), 8082 (backend + UI), 8123 / 9000 (clickhouse) If you're on Windows, also ensure `*.sh` files in your checkout have LF line endings. The `.gitattributes` rule should handle this for fresh clones; if @@ -30,8 +31,9 @@ cp .env.example .env # copy the template — .env itself is gitignored docker compose -f compose.yml -f compose.hobby-dotnet.yml up -d ``` -The first build takes 10–15 minutes (frontend has the largest layer). Subsequent -runs reuse cached layers and complete in seconds. +The first build takes 10–15 minutes (the frontend bundle stage inside +`backend-dotnet.Dockerfile` has the largest layer). Subsequent runs reuse +cached layers and complete in seconds. ## What auto-runs on startup @@ -42,22 +44,18 @@ A handful of bootstrap services run inside the backend on first boot: idempotently. Tracks applied versions in `default.schema_migrations`. Set `ClickHouse__Migrations__Disabled=true` to skip (for environments where the schema is managed externally). -3. **`KafkaTopicBootstrapService`** — creates the topics consumers will subscribe to - (`session-events`, `backend-errors`, `frontend-errors`, `metrics`, `logs`, - `traces`). Idempotent. Disable via `Kafka__TopicBootstrap__Disabled=true`. -4. **`DevSeedService`** — creates an admin user and four demo workspaces with +3. **`DevSeedService`** — creates an admin user and four demo workspaces with one default project each. **Hobby/dev only** — production sets `DevSeed__Enabled=false`. After ~30 seconds you should see in `docker compose logs backend`: - `ClickHouse migrations: 146 applied, 146 total` -- `Kafka topic bootstrap: created N of 6 topics` - `DevSeed: complete — admin=dev@holdfast.local, workspaces=4` ## Logging in ``` -URL: http://localhost:3000 +URL: http://localhost:8082 Email: dev@holdfast.local Password: $ADMIN_PASSWORD from .env (default "password") ``` @@ -108,15 +106,10 @@ Smoke test passed. The ingest pipeline is working end-to-end. warmup. Once `Application started.` appears in the backend logs, it should stay healthy. If it crashes after, check `docker compose logs backend | grep fail`. -- **Port 8888 collision**. The collector's Prometheus self-metrics endpoint is - remapped to host port 8889 to dodge collisions with Jupyter, etc. (see - HOL-7). The container internally still uses 8888. - **`Table default.X does not exist`** in worker logs after a fresh `up`. The migration runner may have failed. Look for `ClickHouse migrations:` lines in the backend logs. If you see `Hosting failed to start`, drop the ClickHouse data volume and try again: `docker compose down -v`. -- **Kafka consumers crashing on subscribe**. Topics aren't created. Check for - `Kafka topic bootstrap:` lines in the backend logs. ## Stopping and restarting @@ -133,14 +126,13 @@ docker compose -f compose.yml -f compose.hobby-dotnet.yml up -d --force-recreate ## Compose file layering -- `compose.yml` — base infra (postgres, clickhouse, kafka, redis, etc.). - Always required. -- `compose.hobby-dotnet.yml` — adds the .NET backend, frontend, and collector - with hobby/dev defaults (DevSeed on, SSL off, plaintext password). -- `compose.hobby.yml` — adds the **legacy Go** backend instead of .NET. Pick - one or the other, not both. Default for new deployments is `hobby-dotnet`. -- `compose.dev-frontend.yml` — overlay for frontend hot-reload during local - development. See `frontend.Dockerfile` and PR #66 for context. +- `compose.yml` — base infra (postgres, clickhouse). Always required. +- `compose.hobby-dotnet.yml` — adds the .NET backend (which now bundles the + SPA + OTLP receiver) with hobby/dev defaults (DevSeed on, SSL off, plaintext + password). +- `compose.hobby.yml` — adds the **legacy Go** backend + a separate nginx + frontend container. Pick one or the other, not both. Default for new + deployments is `hobby-dotnet`. - `compose.enterprise*.yml` — production-shaped variants. Out of scope for this README. @@ -153,7 +145,10 @@ The full list is in `.env.example`. The most commonly tweaked ones: | `ADMIN_PASSWORD` | `password` | Auth password for the seeded admin user | | `DEV_SEED_ENABLED` | `true` | Auto-create demo workspaces + projects | | `CLICKHOUSE_MIGRATIONS_DISABLED` | `false` | Skip the .NET-side migration runner | -| `KAFKA_AUTO_CREATE_TOPICS_ENABLE` | `true` | Kafka broker setting (Confluent default off) | -| `REACT_APP_FRONTEND_URI` | `http://localhost:3000` | Where the frontend is reachable | +| `REACT_APP_FRONTEND_URI` | `http://localhost:8082` | Where the dashboard is reachable | | `REACT_APP_PRIVATE_GRAPH_URI` | `http://localhost:8082/private` | Dashboard GraphQL URL | | `REACT_APP_PUBLIC_GRAPH_URI` | `http://localhost:8082/public` | SDK ingest URL | + +The `REACT_APP_*` URLs are baked into the SPA at backend image build time +(see `backend-dotnet.Dockerfile`). Override the build args and rebuild to +deploy behind a custom domain. diff --git a/infra/docker/backend-dotnet.Dockerfile b/infra/docker/backend-dotnet.Dockerfile index 81bcda9c..007be4da 100644 --- a/infra/docker/backend-dotnet.Dockerfile +++ b/infra/docker/backend-dotnet.Dockerfile @@ -1,5 +1,79 @@ -# ── Build stage ──────────────────────────────────────────────────────── -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build +# ── Frontend pruner ─────────────────────────────────────────────────── +# HOL-20: the dedicated frontend nginx container was folded into the +# backend image. The frontend build stages below mirror what the now-removed +# frontend.Dockerfile did; the runtime stage at the bottom copies the +# resulting bundle into /app/wwwroot for Kestrel to serve. +FROM --platform=$BUILDPLATFORM node:lts-alpine AS frontend-pruner + +RUN apk add --no-cache libc6-compat && npm install -g turbo@^2 + +WORKDIR /app +COPY . . +RUN turbo prune @holdfast-io/frontend --docker + +# ── Frontend build ──────────────────────────────────────────────────── +FROM --platform=$BUILDPLATFORM node:lts-alpine AS frontend-build + +RUN apk update && apk add --no-cache build-base python3 + +WORKDIR /app + +COPY .yarnrc.yml . +COPY .yarn/patches ./.yarn/patches +COPY .yarn/releases ./.yarn/releases + +# Install only pruned workspace deps. rrweb/package.json is added explicitly +# because turbo prune doesn't pick it up (its devDeps are imported by +# rrweb/vite.config.default.ts which every rrweb package uses). +COPY --from=frontend-pruner /app/out/json/ . +COPY rrweb/package.json ./rrweb/package.json +# Use the full yarn.lock so --immutable works for rrweb's deps. +COPY yarn.lock ./yarn.lock + +RUN --mount=type=cache,target=/root/.yarn/berry/cache,sharing=locked \ + yarn install + +COPY --from=frontend-pruner /app/out/full/ . + +# Root config files turbo prune omits. +COPY rrweb/tsconfig.base.json ./rrweb/tsconfig.base.json +COPY rrweb/tsconfig.json ./rrweb/tsconfig.json +COPY rrweb/vite.config.default.ts ./rrweb/vite.config.default.ts +COPY rrweb/turbo.json ./rrweb/turbo.json +COPY tsconfig.json ./tsconfig.json +COPY graphql.config.js ./graphql.config.js + +# GraphQL schemas live outside the frontend workspace; needed for codegen. +COPY src/backend/localhostssl ./src/backend/localhostssl +COPY src/backend/private-graph ./src/backend/private-graph +COPY src/backend/public-graph ./src/backend/public-graph + +# Bake URLs at build time. Defaults match the lean single-port deploy where +# the backend serves both API and frontend on :8082. Override via build args +# (e.g. for production deployments behind a custom domain). The +# frontend-entrypoint.py runtime substitution from the old frontend.Dockerfile +# was dropped — for self-hosted single-tenant deploys, rebuilding on URL +# changes is straightforward and avoids the runtime mutation step. +ARG NODE_OPTIONS="--max-old-space-size=8192" +ARG REACT_APP_AUTH_MODE=Password +ARG REACT_APP_FRONTEND_URI=http://localhost:8082 +ARG REACT_APP_PRIVATE_GRAPH_URI=http://localhost:8082/private +ARG REACT_APP_PUBLIC_GRAPH_URI=http://localhost:8082/public +ARG REACT_APP_OTLP_ENDPOINT=http://localhost:8082/otel +ARG REACT_APP_IN_DOCKER=true + +ENV REACT_APP_AUTH_MODE=$REACT_APP_AUTH_MODE +ENV REACT_APP_FRONTEND_URI=$REACT_APP_FRONTEND_URI +ENV REACT_APP_PRIVATE_GRAPH_URI=$REACT_APP_PRIVATE_GRAPH_URI +ENV REACT_APP_PUBLIC_GRAPH_URI=$REACT_APP_PUBLIC_GRAPH_URI +ENV REACT_APP_OTLP_ENDPOINT=$REACT_APP_OTLP_ENDPOINT +ENV REACT_APP_IN_DOCKER=$REACT_APP_IN_DOCKER + +RUN --mount=type=cache,target=/root/.turbo,sharing=locked \ + TURBO_CACHE_DIR=/root/.turbo npx turbo run build:fast --filter=@holdfast-io/frontend... + +# ── Backend build ───────────────────────────────────────────────────── +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS backend-build WORKDIR /src @@ -32,13 +106,16 @@ ARG REACT_APP_COMMIT_SHA ENV REACT_APP_COMMIT_SHA=$REACT_APP_COMMIT_SHA LABEL org.opencontainers.image.source=https://github.com/BrewingCoder/holdfast -LABEL org.opencontainers.image.description="HoldFast .NET Backend" +LABEL org.opencontainers.image.description="HoldFast .NET Backend (with frontend bundle)" LABEL org.opencontainers.image.licenses="AGPL-3.0" RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* WORKDIR /app -COPY --from=build /app . +COPY --from=backend-build /app . + +# Frontend SPA bundle — served by Kestrel via UseStaticFiles + MapFallbackToFile. +COPY --from=frontend-build /app/src/frontend/build /app/wwwroot # ClickHouse migration files — applied at startup by ClickHouseMigrationService. # Disable via ClickHouse__Migrations__Disabled=true when the schema is managed diff --git a/infra/docker/compose.dev-frontend.yml b/infra/docker/compose.dev-frontend.yml deleted file mode 100644 index 5d425aa0..00000000 --- a/infra/docker/compose.dev-frontend.yml +++ /dev/null @@ -1,36 +0,0 @@ -# Dev overlay: serve a locally-built frontend via nginx instead of rebuilding the image. -# -# Usage (from infra/docker/): -# 1. Build locally: -# cd ../../src/frontend && yarn build:fast -# 2. Start with overlay: -# docker compose -f compose.yml -f compose.hobby-dotnet.yml -f compose.dev-frontend.yml up -d frontend -# -# After any code change: re-run `yarn build:fast`, then refresh the browser. -# No Docker rebuild needed — turnaround goes from ~10 min to ~60s. -# -# Note: frontend-entrypoint.py writes env var substitutions into the mounted -# build directory at container start. Re-run `yarn build:fast` to reset it. - -services: - frontend: - image: nginx:stable-alpine - build: !reset null - restart: on-failure - ports: - - '0.0.0.0:3000:3000' - - '0.0.0.0:8080:8080' - volumes: - - ../../src/frontend/build:/build/frontend/build - - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro - - ../../src/backend/localhostssl/server.key:/etc/ssl/private/ssl-cert.key:ro - - ../../src/backend/localhostssl/server.pem:/etc/ssl/certs/ssl-cert.pem:ro - - ./frontend-entrypoint.py:/frontend-entrypoint.py:ro - environment: - - SSL=false - - REACT_APP_AUTH_MODE=${REACT_APP_AUTH_MODE:-Password} - - REACT_APP_FRONTEND_URI=${REACT_APP_FRONTEND_URI:-http://localhost:3000} - - REACT_APP_PRIVATE_GRAPH_URI=${REACT_APP_PRIVATE_GRAPH_URI:-http://localhost:8082/private} - - REACT_APP_PUBLIC_GRAPH_URI=${REACT_APP_PUBLIC_GRAPH_URI:-http://localhost:8082/public} - - REACT_APP_OTLP_ENDPOINT=${REACT_APP_OTLP_ENDPOINT:-http://localhost:4318} - command: ["python3", "/frontend-entrypoint.py"] diff --git a/infra/docker/compose.hobby-dotnet.yml b/infra/docker/compose.hobby-dotnet.yml index e01e89dc..5105fa14 100644 --- a/infra/docker/compose.hobby-dotnet.yml +++ b/infra/docker/compose.hobby-dotnet.yml @@ -23,7 +23,7 @@ services: - ClickHouse__Migrations__Disabled=${CLICKHOUSE_MIGRATIONS_DISABLED:-false} - Storage__Type=filesystem - Storage__FilesystemRoot=/highlight-data - - Frontend__Uri=${REACT_APP_FRONTEND_URI:-http://localhost:3000} + - Frontend__Uri=${REACT_APP_FRONTEND_URI:-http://localhost:8082} - Auth__Mode=${REACT_APP_AUTH_MODE:-Password} - Auth__AdminPassword=${ADMIN_PASSWORD:-} - DevSeed__Enabled=${DEV_SEED_ENABLED:-true} @@ -40,24 +40,11 @@ services: # Collector removed in HOL-21 — backend has its own OTLP receiver at # /otel/v1/{logs,traces,metrics}. SDKs should target the backend's # OTLP endpoint (default http://backend:8082/otel). - - frontend: - container_name: frontend - build: - context: ../.. - dockerfile: infra/docker/frontend.Dockerfile - image: ${FRONTEND_IMAGE_NAME:-holdfast-frontend:latest} - restart: on-failure - ports: - - '0.0.0.0:3000:3000' - - '0.0.0.0:8080:8080' - environment: - - SSL=false - - REACT_APP_AUTH_MODE=${REACT_APP_AUTH_MODE:-Password} - - REACT_APP_FRONTEND_URI=${REACT_APP_FRONTEND_URI:-http://localhost:3000} - - REACT_APP_PRIVATE_GRAPH_URI=${REACT_APP_PRIVATE_GRAPH_URI:-http://localhost:8082/private} - - REACT_APP_PUBLIC_GRAPH_URI=${REACT_APP_PUBLIC_GRAPH_URI:-http://localhost:8082/public} - - REACT_APP_OTLP_ENDPOINT=${REACT_APP_OTLP_ENDPOINT:-http://localhost:8082/otel} + # + # HOL-20: dedicated frontend nginx container removed. The backend image + # now bundles the SPA in /app/wwwroot and serves it via Kestrel on the + # same port (:8082). Frontend URLs are baked at backend image build time + # — set REACT_APP_* build args on the backend service to customize. volumes: highlight-data: diff --git a/infra/docker/frontend-entrypoint.py b/infra/docker/frontend-entrypoint.py deleted file mode 100644 index 68628c6c..00000000 --- a/infra/docker/frontend-entrypoint.py +++ /dev/null @@ -1,58 +0,0 @@ -import os -import re -import subprocess - -CONSTANTS_FILE = "/build/frontend/build/assets/constants.js" -NGINX_CONFIG_FILE = "/etc/nginx/conf.d/default.conf" - - -def main(): - # Keys are env var names; values are the placeholder strings baked into the - # build by frontend.Dockerfile. At runtime the entrypoint replaces each - # placeholder with the real value from the environment (if set). - envs = { - "REACT_APP_PRIVATE_GRAPH_URI": 'https://pri.highlight.io', - "REACT_APP_PUBLIC_GRAPH_URI": 'https://pub.highlight.run', - "REACT_APP_FRONTEND_URI": 'https://app.highlight.io', - "REACT_APP_AUTH_MODE": 'firebase', - "REACT_APP_OTLP_ENDPOINT": 'http://localhost:4318', - } - use_ssl = os.environ.get("SSL") != "false" - - with open(CONSTANTS_FILE, "r") as f: - data = f.read() - print("read constants file", data, flush=True) - - for key, default in envs.items(): - env = os.environ.get(key) - if env: - print( - "replacing", - {key: key, "default": default, "value": env}, - flush=True, - ) - data = re.sub(re.escape(default), env, data) - try: - with open(CONSTANTS_FILE, "w") as f: - f.write(data) - print("wrote back constants file", flush=True) - except Exception as e: - print("failed to write back constants file", e, flush=True) - - with open(NGINX_CONFIG_FILE, "r") as f: - data = f.read() - if not use_ssl: - data = re.sub("ssl http2 ", "", data, flags=re.MULTILINE) - - try: - with open(NGINX_CONFIG_FILE, "w") as f: - f.write(data) - print("wrote back nginx file", flush=True) - except Exception as e: - print("failed to write back nginx file", e, flush=True) - - return subprocess.check_call(["nginx", "-g", "daemon off;"]) - - -if __name__ == "__main__": - main() diff --git a/infra/docker/frontend.Dockerfile b/infra/docker/frontend.Dockerfile deleted file mode 100644 index 1f7f06f0..00000000 --- a/infra/docker/frontend.Dockerfile +++ /dev/null @@ -1,108 +0,0 @@ -FROM --platform=$BUILDPLATFORM node:lts-alpine AS pruner - -RUN apk add --no-cache libc6-compat && npm install -g turbo@^2 - -WORKDIR /app -COPY . . -RUN turbo prune @holdfast-io/frontend --docker - -# ── Dependency installation layer ──────────────────────────────────────────── -# Uses the pruned package manifest (12 packages vs 87 in the full monorepo) -# to drastically reduce yarn install time. -FROM --platform=$BUILDPLATFORM node:lts-alpine AS frontend-build - -RUN apk update && apk add --no-cache build-base python3 - -WORKDIR /app - -COPY .yarnrc.yml . -COPY .yarn/patches ./.yarn/patches -COPY .yarn/releases ./.yarn/releases - -# Install only pruned workspace deps. -# rrweb/package.json (@rrweb/_monorepo) is not included by turbo prune because -# no pruned package explicitly depends on it, but its devDeps -# (esbuild-plugin-umd-wrapper, rollup-plugin-visualizer, vite-plugin-dts) are -# imported by rrweb/vite.config.default.ts which every rrweb package uses. -COPY --from=pruner /app/out/json/ . -COPY rrweb/package.json ./rrweb/package.json -# Use the full yarn.lock (not the pruned subset) so --immutable works when -# rrweb/package.json brings in deps not covered by the pruned lockfile. -COPY yarn.lock ./yarn.lock - -RUN --mount=type=cache,target=/root/.yarn/berry/cache,sharing=locked \ - yarn install - -# ── Source copy ─────────────────────────────────────────────────────────────── -COPY --from=pruner /app/out/full/ . - -# turbo prune omits rrweb root files that all rrweb/packages/*/ depend on. -# Copy them explicitly from the build context. -# tsconfig.base.json — extended by every rrweb package tsconfig.json -# tsconfig.json — rrweb composite project references root -# vite.config.default.ts — imported by every rrweb package vite.config -# turbo.json — defines the prepublish task (with ^prepublish), extends // -COPY rrweb/tsconfig.base.json ./rrweb/tsconfig.base.json -COPY rrweb/tsconfig.json ./rrweb/tsconfig.json -COPY rrweb/vite.config.default.ts ./rrweb/vite.config.default.ts -COPY rrweb/turbo.json ./rrweb/turbo.json - -# Root config files not included in turbo prune full/ output -COPY tsconfig.json ./tsconfig.json -COPY graphql.config.js ./graphql.config.js - -# GraphQL schemas are outside the frontend workspace; copy them for codegen/typegen -COPY src/backend/localhostssl ./src/backend/localhostssl -COPY src/backend/private-graph ./src/backend/private-graph -COPY src/backend/public-graph ./src/backend/public-graph - -# ── Build ──────────────────────────────────────────────────────────────────── -# Bake in the same placeholder URLs the entrypoint knows to replace at runtime. -# REACT_APP_AUTH_MODE=firebase matches the entrypoint's replacement target. -# All other URLs match the upstream defaults the entrypoint expects to find. -ARG NODE_OPTIONS="--max-old-space-size=8192" -ARG REACT_APP_AUTH_MODE=firebase -ARG REACT_APP_FRONTEND_URI=https://app.highlight.io -ARG REACT_APP_PRIVATE_GRAPH_URI=https://pri.highlight.io -ARG REACT_APP_PUBLIC_GRAPH_URI=https://pub.highlight.run -ARG REACT_APP_OTLP_ENDPOINT=http://localhost:4318 -ARG REACT_APP_IN_DOCKER=true - -ENV REACT_APP_AUTH_MODE=$REACT_APP_AUTH_MODE -ENV REACT_APP_FRONTEND_URI=$REACT_APP_FRONTEND_URI -ENV REACT_APP_PRIVATE_GRAPH_URI=$REACT_APP_PRIVATE_GRAPH_URI -ENV REACT_APP_PUBLIC_GRAPH_URI=$REACT_APP_PUBLIC_GRAPH_URI -ENV REACT_APP_OTLP_ENDPOINT=$REACT_APP_OTLP_ENDPOINT -ENV REACT_APP_IN_DOCKER=$REACT_APP_IN_DOCKER - -# build:fast skips tsc for the frontend; workspace deps still build normally -# via turbo's ^build dependency. Type checking is enforced in CI via `build`. -RUN --mount=type=cache,target=/root/.turbo,sharing=locked \ - TURBO_CACHE_DIR=/root/.turbo npx turbo run build:fast --filter=@holdfast-io/frontend... - -# ── Runtime image ───────────────────────────────────────────────────────────── -FROM nginx:stable-alpine AS frontend-prod - -RUN apk update && apk add --no-cache python3 - -LABEL org.opencontainers.image.source=https://github.com/BrewingCoder/holdfast -LABEL org.opencontainers.image.description="HoldFast Frontend Image" -LABEL org.opencontainers.image.licenses="AGPL-3.0" - -COPY infra/docker/nginx.conf /etc/nginx/conf.d/default.conf -COPY src/backend/localhostssl/server.key /etc/ssl/private/ssl-cert.key -COPY src/backend/localhostssl/server.pem /etc/ssl/certs/ssl-cert.pem -COPY infra/docker/frontend-entrypoint.py /frontend-entrypoint.py - -WORKDIR /build -COPY --from=frontend-build /app/src/frontend/build ./frontend/build - -# Runtime env vars — replaced in constants.js by entrypoint.py at startup -ENV REACT_APP_AUTH_MODE=firebase -ENV REACT_APP_FRONTEND_URI=https://app.highlight.io -ENV REACT_APP_PRIVATE_GRAPH_URI=https://pri.highlight.io -ENV REACT_APP_PUBLIC_GRAPH_URI=https://pub.highlight.run -ENV REACT_APP_OTLP_ENDPOINT=http://localhost:4318 -ENV SSL=false - -CMD ["python3", "/frontend-entrypoint.py"] diff --git a/infra/docker/nginx.conf b/infra/docker/nginx.conf deleted file mode 100644 index ef45f42a..00000000 --- a/infra/docker/nginx.conf +++ /dev/null @@ -1,41 +0,0 @@ -# inspired by https://gkedge.gitbooks.io/react-router-in-the-real/content/nginx.html - -server { - listen 3000 ssl http2 default_server; - listen [::]:3000 ssl http2 default_server; - - ssl_certificate /etc/ssl/certs/ssl-cert.pem; - ssl_certificate_key /etc/ssl/private/ssl-cert.key; - error_page 497 301 =307 https://$host:$server_port$request_uri; - - root /build/frontend/build; - index index.html; - - location ~* \.(?:manifest|appcache|html?|xml|json)$ { - expires -1; - # access_log logs/static.log; # I don't usually include a static log - } - - location ~* \.(?:css|js)$ { - try_files $uri =404; - expires 1y; - access_log off; - add_header Cache-Control "public"; - } - - # Any route containing a file extension (e.g. /devicesfile.js) - location ~ ^.+\..+$ { - try_files $uri =404; - } - - # Any route that doesn't have a file extension (e.g. /devices) - location / { - try_files $uri $uri/ /index.html; - - proxy_pass_header Server; - proxy_set_header Host $http_host; - proxy_redirect off; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Protocol $scheme; - } -} diff --git a/src/dotnet/src/HoldFast.Api/Program.cs b/src/dotnet/src/HoldFast.Api/Program.cs index 4e77e747..cbffe36e 100644 --- a/src/dotnet/src/HoldFast.Api/Program.cs +++ b/src/dotnet/src/HoldFast.Api/Program.cs @@ -289,6 +289,19 @@ req.RequestUri is null || app.MapHealthChecks("/health"); app.UseCors(); // Must be before UseMiddleware and MapGraphQL +// HOL-20: serve the SPA frontend bundle from wwwroot. The dedicated nginx +// frontend container was removed in favor of letting Kestrel handle static +// files. UseDefaultFiles maps "/" → "/index.html"; UseStaticFiles handles +// /assets/*, /static/*, etc. The MapFallbackToFile call further down catches +// SPA routes (e.g. /sessions/123) so the SPA's own router can resolve them. +// Backed by /app/wwwroot inside the container; the directory is missing in +// dev runs from `dotnet run`, so guard the registration. +if (Directory.Exists(System.IO.Path.Combine(app.Environment.ContentRootPath, "wwwroot"))) +{ + app.UseDefaultFiles(); + app.UseStaticFiles(); +} + // Only register auth middleware and endpoints for graph modes if (runtime.IsPrivateGraph() || runtime.IsPublicGraph()) { @@ -315,4 +328,12 @@ req.RequestUri is null || app.MapOtelEndpoints(); } +// HOL-20: SPA fallback — any unmatched GET hits the SPA index, letting +// React Router handle client-side routes. Registered last so it doesn't +// shadow /health, /private, /public, /otel/*, /api/auth/*, etc. +if (Directory.Exists(System.IO.Path.Combine(app.Environment.ContentRootPath, "wwwroot"))) +{ + app.MapFallbackToFile("index.html"); +} + app.Run(); diff --git a/src/dotnet/src/HoldFast.Api/appsettings.json b/src/dotnet/src/HoldFast.Api/appsettings.json index 81b7cbfa..b528c17a 100644 --- a/src/dotnet/src/HoldFast.Api/appsettings.json +++ b/src/dotnet/src/HoldFast.Api/appsettings.json @@ -19,12 +19,6 @@ "ReadonlyUsername": "default", "ReadonlyPassword": "" }, - "Redis": { - "Configuration": "localhost:6379" - }, - "Kafka": { - "BootstrapServers": "localhost:9092" - }, "Storage": { "Type": "filesystem", "FilesystemRoot": "/tmp/holdfast-storage" @@ -35,6 +29,6 @@ "AdminPassword": null }, "Frontend": { - "Uri": "http://localhost:3000" + "Uri": "http://localhost:8082" } }