Skip to content

Commit 0aeef7b

Browse files
committed
feat: basePath
1 parent 060b4a4 commit 0aeef7b

25 files changed

+124
-60
lines changed

Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,16 @@ RUN npm run build
2828
FROM node:22-alpine AS runner
2929
WORKDIR /app
3030

31+
# Re-declare ARG to make it available in this stage
32+
ARG NEXT_PUBLIC_BASE_PATH=""
33+
3134
RUN apk update && apk upgrade && rm -rf /var/cache/apk/* \
3235
&& addgroup --system --gid 1001 nodejs \
3336
&& adduser --system --uid 1001 nextjs
3437

3538
ENV NODE_ENV=production
3639
ENV NEXT_TELEMETRY_DISABLED=1
40+
ENV NEXT_PUBLIC_BASE_PATH=${NEXT_PUBLIC_BASE_PATH}
3741

3842
COPY --from=builder /app/public ./public
3943
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
@@ -44,7 +48,8 @@ EXPOSE 3000
4448
ENV PORT=3000
4549
ENV HOSTNAME="0.0.0.0"
4650

51+
# Healthcheck uses the basePath for the health endpoint
4752
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
48-
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
53+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000${NEXT_PUBLIC_BASE_PATH}/api/health || exit 1
4954

5055
CMD ["node", "server.js"]

README.md

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,52 @@ docker run -p 3000:3000 \
5858
| `SESSION_SECRET` | No | Session signing secret (auto-generated) |
5959
| `NEXT_PUBLIC_BASE_PATH` | No | Base path for reverse proxy (e.g., `/dashboard`) |
6060

61-
## Reverse Proxy
61+
## Reverse Proxy / Kubernetes Ingress
62+
63+
The official Docker images are built with `NEXT_PUBLIC_BASE_PATH=/dashboard`, meaning the app expects to run under the `/dashboard` path.
64+
65+
### Kubernetes Ingress Example
66+
67+
```yaml
68+
apiVersion: networking.k8s.io/v1
69+
kind: Ingress
70+
metadata:
71+
name: dashboard-ingress
72+
spec:
73+
rules:
74+
- host: your-domain.com
75+
http:
76+
paths:
77+
- path: /dashboard
78+
pathType: Prefix
79+
backend:
80+
service:
81+
name: dashboard-service
82+
port:
83+
number: 3000
84+
```
85+
86+
No `rewrite-target` annotation is needed since the app already expects requests at `/dashboard`.
87+
88+
### Custom Base Path
89+
90+
To use a different base path, rebuild the Docker image:
91+
92+
```bash
93+
docker build --build-arg NEXT_PUBLIC_BASE_PATH=/your-path -t librechat-dashboard .
94+
```
95+
96+
### Local Development
6297

63-
To deploy under a sub-path (e.g., `/dashboard`):
98+
For local development without a base path, simply run:
6499

65100
```bash
66-
docker build --build-arg NEXT_PUBLIC_BASE_PATH=/dashboard -t librechat-dashboard .
101+
npm run dev
67102
```
68103

69-
> **Note**: `NEXT_PUBLIC_BASE_PATH` is baked into the build. Rebuild when changing.
104+
The app will be available at `http://localhost:3000` (without any path prefix).
105+
106+
> **Note**: `NEXT_PUBLIC_BASE_PATH` is baked into the build at compile time. You must rebuild when changing this value.
70107

71108
## Development Commands
72109

next.config.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { NextConfig } from "next";
22

3-
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "/dashboard";
3+
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
44

55
const nextConfig: NextConfig = {
66
output: "standalone",
7-
basePath: basePath,
8-
assetPrefix: basePath,
7+
basePath: basePath.length > 0 ? basePath : undefined,
8+
assetPrefix: basePath.length > 0 ? basePath : undefined,
99
poweredByHeader: false,
1010
reactStrictMode: true,
1111
images: {
@@ -33,15 +33,6 @@ const nextConfig: NextConfig = {
3333
},
3434
];
3535
},
36-
async redirects() {
37-
return [
38-
{
39-
source: "/",
40-
destination: "/dashboard",
41-
permanent: false,
42-
},
43-
];
44-
},
4536
};
4637

4738
export default nextConfig;

src/app/login/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from "@mui/material";
1515
import { useTheme } from "@mui/material/styles";
1616
import { type FormEvent, useCallback, useState } from "react";
17+
import { API_BASE, getAbsolutePath } from "@/lib/utils/api-base";
1718

1819
export default function LoginPage() {
1920
const [password, setPassword] = useState("");
@@ -32,7 +33,7 @@ export default function LoginPage() {
3233
setIsLoading(true);
3334

3435
try {
35-
const response = await fetch("/api/auth/login", {
36+
const response = await fetch(`${API_BASE}/auth/login`, {
3637
method: "POST",
3738
headers: {
3839
"Content-Type": "application/json",
@@ -44,7 +45,7 @@ export default function LoginPage() {
4445
if (response.ok) {
4546
setPassword(""); // Clear password from memory
4647
// Force a full page reload to reset AuthGuard state
47-
window.location.href = "/dashboard/dashboard";
48+
window.location.href = getAbsolutePath("/dashboard");
4849
} else {
4950
const data = await response.json();
5051
setError(data.error || "Authentication failed. Please try again.");

src/app/page.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { redirect } from "next/navigation";
22

33
export default function RootPage() {
4-
// When basePath is set to /dashboard, this page will be at /dashboard
5-
// Redirect to /dashboard/dashboard for consistency, but next.config.ts
6-
// already handles / -> /dashboard redirect
4+
// Redirect to the dashboard page
5+
// Next.js router automatically applies basePath, so "/dashboard" becomes:
6+
// - "/dashboard" when no basePath is set (local dev)
7+
// - "/dashboard/dashboard" when basePath="/dashboard" (Docker/production)
78
redirect("/dashboard");
89
}

src/atoms/active-users-atom.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { atom } from "jotai";
22
import type { ActiveUsers } from "@/components/models/active-users";
3+
import { API_BASE } from "@/lib/utils/api-base";
34
import { dateRangeAtom } from "./date-range-atom";
45

5-
const API_BASE = process.env.NEXT_PUBLIC_API_BACKEND_BASE_URL_NODE || "/api";
6-
76
export const activeUsersAtom = atom(async (get) => {
87
const timeArea = get(dateRangeAtom);
98
const res = await fetch(

src/atoms/agent-count-atom.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { atom } from "jotai";
22
import type { AgentCount } from "@/components/models/agent-count";
3-
4-
const API_BASE = process.env.NEXT_PUBLIC_API_BACKEND_BASE_URL_NODE || "/api";
3+
import { API_BASE } from "@/lib/utils/api-base";
54

65
export const agentCountAtom = atom(async () => {
76
const res = await fetch(`${API_BASE}/all-agents`);

src/atoms/all-agents-stats-table-atom.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { atom } from "jotai";
22
import type { AllAgentsStatsTable } from "@/components/models/all-agents-stats-table";
3+
import { API_BASE } from "@/lib/utils/api-base";
34
import { dateRangeAtom } from "./date-range-atom";
45

5-
const API_BASE = process.env.NEXT_PUBLIC_API_BACKEND_BASE_URL_NODE || "/api";
6-
76
export const allAgentsStatsTableAtom = atom(async (get) => {
87
const timeArea = get(dateRangeAtom);
98
const res = await fetch(

src/atoms/all-agents-stats-table-chart.atom.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import { atom } from "jotai";
22
import { atomFamily } from "jotai/utils";
33
import type { AllAgentsStatsChart } from "@/components/models/all-agents-stats-chart";
44
import { timeMap } from "@/components/utils/time-map";
5+
import { API_BASE } from "@/lib/utils/api-base";
56
import { dateRangeAtom } from "./date-range-atom";
67

7-
const API_BASE = process.env.NEXT_PUBLIC_API_BACKEND_BASE_URL_NODE || "/api";
8-
98
export const allAgentsStatsTableChartAtom = atomFamily((agent: string) =>
109
atom(async (get) => {
1110
const timeArea = get(dateRangeAtom);

src/atoms/all-models-stats-table-atom.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { atom } from "jotai";
22
import type { AllModelsStatsTable } from "@/components/models/all-models-stats-table";
3+
import { API_BASE } from "@/lib/utils/api-base";
34
import { dateRangeAtom } from "./date-range-atom";
45

5-
const API_BASE = process.env.NEXT_PUBLIC_API_BACKEND_BASE_URL_NODE || "/api";
6-
76
export const allModelsStatsTableAtom = atom(async (get) => {
87
const timeArea = get(dateRangeAtom);
98
const res = await fetch(

0 commit comments

Comments
 (0)