Skip to content

Commit 05cc191

Browse files
author
Saravana Kumar Rajendran
committed
Split frontend bundles behind nginx gateway
1 parent 4e690d5 commit 05cc191

10 files changed

Lines changed: 405 additions & 7 deletions

File tree

docker/build_and_push.Dockerfile

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,13 @@ ENV VITE_CLERK_PUBLISHABLE_KEY=$VITE_CLERK_PUBLISHABLE_KEY
6666
RUN --mount=type=cache,target=/root/.npm \
6767
npm ci \
6868
&& ESBUILD_BINARY_PATH="" NODE_OPTIONS="--max-old-space-size=12288" JOBS=1 npm run build \
69-
&& cp -r build /app/src/backend/langflow/frontend \
69+
&& ESBUILD_BINARY_PATH="" NODE_OPTIONS="--max-old-space-size=12288" JOBS=1 npm run build -- --config ./vite.shell.config.mts \
70+
&& mkdir -p /app/static/frontend-app \
71+
&& cp -r build/. /app/static/frontend-app/ \
72+
&& mkdir -p /app/static/frontend-shell \
73+
&& cp -r build-shell/. /app/static/frontend-shell/ \
74+
&& cp -r build/. /app/src/backend/langflow/frontend \
75+
&& rm -rf build build-shell \
7076
&& rm -rf /tmp/src/frontend
7177

7278
WORKDIR /app
@@ -83,14 +89,19 @@ FROM python:3.12.3-slim AS runtime
8389

8490
RUN apt-get update \
8591
&& apt-get upgrade -y \
86-
&& apt-get install -y curl git libpq5 gnupg \
92+
&& apt-get install -y curl git libpq5 gnupg nginx \
8793
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
8894
&& apt-get install -y nodejs \
8995
&& apt-get clean \
9096
&& rm -rf /var/lib/apt/lists/* \
9197
&& useradd user -u 1000 -g 0 --no-create-home --home-dir /app/data
9298

99+
RUN rm -rf /etc/nginx/conf.d/* /etc/nginx/sites-enabled/*
100+
93101
COPY --from=builder --chown=1000 /app/.venv /app/.venv
102+
COPY --from=builder --chown=1000 /app/static /app/static
103+
COPY docker/nginx/multi-frontend.conf /etc/nginx/nginx.conf
104+
COPY --chmod=0755 --chown=1000 docker/runtime/start-services.sh /app/start-services.sh
94105

95106
# Place executables in the environment at the front of the path
96107
ENV PATH="/app/.venv/bin:$PATH"
@@ -105,7 +116,7 @@ USER user
105116
WORKDIR /app
106117

107118
ENV LANGFLOW_HOST=0.0.0.0
108-
ENV LANGFLOW_PORT=7860
119+
ENV LANGFLOW_PORT=7861
109120

110-
CMD ["langflow", "run"]
121+
CMD ["/app/start-services.sh"]
111122

docker/nginx/multi-frontend.conf

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
user user;
2+
pid /tmp/nginx.pid;
3+
worker_processes auto;
4+
5+
events {
6+
worker_connections 1024;
7+
}
8+
9+
http {
10+
include /etc/nginx/mime.types;
11+
default_type application/octet-stream;
12+
13+
sendfile on;
14+
tcp_nopush on;
15+
tcp_nodelay on;
16+
keepalive_timeout 65;
17+
types_hash_max_size 4096;
18+
19+
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
20+
'$status $body_bytes_sent "$http_referer" '
21+
'"$http_user_agent" "$http_x_forwarded_for"';
22+
23+
access_log /dev/stdout main;
24+
error_log /dev/stderr warn;
25+
26+
map $http_upgrade $connection_upgrade {
27+
default upgrade;
28+
'' close;
29+
}
30+
31+
upstream backend {
32+
server 127.0.0.1:7861;
33+
}
34+
35+
server {
36+
listen 7860;
37+
listen [::]:7860;
38+
39+
# Static assets for the landing/login shell
40+
location ^~ /shell-assets/ {
41+
alias /app/static/frontend-shell/shell-assets/;
42+
access_log off;
43+
expires 1y;
44+
add_header Cache-Control "public, immutable";
45+
}
46+
47+
# Static assets for the application bundle
48+
location ^~ /app-assets/ {
49+
alias /app/static/frontend-app/app-assets/;
50+
access_log off;
51+
expires 1y;
52+
add_header Cache-Control "public, immutable";
53+
}
54+
55+
location = / {
56+
root /app/static/frontend-shell;
57+
try_files $uri /index.html;
58+
}
59+
60+
location = /login {
61+
root /app/static/frontend-shell;
62+
try_files $uri /index.html;
63+
}
64+
65+
location = /organization {
66+
root /app/static/frontend-shell;
67+
try_files $uri /index.html;
68+
}
69+
70+
location /api/ {
71+
proxy_pass http://backend;
72+
proxy_http_version 1.1;
73+
proxy_set_header Host $host;
74+
proxy_set_header X-Real-IP $remote_addr;
75+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
76+
proxy_set_header X-Forwarded-Proto $scheme;
77+
proxy_set_header Upgrade $http_upgrade;
78+
proxy_set_header Connection $connection_upgrade;
79+
}
80+
81+
location /health {
82+
proxy_pass http://backend/health;
83+
proxy_http_version 1.1;
84+
proxy_set_header Host $host;
85+
proxy_set_header X-Real-IP $remote_addr;
86+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
87+
proxy_set_header X-Forwarded-Proto $scheme;
88+
}
89+
90+
location / {
91+
root /app/static/frontend-app;
92+
try_files $uri /index.html;
93+
}
94+
}
95+
}

docker/runtime/start-services.sh

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/sh
2+
set -e
3+
4+
LANGFLOW_HOST="${LANGFLOW_HOST:-0.0.0.0}"
5+
LANGFLOW_PORT="${LANGFLOW_PORT:-7861}"
6+
7+
cleanup() {
8+
if [ -n "${BACKEND_PID:-}" ]; then
9+
kill "$BACKEND_PID" 2>/dev/null || true
10+
fi
11+
if [ -n "${NGINX_PID:-}" ]; then
12+
kill "$NGINX_PID" 2>/dev/null || true
13+
fi
14+
}
15+
16+
trap cleanup INT TERM
17+
18+
langflow run --host "$LANGFLOW_HOST" --port "$LANGFLOW_PORT" &
19+
BACKEND_PID=$!
20+
21+
nginx -g "daemon off;" &
22+
NGINX_PID=$!
23+
24+
STATUS=0
25+
26+
set +e
27+
while :; do
28+
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
29+
wait "$BACKEND_PID"
30+
STATUS=$?
31+
break
32+
fi
33+
if ! kill -0 "$NGINX_PID" 2>/dev/null; then
34+
wait "$NGINX_PID"
35+
STATUS=$?
36+
break
37+
fi
38+
sleep 1
39+
done
40+
41+
cleanup
42+
wait 2>/dev/null || true
43+
exit "$STATUS"

src/frontend/shell/App.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Suspense, useEffect } from "react";
2+
import { RouterProvider } from "react-router-dom";
3+
import { LoadingPage } from "../src/pages/LoadingPage";
4+
import { useDarkStore } from "../src/stores/darkStore";
5+
import router from "./routes";
6+
7+
export default function ShellApp() {
8+
const dark = useDarkStore((state) => state.dark);
9+
10+
useEffect(() => {
11+
const body = document.getElementById("body");
12+
if (!body) return;
13+
if (dark) {
14+
body.classList.add("dark");
15+
} else {
16+
body.classList.remove("dark");
17+
}
18+
}, [dark]);
19+
20+
return (
21+
<Suspense fallback={<LoadingPage />}>
22+
<RouterProvider router={router} />
23+
</Suspense>
24+
);
25+
}

src/frontend/shell/index.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<base href="/" />
5+
<meta charset="UTF-8" />
6+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
9+
<meta http-equiv="Pragma" content="no-cache" />
10+
<meta http-equiv="Expires" content="0" />
11+
<link rel="icon" href="/visualaifavicon.png" />
12+
<link rel="manifest" href="/manifest.json" />
13+
<link rel="preconnect" href="https://fonts.googleapis.com" />
14+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
15+
<link
16+
href="https://fonts.googleapis.com/css2?family=Chivo:ital,wght@0,100..900;1,100..900&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
17+
rel="stylesheet"
18+
/>
19+
<title>Visual AI Agents Builder</title>
20+
</head>
21+
<body id="body" class="dark" style="width: 100%; height: 100%">
22+
<noscript>You need to enable JavaScript to run this app.</noscript>
23+
<div style="width: 100vw; height: 100vh" id="root"></div>
24+
<script type="module" src="/main.tsx"></script>
25+
</body>
26+
</html>

src/frontend/shell/main.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import ReactDOM from "react-dom/client";
2+
3+
import "../src/style/classes.css";
4+
// @ts-ignore
5+
import "../src/style/index.css";
6+
// @ts-ignore
7+
import "../src/App.css";
8+
import "../src/style/applies.css";
9+
10+
import AppWithProvider from "../src/clerk/auth";
11+
import ShellApp from "./App";
12+
13+
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
14+
15+
root.render(
16+
<AppWithProvider>
17+
<ShellApp />
18+
</AppWithProvider>,
19+
);

src/frontend/shell/routes.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Suspense, lazy } from "react";
2+
import {
3+
Outlet,
4+
Route,
5+
createBrowserRouter,
6+
createRoutesFromElements,
7+
} from "react-router-dom";
8+
import ContextWrapper from "../src/contexts";
9+
import { LoadingPage } from "../src/pages/LoadingPage";
10+
import { ProtectedLoginRoute } from "../src/components/authorization/authLoginGuard";
11+
import { CustomNavigate } from "../src/customization/components/custom-navigate";
12+
import { BASENAME } from "../src/customization/config-constants";
13+
import { ENABLE_CUSTOM_PARAM } from "../src/customization/feature-flags";
14+
15+
const LandingPage = lazy(() => import("../src/pages/LandingPage"));
16+
const LoginPage = lazy(() =>
17+
import("../src/clerk/login-pages").then((module) => ({
18+
default: module.LoginPage,
19+
})),
20+
);
21+
const OrganizationPage = lazy(() => import("../src/clerk/OrganizationPage"));
22+
23+
const router = createBrowserRouter(
24+
createRoutesFromElements(
25+
<Route
26+
path={ENABLE_CUSTOM_PARAM ? "/:customParam?" : "/"}
27+
element={
28+
<ContextWrapper key="shell">
29+
<Outlet />
30+
</ContextWrapper>
31+
}
32+
>
33+
<Route
34+
index
35+
element={
36+
<Suspense fallback={<LoadingPage />}>
37+
<LandingPage />
38+
</Suspense>
39+
}
40+
/>
41+
<Route
42+
path="login"
43+
element={
44+
<Suspense fallback={<LoadingPage />}>
45+
<ProtectedLoginRoute>
46+
<LoginPage />
47+
</ProtectedLoginRoute>
48+
</Suspense>
49+
}
50+
/>
51+
<Route
52+
path="organization"
53+
element={
54+
<Suspense fallback={<LoadingPage />}>
55+
<ProtectedLoginRoute>
56+
<OrganizationPage />
57+
</ProtectedLoginRoute>
58+
</Suspense>
59+
}
60+
/>
61+
<Route path="*" element={<CustomNavigate replace to="flows" />} />
62+
</Route>,
63+
),
64+
{ basename: BASENAME || undefined },
65+
);
66+
67+
export default router;

src/frontend/shell/tsconfig.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"compilerOptions": {
4+
"rootDir": "..",
5+
"outDir": "../dist"
6+
},
7+
"include": [
8+
"./**/*.ts",
9+
"./**/*.tsx",
10+
"../src/**/*.ts",
11+
"../src/**/*.tsx"
12+
]
13+
}

src/frontend/vite.config.mts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,19 @@ export default defineConfig(({ mode }) => {
3737
return proxyObj;
3838
}, {});
3939

40+
const assetsDir = "app-assets";
41+
4042
return {
4143
base: BASENAME || "",
4244
build: {
4345
outDir: "build",
46+
assetsDir,
4447
rollupOptions: {
4548
output: {
4649
// Add hash to filenames for cache busting
47-
entryFileNames: `assets/[name].[hash].js`,
48-
chunkFileNames: `assets/[name].[hash].js`,
49-
assetFileNames: `assets/[name].[hash].[ext]`
50+
entryFileNames: `${assetsDir}/[name].[hash].js`,
51+
chunkFileNames: `${assetsDir}/[name].[hash].js`,
52+
assetFileNames: `${assetsDir}/[name].[hash].[ext]`
5053
}
5154
}
5255
},

0 commit comments

Comments
 (0)