diff --git a/.changeset/feat-subpath-config.md b/.changeset/feat-subpath-config.md new file mode 100644 index 000000000..18d179c2b --- /dev/null +++ b/.changeset/feat-subpath-config.md @@ -0,0 +1,9 @@ +--- +'@hyperdx/app': minor +--- + +feat: Add subpath configuration support + +This change allows the HyperDX frontend to be served from a subpath (e.g., +`/hyperdx`). It includes updated Next.js, NGINX, and Traefik configurations, +along with documentation for the new setup. diff --git a/.env b/.env index 685e5cb85..c8f25be9e 100644 --- a/.env +++ b/.env @@ -20,6 +20,7 @@ HYPERDX_APP_PORT=8080 HYPERDX_APP_URL=http://localhost HYPERDX_LOG_LEVEL=debug HYPERDX_OPAMP_PORT=4320 +HYPERDX_BASE_PATH= # Otel/Clickhouse config HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE=default diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7cf0381a5..d376d4a24 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -83,5 +83,29 @@ services: interval: 1s timeout: 1s retries: 60 + + # nginx: + # image: nginx:alpine + # ports: + # - '4040:4040' + # volumes: + # - ./proxy/nginx/nginx.conf.template:/etc/nginx/templates/default.conf.template:ro + # environment: + # HYPERDX_BASE_PATH: ${HYPERDX_BASE_PATH:-/} + # network_mode: host + # restart: always + + # traefik: + # image: traefik:latest + # ports: + # - '4040:4040' + # volumes: + # - ./proxy/traefik/traefik.yml:/etc/traefik/traefik.yml:ro + # - ./proxy/traefik/config.yml:/etc/traefik/dynamic/config.yml:ro + # environment: + # HYPERDX_BASE_PATH: ${HYPERDX_BASE_PATH:-/} + # network_mode: host + # restart: always + networks: internal: diff --git a/packages/app/.env.development b/packages/app/.env.development index 15c5e343e..0c63da39c 100644 --- a/packages/app/.env.development +++ b/packages/app/.env.development @@ -7,3 +7,4 @@ OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" OTEL_SERVICE_NAME="hdx-oss-dev-app" PORT=${HYPERDX_APP_PORT} NODE_OPTIONS="--max-http-header-size=131072" +NEXT_PUBLIC_HYPERDX_BASE_PATH= \ No newline at end of file diff --git a/packages/app/next.config.js b/packages/app/next.config.js index f4b04a4d5..ee8a943a8 100644 --- a/packages/app/next.config.js +++ b/packages/app/next.config.js @@ -8,7 +8,10 @@ const withNextra = require('nextra')({ themeConfig: './src/nextra.config.tsx', }); +const basePath = process.env.NEXT_PUBLIC_HYPERDX_BASE_PATH; + module.exports = { + basePath: basePath, experimental: { instrumentationHook: true, // External packages to prevent bundling issues with Next.js 14 @@ -57,8 +60,8 @@ module.exports = { productionBrowserSourceMaps: false, ...(process.env.NEXT_OUTPUT_STANDALONE === 'true' ? { - output: 'standalone', - } + output: 'standalone', + } : {}), }), }; diff --git a/proxy/README.md b/proxy/README.md new file mode 100644 index 000000000..04874530e --- /dev/null +++ b/proxy/README.md @@ -0,0 +1,74 @@ +# Proxy Configuration + +This directory contains configurations for running the HyperDX frontend behind a +reverse proxy that serves the application under a specific subpath. This is +useful for deployments where HyperDX is not at the root of a domain (e.g., +`http://example.com/hyperdx`). + +We provide configurations for two popular reverse proxies: + +- [Nginx](./nginx/nginx.conf.template) +- [Traefik](./traefik/config.yml) + +## Environment Variables + +To configure the subpath, you need to set the following environment variables in +your `.env` file. + +### `HYPERDX_BASE_PATH` and `NEXT_PUBLIC_HYPERDX_BASE_PATH` + +To serve the application from a subpath, two environment variables must be set +to the **same value**: + +1. `HYPERDX_BASE_PATH`: This is used by the reverse proxy (Nginx or Traefik) to + handle path routing and rewriting. +2. `NEXT_PUBLIC_HYPERDX_BASE_PATH`: This is used by the Next.js application to + generate correct asset links and API routes. + +- The value **must** start with a `/` if it's not an empty string (ex: + `/hyperdx`). +- If you want to serve from the root, you can omit these variables or set them + to `/`. + +### `FRONTEND_URL` + +This variable should be set to the full public URL of the frontend, including +the subpath. The API server uses this URL for various purposes such as +generating absolute URLs for redirects, links in emails, or alerts. + +- It should be a full URL, including the protocol (`http` or `https`). +- It should include the subpath defined in `HYPERDX_BASE_PATH`. + +**Example `.env` Configuration:** + +For local development with the subpath `/hyperdx`, your configuration would look +like this: + +``` +HYPERDX_BASE_PATH=/hyperdx +NEXT_PUBLIC_HYPERDX_BASE_PATH=/hyperdx +FRONTEND_URL=http://localhost:4040/hyperdx +``` + +## How It Works + +The proxy configurations are designed to handle subpath routing with minimal +changes to the application code. Here's a high-level overview of the logic: + +1. **Root Redirect**: If a subpath is configured (e.g., `/hyperdx`), any + requests to the root (`/`) are automatically redirected to that subpath. + This ensures users always land on the correct URL. + +2. **Path Rewriting**: The application's frontend code sometimes makes requests + to root-level paths (e.g., `/api/...` or `/_next/...`). The proxy intercepts + these requests, prepends the configured subpath, and forwards them to the + Next.js server. For example, a request for `/_next/static/chunk.js` becomes + a request for `/hyperdx/_next/static/chunk.js` before being sent to the + application. + +3. **Direct Proxy**: Any requests that already include the correct subpath are + passed directly to the Next.js application, which is configured via + `basePath` to handle them correctly. + +This setup allows the frontend application to be developed as if it were running +at the root, while the proxy transparently manages the subpath routing. diff --git a/proxy/nginx/nginx.conf.template b/proxy/nginx/nginx.conf.template new file mode 100644 index 000000000..34749b35f --- /dev/null +++ b/proxy/nginx/nginx.conf.template @@ -0,0 +1,43 @@ +upstream app { + server 127.0.0.1:8080; +} + +server { + listen 4040; + + set $base_path "${HYPERDX_BASE_PATH}"; + if ($base_path = "/") { + set $base_path ""; + } + + # Common proxy headers + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Redirect root to base path, if a base path is set + location = / { + if ($base_path != "") { + return 301 $base_path; + } + # If no base path, just proxy to the app + proxy_pass http://app; + } + + # This handles assets and api calls made to the root and rewrites them to include the base path + location ~ ^(/api/|/_next/|/__ENV\.js$|/Icon32\.png$) { + # Note: $request_uri includes the original full path including query string + proxy_pass http://app$base_path$request_uri; + } + + # Proxy requests that are already prefixed with the base path to the app + location ${HYPERDX_BASE_PATH} { + # The full request URI (e.g., /hyperdx/settings) is passed to the upstream + proxy_pass http://app; + } +} \ No newline at end of file diff --git a/proxy/traefik/config.yml b/proxy/traefik/config.yml new file mode 100644 index 000000000..bc7625941 --- /dev/null +++ b/proxy/traefik/config.yml @@ -0,0 +1,46 @@ +http: + routers: + # This handles the main app at the basepath + app-router: + entryPoints: + - web + rule: 'PathPrefix(`{{ env "HYPERDX_BASE_PATH" }}`)' + service: app-service + + # This handles assets and api calls at the root and rewrites them + assets-api-router: + entryPoints: + - web + rule: + 'PathPrefix(`/api`) || PathPrefix(`/_next`) || Path(`/__ENV.js`) || + Path(`/Icon32.png`)' + service: app-service + middlewares: + - add-basepath + + # This redirects from / to the basepath + root-redirect: + entryPoints: + - web + rule: 'Path(`/`)' + service: app-service # service is required, but redirect will happen first + middlewares: + - redirect-to-basepath + + middlewares: + add-basepath: + addPrefix: + prefix: '{{ env "HYPERDX_BASE_PATH" }}' + + redirect-to-basepath: + redirectRegex: + regex: '^/$' + replacement: '{{ env "HYPERDX_BASE_PATH" }}' + permanent: true + + services: + app-service: + loadBalancer: + passHostHeader: true + servers: + - url: 'http://127.0.0.1:8080' diff --git a/proxy/traefik/traefik.yml b/proxy/traefik/traefik.yml new file mode 100644 index 000000000..7d3ff7471 --- /dev/null +++ b/proxy/traefik/traefik.yml @@ -0,0 +1,8 @@ +entryPoints: + web: + address: ':4040' + +providers: + file: + filename: /etc/traefik/dynamic/config.yml + watch: true