From f9b5a5b1448cb6a5fe80caa04949053ac932b6da Mon Sep 17 00:00:00 2001 From: hengxian-jiang Date: Thu, 28 Aug 2025 17:39:19 +0800 Subject: [PATCH] Customize prefix_path --- docker/Dockerfile | 53 +++++++++++++++++++++---- packages/server/src/index.ts | 77 +++++++++++++++++++++--------------- packages/ui/src/index.jsx | 17 ++++---- packages/ui/vite.config.js | 6 ++- 4 files changed, 105 insertions(+), 48 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 82a55d6a2b4..d34cfc801c2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,20 +6,59 @@ USER root # Skip downloading Chrome for Puppeteer (saves build time) ENV PUPPETEER_SKIP_DOWNLOAD=true -# Install latest Flowise globally (specific version can be set: flowise@1.0.0) -RUN npm install -g flowise +# Install build tools and pnpm +RUN apk add --no-cache python3 py3-pip make g++ git +RUN npm i -g pnpm turbo + +# Copy monorepo +WORKDIR /app +COPY . . + +# Set Vite base path for UI build at build time via ARG -> ENV +ARG VITE_BASE_PATH +ARG VITE_API_BASE_URL +ENV VITE_BASE_PATH=${VITE_BASE_PATH} +ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} + +# Install deps and build all packages +RUN pnpm install --frozen-lockfile && pnpm build # Stage 2: Runtime stage FROM node:20-alpine # Install runtime dependencies -RUN apk add --no-cache chromium git python3 py3-pip make g++ build-base cairo-dev pango-dev curl +RUN apk add --no-cache chromium curl cairo-dev pango-dev # Set the environment variable for Puppeteer to find Chromium ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser -# Copy Flowise from the build stage -COPY --from=build /usr/local/lib/node_modules /usr/local/lib/node_modules -COPY --from=build /usr/local/bin /usr/local/bin +# Create app directory +WORKDIR /app + +# Copy node_modules from builder for runtime first to preserve workspace structure +COPY --from=build /app/node_modules /app/node_modules +COPY --from=build /app/packages/server/node_modules /app/packages/server/node_modules + +# Copy built server dist and bin +COPY --from=build /app/packages/server/dist /app/packages/server/dist +COPY --from=build /app/packages/server/bin /app/packages/server/bin +COPY --from=build /app/packages/server/package.json /app/packages/server/package.json + +# Copy workspaces that are symlinked from node_modules +COPY --from=build /app/packages/ui /app/packages/ui +COPY --from=build /app/packages/components /app/packages/components + +# Default PORT +ENV PORT=3000 + +# Optional base path for server to mount under (e.g., "/flowise") +ENV BASE_PATH= + +# Expose port +EXPOSE 3000 -ENTRYPOINT ["flowise", "start"] +# Start server +# WORKDIR /app/packages/server +# ENTRYPOINT ["node", "dist/index.js"] +WORKDIR /app/packages/server/bin +ENTRYPOINT ["./run", "start"] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 40666c5c5fa..c3bb3ffd6ad 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,44 +1,44 @@ -import express, { Request, Response } from 'express' -import path from 'path' +import { ExpressAdapter } from '@bull-board/express' +import cookieParser from 'cookie-parser' import cors from 'cors' +import express, { Request, Response } from 'express' +import 'global-agent/bootstrap' import http from 'http' -import cookieParser from 'cookie-parser' +import path from 'path' import { DataSource, IsNull } from 'typeorm' -import { MODE, Platform } from './Interface' -import { getNodeModulesPackagePath, getEncryptionKey } from './utils' -import logger, { expressRequestLogger } from './utils/logger' -import { getDataSource } from './DataSource' -import { NodesPool } from './NodesPool' -import { ChatFlow } from './database/entities/ChatFlow' -import { CachePool } from './CachePool' import { AbortControllerPool } from './AbortControllerPool' -import { RateLimiterManager } from './utils/rateLimit' -import { getAllowedIframeOrigins, getCorsOptions, sanitizeMiddleware } from './utils/XSS' -import { Telemetry } from './utils/telemetry' -import flowiseApiV1Router from './routes' -import errorHandlerMiddleware from './middlewares/errors' -import { WHITELIST_URLS } from './utils/constants' +import { CachePool } from './CachePool' +import { ChatFlow } from './database/entities/ChatFlow' +import { getDataSource } from './DataSource' +import { Organization } from './enterprise/database/entities/organization.entity' +import { GeneralRole, Role } from './enterprise/database/entities/role.entity' +import { Workspace } from './enterprise/database/entities/workspace.entity' +import { LoggedInUser } from './enterprise/Interface.Enterprise' import { initializeJwtCookieMiddleware, verifyToken } from './enterprise/middleware/passport' import { IdentityManager } from './IdentityManager' -import { SSEStreamer } from './utils/SSEStreamer' -import { validateAPIKey } from './utils/validateKey' -import { LoggedInUser } from './enterprise/Interface.Enterprise' +import { MODE, Platform } from './Interface' import { IMetricsProvider } from './Interface.Metrics' -import { Prometheus } from './metrics/Prometheus' import { OpenTelemetry } from './metrics/OpenTelemetry' +import { Prometheus } from './metrics/Prometheus' +import errorHandlerMiddleware from './middlewares/errors' +import { NodesPool } from './NodesPool' import { QueueManager } from './queue/QueueManager' import { RedisEventSubscriber } from './queue/RedisEventSubscriber' -import 'global-agent/bootstrap' +import flowiseApiV1Router from './routes' import { UsageCacheManager } from './UsageCacheManager' -import { Workspace } from './enterprise/database/entities/workspace.entity' -import { Organization } from './enterprise/database/entities/organization.entity' -import { GeneralRole, Role } from './enterprise/database/entities/role.entity' +import { getEncryptionKey, getNodeModulesPackagePath } from './utils' import { migrateApiKeysFromJsonToDb } from './utils/apiKey' -import { ExpressAdapter } from '@bull-board/express' +import { WHITELIST_URLS } from './utils/constants' +import logger, { expressRequestLogger } from './utils/logger' +import { RateLimiterManager } from './utils/rateLimit' +import { SSEStreamer } from './utils/SSEStreamer' +import { Telemetry } from './utils/telemetry' +import { validateAPIKey } from './utils/validateKey' +import { getAllowedIframeOrigins, getCorsOptions, sanitizeMiddleware } from './utils/XSS' declare global { namespace Express { - interface User extends LoggedInUser {} + interface User extends LoggedInUser { } interface Request { user?: LoggedInUser } @@ -209,8 +209,10 @@ export class App { if (URL_CASE_INSENSITIVE_REGEX.test(req.path)) { // Step 2: Check if the req path is casesensitive if (URL_CASE_SENSITIVE_REGEX.test(req.path)) { - // Step 3: Check if the req path is in the whitelist - const isWhitelisted = whitelistURLs.some((url) => req.path.startsWith(url)) + // Step 3: Check if the req path is in the whitelist (respect BASE_PATH) + const basePath = (process.env.BASE_PATH || '').trim().replace(/\/$/, '') + const prefixedWhitelist = whitelistURLs.map((u) => (basePath ? `${basePath}${u}` : u)) + const isWhitelisted = prefixedWhitelist.some((url) => req.path.startsWith(url)) if (isWhitelisted) { next() } else if (req.headers['x-request-from'] === 'internal') { @@ -306,12 +308,20 @@ export class App { } } - this.app.use('/api/v1', flowiseApiV1Router) + // Respect optional URL prefix for both API and UI + const basePath = (process.env.BASE_PATH || '').trim().replace(/\/$/, '') || '' + + // Mount API with optional base path + if (basePath) { + this.app.use(`${basePath}/api/v1`, flowiseApiV1Router) + } else { + this.app.use('/api/v1', flowiseApiV1Router) + } // ---------------------------------------- // Configure number of proxies in Host Environment // ---------------------------------------- - this.app.get('/api/v1/ip', (request, response) => { + this.app.get(`${basePath || ''}/api/v1/ip`, (request, response) => { response.send({ ip: request.ip, msg: 'Check returned IP address in the response. If it matches your current IP address ( which you can get by going to http://ip.nfriedly.com/ or https://api.ipify.org/ ), then the number of proxies is correct and the rate limiter should now work correctly. If not, increase the number of proxies by 1 and restart Cloud-Hosted Flowise until the IP address matches your own. Visit https://docs.flowiseai.com/configuration/rate-limit#cloud-hosted-rate-limit-setup-guide for more information.' @@ -319,7 +329,8 @@ export class App { }) if (process.env.MODE === MODE.QUEUE && process.env.ENABLE_BULLMQ_DASHBOARD === 'true' && !this.identityManager.isCloud()) { - this.app.use('/admin/queues', this.queueManager.getBullBoardRouter()) + const adminBase = basePath ? `${basePath}/admin/queues` : '/admin/queues' + this.app.use(adminBase, this.queueManager.getBullBoardRouter()) } // ---------------------------------------- @@ -330,7 +341,9 @@ export class App { const uiBuildPath = path.join(packagePath, 'build') const uiHtmlPath = path.join(packagePath, 'build', 'index.html') - this.app.use('/', express.static(uiBuildPath)) + // Serve UI assets under base path + const uiMountPath = basePath || '/' + this.app.use(uiMountPath, express.static(uiBuildPath)) // All other requests not handled will return React app this.app.use((req: Request, res: Response) => { diff --git a/packages/ui/src/index.jsx b/packages/ui/src/index.jsx index d8833eed413..e956d933ce7 100644 --- a/packages/ui/src/index.jsx +++ b/packages/ui/src/index.jsx @@ -1,27 +1,30 @@ -import React from 'react' import App from '@/App' import { store } from '@/store' +import React from 'react' import { createRoot } from 'react-dom/client' // style + assets import '@/assets/scss/style.scss' // third party -import { BrowserRouter } from 'react-router-dom' -import { Provider } from 'react-redux' -import { SnackbarProvider } from 'notistack' -import ConfirmContextProvider from '@/store/context/ConfirmContextProvider' -import { ReactFlowContext } from '@/store/context/ReactFlowContext' import { ConfigProvider } from '@/store/context/ConfigContext' +import ConfirmContextProvider from '@/store/context/ConfirmContextProvider' import { ErrorProvider } from '@/store/context/ErrorContext' +import { ReactFlowContext } from '@/store/context/ReactFlowContext' +import { SnackbarProvider } from 'notistack' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' const container = document.getElementById('root') const root = createRoot(container) +// Read base path for routing from env (e.g., "/flowise"). Empty string or undefined means root +const routerBaseName = import.meta.env.VITE_BASE_PATH || '' + root.render( - + diff --git a/packages/ui/vite.config.js b/packages/ui/vite.config.js index c336cd4def2..cfab2f41f86 100644 --- a/packages/ui/vite.config.js +++ b/packages/ui/vite.config.js @@ -1,7 +1,7 @@ -import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -import { resolve } from 'path' import dotenv from 'dotenv' +import { resolve } from 'path' +import { defineConfig } from 'vite' export default defineConfig(async ({ mode }) => { let proxy = undefined @@ -41,6 +41,8 @@ export default defineConfig(async ({ mode }) => { build: { outDir: './build' }, + // Ensure built assets (css/js) are requested from the correct URL prefix + base: process.env.VITE_BASE_PATH || '/', server: { open: true, proxy,