Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 46 additions & 7 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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: [email protected])
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}
Comment on lines +18 to +21
Copy link
Author

@hengxian-jiang hengxian-jiang Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docker build -t flowise:prefix \
  --build-arg VITE_BASE_PATH=/flowise \
  --build-arg VITE_API_BASE_URL=https://localhost:41100/flowise \
  -f docker/Dockerfile .

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hengxian-jiang the server should also have support to custom base path


# 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"]
77 changes: 45 additions & 32 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -306,20 +308,29 @@ 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.'
})
})

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())
}

// ----------------------------------------
Expand All @@ -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) => {
Expand Down
17 changes: 10 additions & 7 deletions packages/ui/src/index.jsx
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<BrowserRouter basename={routerBaseName}>
<SnackbarProvider>
<ConfigProvider>
<ErrorProvider>
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/vite.config.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down