Skip to content

Comments

Fix/issue 5200 web base path#8589

Open
pesnik wants to merge 5 commits intomakeplane:previewfrom
pesnik:fix/issue-5200-web-base-path
Open

Fix/issue 5200 web base path#8589
pesnik wants to merge 5 commits intomakeplane:previewfrom
pesnik:fix/issue-5200-web-base-path

Conversation

@pesnik
Copy link

@pesnik pesnik commented Jan 27, 2026

Fix: Add custom base path support for subpath deployments

Description

Fixes #5200

This PR adds comprehensive support for deploying Plane on custom base paths (e.g., mydomain.com/plane), enabling reverse proxy deployments on subpaths. Previously, static assets, internal links, and authentication redirects failed when deploying on a subpath.

Problem

When users deploy Plane on a subpath using reverse proxies, multiple issues occurred:

  1. Static assets 404: Assets served without base path prefix
  2. Broken manifest links: PWA manifests referenced with absolute paths
  3. Hydration mismatches: Server/client rendering differences in fallback components
  4. Admin app redirects: "Redirect to Plane" and workspace links pointed to wrong URLs
  5. Auth redirect failures: Login/logout redirected to root domain without subpath
  6. Admin container dropped by Traefik: Docker healthcheck failed, causing Traefik to remove admin from routing pool
  7. File upload failures (Mixed Content & Subpath): Presigned URLs and upload services ignored the subpath and used insecure protocols.

The space and admin apps already supported base paths via VITE_SPACE_BASE_PATH and VITE_ADMIN_BASE_PATH, but the web app was missing this feature, cross-app navigation was broken, and backend auth redirects ignored APP_BASE_PATH.

Solution

1. Web App Base Path Support

Added VITE_WEB_BASE_PATH configuration following the same pattern as other apps:

  • Updated vite.config.ts to use joinUrlPath utility and set Vite's base option
  • Modified Dockerfile.web to accept VITE_WEB_BASE_PATH build argument
  • Configured React Router basename with trailing slash strip in react-router.config.ts

2. Manifest Path Fixes

Fixed hardcoded manifest paths across all apps to support subpaths:

  • Changed absolute paths (/site.webmanifest.json) to use joinUrlPath() with base path
  • Updated icon paths in manifest JSON files to be relative (removed leading /)

3. Hydration Fixes

Resolved React hydration mismatches in HydrateFallback components:

  • Added mounted state checks to ensure server/client render the same initial content
  • Applied fix to web, admin, and space apps

4. Admin App Cross-Navigation

Fixed admin app links to respect web app's custom base path:

  • Updated "Redirect to Plane" button to use WEB_URL (includes base path)
  • Fixed workspace list links to include base path
  • Updated workspace creation form URL preview

5. Backend Auth Redirect Fix

Fixed base_host() in plane/utils/host.py and plane/authentication/utils/host.py to append APP_BASE_PATH when generating redirect URLs. Previously, the function formatted and appended paths for ADMIN_BASE_PATH and SPACE_BASE_PATH but completely skipped APP_BASE_PATH, causing login/logout to redirect to root domain without the subpath prefix.

6. Admin Nginx Configuration

  • Added health check endpoint (location = /) returning 200 to satisfy Docker's HEALTHCHECK. Without this, the container was marked unhealthy and Traefik removed it from its routing pool entirely.
  • Added exact location = /god-mode match to serve index.html directly, preventing Nginx's default 301 trailing-slash redirect which broke the proxy chain.

7. MinIO Upload Fixes (Mixed Content & Subpath)

  • Backend Storage: Added APP_BASE_PATH to S3Storage endpoint URL in storage.py to ensure presigned URLs point to the correct subpath.
  • Frontend Service: Updated FileUploadService to use API_BASE_URL ensuring the upload request origin matches the application origin.
  • Traefik Routing: Added a specific Traefik router for /plane/uploads in docker-compose-subpath.yml with TLS support to fix mixed-content blocks and route directly to MinIO.

8. Subpath Deployment Example

Added docker-compose-subpath.yml with a complete Traefik-based routing configuration:

  • Priority-based path matching: admin (100), API (100), uploads (99), live (90), space (80), web (50)
  • Correct stripprefix middleware configuration (strip only /plane, not the full subpath)
  • Full infrastructure stack (Postgres, Redis, RabbitMQ, MinIO)

8. Configuration & Documentation

  • Added BASE_URL to turbo.json globalEnv for linting
  • Updated all .env.example files with VITE_WEB_BASE_PATH documentation
  • Added helpful comments for backend APP_BASE_PATH configuration

Changes

Core Files Modified

Web App:

  • apps/web/vite.config.ts — Base path configuration
  • apps/web/Dockerfile.web — Build argument support
  • apps/web/react-router.config.ts — React Router basename with trailing slash strip
  • apps/web/app/root.tsx — Manifest path fix, hydration fix
  • apps/web/app/layout.tsx — Manifest path fix, base path support
  • apps/web/public/manifest.json — Relative icon paths
  • apps/web/public/site.webmanifest.json — Relative icon paths
  • apps/web/.env.example — Documentation

Admin App:

  • apps/admin/app/root.tsx — Manifest path fix, hydration fix
  • apps/admin/react-router.config.ts — Basename trailing slash strip
  • apps/admin/Dockerfile.admin — Build arg addition
  • apps/admin/nginx/nginx.conf — Health check endpoint, exact /god-mode match
  • apps/admin/app/(all)/(dashboard)/sidebar-help-section.tsx — Use WEB_URL for redirect
  • apps/admin/app/(all)/(dashboard)/workspace/create/form.tsx — Use WEB_URL for preview
  • apps/admin/core/components/workspace/list-item.tsx — Use WEB_URL for links
  • apps/admin/public/site.webmanifest.json — Relative icon paths
  • apps/admin/.env.example — Documentation

Space App:

  • apps/space/app/root.tsx — Hydration fix
  • apps/space/react-router.config.ts — Basename trailing slash strip
  • apps/space/public/site.webmanifest.json — Relative icon paths
  • apps/space/.env.example — Documentation

Backend API:

  • apps/api/plane/utils/host.py — APP_BASE_PATH support in base_host()
  • apps/api/plane/authentication/utils/host.py — Same fix (duplicate utility)
  • apps/api/plane/settings/storage.py — Added APP_BASE_PATH to S3 endpoint URL
  • apps/api/.env.example — Documentation for APP_BASE_PATH

Infrastructure:

  • docker-compose-subpath.yml — [NEW] Complete Traefik subpath deployment example
  • turbo.jsonBASE_URL in globalEnv

Testing

Quick Start (Subpath Deployment):

# 1. Configure backend env
cp apps/api/.env.example apps/api/.env
# Edit apps/api/.env:
#   WEB_URL="http://localhost/plane"
#   APP_BASE_URL="http://localhost"
#   APP_BASE_PATH="/plane"
#   ADMIN_BASE_PATH="/plane/god-mode"
#   SPACE_BASE_PATH="/plane/spaces"

# 2. Build and deploy
docker compose -f docker-compose-subpath.yml build
docker compose -f docker-compose-subpath.yml up -d

# 3. Access at http://localhost/plane

Verification:

  1. ✅ Static assets load from /plane/assets/...
  2. ✅ PWA manifests accessible at /plane/site.webmanifest.json
  3. ✅ No hydration warnings in console
  4. ✅ "Redirect to Plane" button in admin points to /plane/
  5. ✅ Workspace links include /plane/ prefix
  6. ✅ Login/logout redirects preserve /plane/ prefix
  7. ✅ Admin container stays healthy (Traefik keeps routing active)
  8. /plane/god-mode serves admin app without redirect loops
  9. ✅ Full stack deployment works via docker-compose-subpath.yml

Backward Compatibility

Not a breaking change

  • Default VITE_WEB_BASE_PATH is / (root path)
  • Default APP_BASE_PATH is / (root path)
  • Existing deployments continue to work without modification
  • All changes are additive or fix existing bugs

Checklist

  • Code follows project coding guidelines
  • Changes match existing patterns (space/admin apps)
  • Default values maintain backward compatibility
  • Hydration issues resolved
  • Cross-app navigation works with custom paths
  • Backend auth redirects preserve subpath
  • Admin Nginx healthcheck passes for Traefik routing
  • Documentation updated (.env.example files)
  • Subpath deployment example included (docker-compose-subpath.yml)
  • Commit messages follow contributing guidelines
  • References issue [bug]: APP_BASE_URL does not change static paths #5200

Related

@CLAassistant
Copy link

CLAassistant commented Jan 27, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds configurable app base-path support (VITE_WEB_BASE_PATH / APP_BASE_PATH), wires base-path into Vite, router basenames, manifests and Docker build args, normalizes manifest icon paths to relative, replaces WEB_BASE_URL → WEB_URL usages, adds client-side mount guards for hydration, and provides a subpath docker-compose for Traefik routing.

Changes

Cohort / File(s) Summary
Env examples
apps/admin/.env.example, apps/space/.env.example, apps/web/.env.example, apps/api/.env.example
Added VITE_WEB_BASE_PATH to frontends and APP_BASE_PATH + explanatory comments to API example to support subpath deployments.
Vite / build / env propagation
apps/web/vite.config.ts, apps/web/Dockerfile.web, turbo.json
Vite base set from VITE_WEB_BASE_PATH; Dockerfile exposes ARG/ENV VITE_WEB_BASE_PATH; BASE_URL added to Turbo globalEnv.
App roots / Hydration / Manifests
apps/admin/app/root.tsx, apps/space/app/root.tsx, apps/web/app/root.tsx, apps/web/app/layout.tsx
Introduce WEB_BASE_PATH from env, use joinUrlPath(WEB_BASE_PATH, ...) for manifest hrefs, and add an isMounted mount-guard to HydrateFallback to avoid SSR hydration mismatches.
React Router basePath
apps/web/react-router.config.ts, apps/space/react-router.config.ts, apps/admin/react-router.config.ts
Compute normalized basePath from env via joinUrlPath(...), strip trailing slash, and assign to router basename.
Constant usage updates
apps/admin/app/(all)/(dashboard)/sidebar-help-section.tsx, apps/admin/app/(all)/(dashboard)/workspace/create/form.tsx, apps/admin/core/components/workspace/list-item.tsx
Replaced WEB_BASE_URL imports/usage with WEB_URL and updated href/URL construction accordingly.
Manifests / static files
apps/admin/public/site.webmanifest.json, apps/space/public/site.webmanifest.json, apps/web/public/site.webmanifest.json, apps/web/public/manifest.json
Normalized icon entries to expanded objects and removed leading slashes from src paths (relative paths); apps/web/public/manifest.json also sets start_url to ".".
Admin Docker / Nginx
apps/admin/Dockerfile.admin, apps/admin/nginx/nginx.conf
Changed VITE_ADMIN_BASE_PATH default to "/plane/god-mode", removed default Nginx conf in prod stage, and added exact-match routes including a health endpoint and dedicated /god-mode static-serving block.
Compose for subpath deployments
docker-compose-subpath.yml
New comprehensive docker-compose enabling path-based routing via Traefik; adds many services, volumes, networks, and path-prefix labels for subpath deployment.
Miscellaneous
apps/live/package.json, apps/admin/core/components/workspace/list-item.tsx
Removed tsc --noEmit from live build script; minor formatting and className consolidation in admin component.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Traefik
  participant AppService
  participant StaticFiles
  Client->>Traefik: GET /plane/_next/static/...
  Traefik->>Traefik: StripPrefix /plane
  Traefik->>AppService: GET /_next/static/...
  AppService->>StaticFiles: Resolve asset via joinUrlPath(WEB_BASE_PATH, assetPath)
  StaticFiles-->>AppService: Return asset bytes
  AppService-->>Traefik: 200 + Asset
  Traefik-->>Client: 200 + Asset
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I hop through envs and stitch each trail,
strip trailing slashes, follow the rail.
Manifests turn relative, paths unwrap,
Traefik peels prefixes — no more gap.
A rabbit's nudge: subpaths spring to map. 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Most changes align with base path support objectives, but some modifications appear tangential: the build script simplification in apps/live/package.json (removing tsc --noEmit) and the Nginx routing changes are not clearly tied to the core base path fix. Clarify whether apps/live/package.json build-script change and Nginx routing refactor in apps/admin/nginx/nginx.conf are necessary for base path support or separate improvements.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix/issue 5200 web base path' clearly references the main objective (fixing issue #5200) and identifies the specific area (web base path support), accurately summarizing the changeset's primary focus.
Linked Issues check ✅ Passed The changeset fully addresses the requirements from issue #5200: adds configurable base path support (VITE_WEB_BASE_PATH), replaces hardcoded absolute paths with joinUrlPath calls, fixes hydration mismatches, updates cross-app navigation to use WEB_URL, and maintains backward compatibility.
Description check ✅ Passed The PR description is comprehensive and well-structured, following the template with all required sections including Description, Type of Change, Test Scenarios, and References.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/admin/app/`(all)/(dashboard)/workspace/create/form.tsx:
- Line 40: The current line double-encodes WEB_URL and uses a fallback that
omits the app base path; update the workspaceBaseURL logic: stop calling
encodeURI on WEB_URL (use WEB_URL as-is) and build a fallback that preserves the
origin plus the app base path from window.location.pathname (e.g., reconstruct
origin + first path segment(s) so the base path isn't lost) when WEB_URL is
undefined; locate and change the workspaceBaseURL constant in form.tsx
(references: workspaceBaseURL, WEB_URL, window.location).

In `@apps/web/app/layout.tsx`:
- Line 63: The head contains two <link rel="manifest"> tags pointing to
different files (site.webmanifest.json and manifest.json) so the browser only
uses the first; pick one approach: either remove the duplicate link (delete the
extra <link rel="manifest"> that uses joinUrlPath(WEB_BASE_PATH, "...")
referencing the manifest you don't want) or merge the two manifest files into a
single consolidated manifest (combine icon entries and other fields, save as a
single filename) and update the remaining <link rel="manifest"> (the usage that
calls joinUrlPath and WEB_BASE_PATH) to point to the consolidated manifest;
ensure icon paths inside the chosen manifest are correct for the deployed
assets.
🧹 Nitpick comments (2)
turbo.json (1)

9-9: Addition of BASE_URL to globalEnv looks correct.

This ensures Turbo properly invalidates the build cache when the base URL configuration changes, which is necessary for the custom base path feature.

Nitpick: For consistency, consider placing BASE_URL in alphabetical order (after APP_VERSION, before DEV).

   "globalEnv": [
     "APP_VERSION",
+    "BASE_URL",
     "DEV",
     "LOG_LEVEL",
     "NODE_ENV",
-    "BASE_URL",
     "SENTRY_DSN",
apps/web/public/site.webmanifest.json (1)

10-19: LGTM on relative paths. Consider providing properly sized icons.

The switch to relative paths (plane-logos/plane-mobile-pwa.png) is correct for base path support.

However, both icon entries reference the same image file with different declared sizes (192x192 and 512x512). For optimal PWA quality, consider providing separate image files at each resolution to avoid browser scaling artifacts.

@pesnik pesnik force-pushed the fix/issue-5200-web-base-path branch from ea2077c to da879a3 Compare January 27, 2026 11:26
@pesnik pesnik marked this pull request as draft January 27, 2026 11:31
@pesnik pesnik marked this pull request as ready for review January 27, 2026 14:16
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/admin/nginx/nginx.conf (1)

29-33: ⚠️ Potential issue | 🟠 Major

Healthcheck will fail — no location handles /

The Dockerfile.admin (Line 87) defines a healthcheck that curls http://127.0.0.1:3000/, but the only location block here is /god-mode/. Requests to / will return a 404, causing the container to be perpetually marked unhealthy.

Additionally, location /god-mode/ (with trailing slash) won't match a bare /god-mode request (without trailing slash), which could cause a redirect loop or 404 depending on the client.

Consider adding a catch-all or explicit health endpoint, and handling the no-trailing-slash case:

Proposed fix
-    location /god-mode/ {
+    location /god-mode {
       root   /usr/share/nginx/html;
       index  index.html index.htm;
       try_files $uri $uri/ /god-mode/index.html;
     }
+
+    location = /healthz {
+      return 200 'ok';
+      add_header Content-Type text/plain;
+    }

Then update the Dockerfile healthcheck to target /healthz:

-  CMD curl -fsS http://127.0.0.1:3000/ >/dev/null || exit 1
+  CMD curl -fsS http://127.0.0.1:3000/healthz >/dev/null || exit 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin/nginx/nginx.conf` around lines 29 - 33, Add a root/catch-all
location and an explicit health endpoint so the Docker healthcheck (which
currently curls the container root) succeeds, and make `/god-mode` without
trailing slash redirect or serve the same content as `/god-mode/`; specifically,
update the nginx config to (1) add a location /healthz that returns 200 for
health checks, (2) add a location / (or a catch-all) that serves the app (or
proxies) so requests to `/` don't 404, and (3) ensure the existing location
/god-mode/ also handles the no-trailing-slash case by adding a redirect or a
`location = /god-mode { ... }` that serves the same index; then update
Dockerfile.admin healthcheck to curl /healthz.
🧹 Nitpick comments (4)
docker-compose-subpath.yml (3)

75-80: space service lacks stripprefix middleware — intentional but inconsistent with admin.

The space service forwards /plane/spaces/... without stripping, relying on SSR with basename="/plane/spaces" to handle full paths. This is correct for SSR. However, the admin service also defines its own base path (/plane/god-mode) but uses stripprefix to remove /plane. This asymmetry is fine given the different architectures (SSR vs static nginx), but documenting the rationale in comments would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose-subpath.yml` around lines 75 - 80, The space service's Traefik
labels (traefik.http.routers.plane-space.rule and related labels) intentionally
omit a stripprefix middleware while the admin service uses stripprefix; add a
concise comment above the space service labels explaining that space is an SSR
app using basename="/plane/spaces" so it must receive the full path (hence no
stripprefix), and reference that the admin service is static/nginx and strips
/plane for routing—this documents the asymmetry for future maintainers.

239-241: External traefik_default network requires a pre-existing Traefik instance.

This compose file won't start successfully without a Traefik container already running and creating the traefik_default network. Consider adding a brief comment or a companion docker-compose-traefik.yml reference so users know how to bootstrap the required Traefik infrastructure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose-subpath.yml` around lines 239 - 241, The Compose file declares
an external network "traefik_default" (networks -> traefik_default -> external:
true) which requires a pre-existing Traefik stack to create that network; add a
brief inline comment above the networks block explaining that Traefik must be
deployed first (or reference a companion compose like
docker-compose-traefik.yml), or alternatively provide an optional local network
fallback by removing external: true or documenting how to bootstrap the
traefik_default network with docker-compose up for the Traefik stack.

186-188: Hardcoded default credentials — document as development-only.

The Postgres (plane/plane), RabbitMQ (plane/plane), and MinIO (access-key/secret-key) credentials are fine for a local development/demo compose file, but add a comment at the top of the file (or in a README) warning that these must be changed for production deployments.

Also applies to: 205-207, 221-222

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose-subpath.yml` around lines 186 - 188, Add a clear
development-only warning comment at the top of the compose file (and/or add a
README note) stating that hardcoded credentials must be changed for production;
specifically call out the Postgres env vars POSTGRES_USER, POSTGRES_PASSWORD,
POSTGRES_DB and the RabbitMQ and MinIO default credentials (e.g., plane/plane
and access-key/secret-key) so operators know these are insecure for production
and must be replaced or injected via secrets/ENV management.
apps/admin/react-router.config.ts (1)

4-4: Identical basePath expression duplicated across three router configs.

This exact expression appears in apps/web/react-router.config.ts, apps/space/react-router.config.ts, and here. Consider extracting a shared helper (e.g., resolveBasePath(envVar: string) in @plane/utils) to keep them in sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin/react-router.config.ts` at line 4, The duplicate complex basePath
computation (const basePath = (joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ??
"") ?? "/").replace(/\/$/, "") || "/";) is repeated across three router configs;
extract it to a shared helper like resolveBasePath(envVar: string) in
`@plane/utils` and use that helper from each config. Implement resolveBasePath to
accept the env var name or value, call joinUrlPath internally, ensure trailing
slash trimming (.replace(/\/$/, "")) and fallback to "/" exactly as the current
logic does, then replace the inline basePath declarations in
apps/admin/react-router.config.ts (and the other router configs) with a call to
resolveBasePath(process.env.VITE_ADMIN_BASE_PATH) so all configs stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/admin/Dockerfile.admin`:
- Line 38: The Dockerfile's build ARG VITE_ADMIN_BASE_PATH was changed to
"/plane/god-mode", breaking the previous default; revert ARG
VITE_ADMIN_BASE_PATH back to "/god-mode" so existing deployments stay
compatible, and keep docker-compose-subpath (which already overrides
VITE_ADMIN_BASE_PATH) to provide the subpath "/plane/god-mode" for those
deployments; update the ARG declaration (VITE_ADMIN_BASE_PATH) to the original
default and ensure documentation/comments note that docker-compose-subpath.yml
supplies the subpath override.

In `@docker-compose-subpath.yml`:
- Around line 158-175: The live service is forwarding requests at /plane/live
but the app expects env.LIVE_BASE_PATH (default /live); fix by either adding a
Traefik StripPrefix middleware and attaching it to the router or by configuring
the app to accept /plane/live via env: for example create a middleware name like
plane-live-strip using
traefik.http.middlewares.plane-live-strip.stripprefix.prefixes=/plane then add
traefik.http.routers.plane-live.middlewares=plane-live-strip to the live service
labels, or set LIVE_BASE_PATH="/plane/live" in the live env_file to align the
app’s router with the forwarded path (reference symbols: service name "live",
env var "LIVE_BASE_PATH", label keys
"traefik.http.middlewares.*.stripprefix.prefixes" and
"traefik.http.routers.plane-live.middlewares").

---

Outside diff comments:
In `@apps/admin/nginx/nginx.conf`:
- Around line 29-33: Add a root/catch-all location and an explicit health
endpoint so the Docker healthcheck (which currently curls the container root)
succeeds, and make `/god-mode` without trailing slash redirect or serve the same
content as `/god-mode/`; specifically, update the nginx config to (1) add a
location /healthz that returns 200 for health checks, (2) add a location / (or a
catch-all) that serves the app (or proxies) so requests to `/` don't 404, and
(3) ensure the existing location /god-mode/ also handles the no-trailing-slash
case by adding a redirect or a `location = /god-mode { ... }` that serves the
same index; then update Dockerfile.admin healthcheck to curl /healthz.

---

Duplicate comments:
In `@apps/admin/Dockerfile.admin`:
- Around line 86-87: The HEALTHCHECK in Dockerfile.admin uses CMD curl -fsS
http://127.0.0.1:3000/ which will always fail because nginx only serves under
/god-mode/; update the HEALTHCHECK command to target the correct path (e.g.,
http://127.0.0.1:3000/god-mode/) so the container health probe hits the served
endpoint; modify the HEALTHCHECK line in Dockerfile.admin accordingly and keep
the same flags (--interval, --timeout, --start-period, --retries) while changing
only the URL path.

---

Nitpick comments:
In `@apps/admin/react-router.config.ts`:
- Line 4: The duplicate complex basePath computation (const basePath =
(joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ?? "") ?? "/").replace(/\/$/, "")
|| "/";) is repeated across three router configs; extract it to a shared helper
like resolveBasePath(envVar: string) in `@plane/utils` and use that helper from
each config. Implement resolveBasePath to accept the env var name or value, call
joinUrlPath internally, ensure trailing slash trimming (.replace(/\/$/, "")) and
fallback to "/" exactly as the current logic does, then replace the inline
basePath declarations in apps/admin/react-router.config.ts (and the other router
configs) with a call to resolveBasePath(process.env.VITE_ADMIN_BASE_PATH) so all
configs stay in sync.

In `@docker-compose-subpath.yml`:
- Around line 75-80: The space service's Traefik labels
(traefik.http.routers.plane-space.rule and related labels) intentionally omit a
stripprefix middleware while the admin service uses stripprefix; add a concise
comment above the space service labels explaining that space is an SSR app using
basename="/plane/spaces" so it must receive the full path (hence no
stripprefix), and reference that the admin service is static/nginx and strips
/plane for routing—this documents the asymmetry for future maintainers.
- Around line 239-241: The Compose file declares an external network
"traefik_default" (networks -> traefik_default -> external: true) which requires
a pre-existing Traefik stack to create that network; add a brief inline comment
above the networks block explaining that Traefik must be deployed first (or
reference a companion compose like docker-compose-traefik.yml), or alternatively
provide an optional local network fallback by removing external: true or
documenting how to bootstrap the traefik_default network with docker-compose up
for the Traefik stack.
- Around line 186-188: Add a clear development-only warning comment at the top
of the compose file (and/or add a README note) stating that hardcoded
credentials must be changed for production; specifically call out the Postgres
env vars POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB and the RabbitMQ and
MinIO default credentials (e.g., plane/plane and access-key/secret-key) so
operators know these are insecure for production and must be replaced or
injected via secrets/ENV management.

ARG VITE_ADMIN_BASE_URL=""
ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL
ARG VITE_ADMIN_BASE_PATH="/god-mode"
ARG VITE_ADMIN_BASE_PATH="/plane/god-mode"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Breaking default: VITE_ADMIN_BASE_PATH changed from /god-mode to /plane/god-mode

The PR states backward compatibility is preserved, but changing this default means existing deployments that rely on the previous /god-mode default will break without explicitly overriding the build arg. Consider keeping the original default and letting subpath deployments override it:

-ARG VITE_ADMIN_BASE_PATH="/plane/god-mode"
+ARG VITE_ADMIN_BASE_PATH="/god-mode"

The docker-compose-subpath.yml already passes VITE_ADMIN_BASE_PATH: "/plane/god-mode" explicitly (Line 13), so subpath deployments are unaffected.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ARG VITE_ADMIN_BASE_PATH="/plane/god-mode"
ARG VITE_ADMIN_BASE_PATH="/god-mode"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin/Dockerfile.admin` at line 38, The Dockerfile's build ARG
VITE_ADMIN_BASE_PATH was changed to "/plane/god-mode", breaking the previous
default; revert ARG VITE_ADMIN_BASE_PATH back to "/god-mode" so existing
deployments stay compatible, and keep docker-compose-subpath (which already
overrides VITE_ADMIN_BASE_PATH) to provide the subpath "/plane/god-mode" for
those deployments; update the ARG declaration (VITE_ADMIN_BASE_PATH) to the
original default and ensure documentation/comments note that
docker-compose-subpath.yml supplies the subpath override.

Comment on lines +158 to +175
live:
build:
context: .
dockerfile: ./apps/live/Dockerfile.live
args:
DOCKER_BUILDKIT: 1
restart: unless-stopped
networks:
- traefik_default
- plane_backend
env_file:
- ./apps/live/.env
labels:
- "traefik.enable=true"
- "traefik.http.routers.plane-live.rule=PathPrefix(`/plane/live`)"
- "traefik.http.routers.plane-live.priority=90"
- "traefik.http.routers.plane-live.entrypoints=web"
- "traefik.http.services.plane-live.loadbalancer.server.port=3000"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and inspect the live service configuration files
echo "=== Checking apps/live structure ==="
find apps/live -type f \( -name "*.ts" -o -name "*.js" -o -name "*.conf" -o -name "Dockerfile*" \) | head -20

echo -e "\n=== Checking Dockerfile ==="
fd -t f 'Dockerfile' apps/live/ -x cat {}

echo -e "\n=== Looking for routing/server setup in source files ==="
fd -t f '\.(ts|js)$' apps/live/src/ -x grep -l 'listen\|port\|path\|prefix\|basePath\|router' {} \; | head -10

echo -e "\n=== Checking for base path or prefix configuration ==="
rg -i 'base.*path|prefix|router.*path' apps/live/src/ -A2 -B2 --type ts 2>/dev/null | head -40

Repository: makeplane/plane

Length of output: 6998


🏁 Script executed:

#!/bin/bash
# Check the server setup and HTTP routing configuration
echo "=== Checking server.ts (HTTP server setup) ==="
cat apps/live/src/server.ts

echo -e "\n=== Checking start.ts (application startup) ==="
cat apps/live/src/start.ts

echo -e "\n=== Checking hocuspocus.ts (WebSocket routing) ==="
head -50 apps/live/src/hocuspocus.ts

Repository: makeplane/plane

Length of output: 7078


🏁 Script executed:

#!/bin/bash
# Check the environment configuration for LIVE_BASE_PATH
echo "=== Checking env.ts for LIVE_BASE_PATH ==="
cat apps/live/src/env.ts

Repository: makeplane/plane

Length of output: 1403


Add stripprefix middleware or configure LIVE_BASE_PATH for the live service.

The live service mounts its Express router at env.LIVE_BASE_PATH (default: /live), but Traefik routes /plane/live without a stripprefix middleware. This causes a path mismatch: requests at /plane/live/ws are forwarded to the container as-is, but the app expects /live/ws. Either add a stripprefix middleware to remove /plane (like web, admin, and api services have), or set LIVE_BASE_PATH="/plane/live" via environment configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose-subpath.yml` around lines 158 - 175, The live service is
forwarding requests at /plane/live but the app expects env.LIVE_BASE_PATH
(default /live); fix by either adding a Traefik StripPrefix middleware and
attaching it to the router or by configuring the app to accept /plane/live via
env: for example create a middleware name like plane-live-strip using
traefik.http.middlewares.plane-live-strip.stripprefix.prefixes=/plane then add
traefik.http.routers.plane-live.middlewares=plane-live-strip to the live service
labels, or set LIVE_BASE_PATH="/plane/live" in the live env_file to align the
app’s router with the forwarded path (reference symbols: service name "live",
env var "LIVE_BASE_PATH", label keys
"traefik.http.middlewares.*.stripprefix.prefixes" and
"traefik.http.routers.plane-live.middlewares").

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
apps/admin/nginx/nginx.conf (2)

33-42: Hardcoded /god-mode paths vs. configurable VITE_ADMIN_BASE_PATH.

The PR introduces a configurable VITE_ADMIN_BASE_PATH (defaulting to /plane/god-mode in the Dockerfile), but this nginx config hardcodes /god-mode in all location blocks. This works when the reverse proxy (Traefik) strips the outer prefix before forwarding, but if the deployment topology changes or someone customizes the admin base path without a corresponding proxy rewrite, these routes will break silently.

Consider either documenting this coupling or templating the nginx config (e.g., via envsubst at container start) to stay in sync with VITE_ADMIN_BASE_PATH.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin/nginx/nginx.conf` around lines 33 - 42, The nginx config hardcodes
the /god-mode paths in the location blocks (the exact "location = /god-mode" and
"location /god-mode/" entries) while the app uses a configurable
VITE_ADMIN_BASE_PATH, causing mismatches; update the nginx config to derive the
base path from the VITE_ADMIN_BASE_PATH environment variable (for example by
templating the two location blocks and replacing /god-mode with the env var at
container start via envsubst or another templating step) or add a clear
comment/doc explaining the required proxy rewrite so deployments that change
VITE_ADMIN_BASE_PATH remain consistent with the location rules.

29-31: Health check response will have Content-Type: application/octet-stream.

The default_type at the http level (line 10) is application/octet-stream, so the health probe response body "healthy\n" will be served with that content type. While most health-check consumers only inspect the status code, it's cleaner to set it explicitly.

Suggested fix
 location = / {
+  default_type text/plain;
   return 200 "healthy\n";
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin/nginx/nginx.conf` around lines 29 - 31, The health-check location
block (location = /) currently returns a plain string but inherits default_type
application/octet-stream from the http context; update the location block to
explicitly set the response content type (e.g. add default_type text/plain; or
add_header Content-Type "text/plain; charset=utf-8" always;) so the body
"healthy\n" is served with text/plain; modify the location = / block
accordingly.
docker-compose-subpath.yml (2)

211-213: Pin the MinIO image version for reproducibility.

All other infrastructure images are pinned (postgres:15.7-alpine, valkey:7.2.11-alpine, rabbitmq:3.13.6-management-alpine), but minio/minio defaults to latest. This can cause unexpected breakage on rebuild.

♻️ Proposed fix
-    image: minio/minio
+    image: minio/minio:RELEASE.2024-06-13T22-53-53Z

Pick a suitable stable release tag.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose-subpath.yml` around lines 211 - 213, The MinIO service
"plane-minio" currently uses the unpinned image "minio/minio" which may pull
latest; update the image field for the plane-minio service to a specific stable
MinIO release tag (e.g., replace "minio/minio" with "minio/minio:<stable-tag>")
to ensure reproducible builds and avoid accidental upgrades—choose an
appropriate stable tag consistent with other pinned images.

2-3: Inconsistent container_name usage across services.

plane-admin specifies container_name: plane-admin-subpath but no other service does. This can cause confusion when inspecting containers. Consider either adding container_name to all services or removing it from plane-admin for consistency.

Also applies to: 30-31

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose-subpath.yml` around lines 2 - 3, The docker-compose service
"plane-admin" sets container_name: plane-admin-subpath while other services do
not, causing inconsistency; either remove the container_name line from the
"plane-admin" service to let Docker auto-name containers, or add consistent
container_name entries for every service (e.g., <service>-subpath) so naming is
uniform—locate the "plane-admin" service block and the other service blocks in
docker-compose-subpath.yml and apply one consistent approach across all
services.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docker-compose-subpath.yml`:
- Around line 186-188: Replace hardcoded credentials by using environment
variable substitution with sensible defaults and add a top-of-file warning;
specifically change POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB to use
${POSTGRES_USER:-plane}, ${POSTGRES_PASSWORD:-plane}, ${POSTGRES_DB:-plane} and
apply the same pattern to the plane-mq and plane-minio credentials (e.g.,
replace literal access-key/secret-key with ${MINIO_ACCESS_KEY:-access-key} /
${MINIO_SECRET_KEY:-secret-key} or analogous MQ vars), and add a prominent
comment at the top of the compose file instructing users to override these vars
for production.
- Around line 75-80: The plane-space router is missing the Traefik stripPrefix
middleware so requests keep the leading /plane and 404; add a middleware label
(e.g., traefik.http.middlewares.plane-strip.stripprefix.prefixes=/plane) and
attach it to the router via
traefik.http.routers.plane-space.middlewares=plane-strip@docker (use the same
middleware name used by admin/web if one exists) so the container receives paths
starting with /spaces.

---

Duplicate comments:
In `@docker-compose-subpath.yml`:
- Around line 158-175: The live service is missing the Traefik stripprefix
middleware so requests come in as /plane/live/... but the app expects /live/...;
either add the same stripprefix middleware labels used for the space service
(create/attach a middleware that strips the `/plane` prefix and reference it in
the live service labels, e.g. traefik.http.routers.plane-live.middlewares=...)
or set LIVE_BASE_PATH="/plane/live" in the live service environment (.env) so
Express receives the expected base path; update the service named "live" and
ensure the middleware name matches the router label used by plane-live.

---

Nitpick comments:
In `@apps/admin/nginx/nginx.conf`:
- Around line 33-42: The nginx config hardcodes the /god-mode paths in the
location blocks (the exact "location = /god-mode" and "location /god-mode/"
entries) while the app uses a configurable VITE_ADMIN_BASE_PATH, causing
mismatches; update the nginx config to derive the base path from the
VITE_ADMIN_BASE_PATH environment variable (for example by templating the two
location blocks and replacing /god-mode with the env var at container start via
envsubst or another templating step) or add a clear comment/doc explaining the
required proxy rewrite so deployments that change VITE_ADMIN_BASE_PATH remain
consistent with the location rules.
- Around line 29-31: The health-check location block (location = /) currently
returns a plain string but inherits default_type application/octet-stream from
the http context; update the location block to explicitly set the response
content type (e.g. add default_type text/plain; or add_header Content-Type
"text/plain; charset=utf-8" always;) so the body "healthy\n" is served with
text/plain; modify the location = / block accordingly.

In `@docker-compose-subpath.yml`:
- Around line 211-213: The MinIO service "plane-minio" currently uses the
unpinned image "minio/minio" which may pull latest; update the image field for
the plane-minio service to a specific stable MinIO release tag (e.g., replace
"minio/minio" with "minio/minio:<stable-tag>") to ensure reproducible builds and
avoid accidental upgrades—choose an appropriate stable tag consistent with other
pinned images.
- Around line 2-3: The docker-compose service "plane-admin" sets container_name:
plane-admin-subpath while other services do not, causing inconsistency; either
remove the container_name line from the "plane-admin" service to let Docker
auto-name containers, or add consistent container_name entries for every service
(e.g., <service>-subpath) so naming is uniform—locate the "plane-admin" service
block and the other service blocks in docker-compose-subpath.yml and apply one
consistent approach across all services.

Comment on lines +75 to +80
labels:
- "traefik.enable=true"
- "traefik.http.routers.plane-space.rule=PathPrefix(`/plane/spaces`)"
- "traefik.http.routers.plane-space.priority=80"
- "traefik.http.routers.plane-space.entrypoints=web"
- "traefik.http.services.plane-space.loadbalancer.server.port=3000"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing stripprefix middleware on the space service will cause routing failures.

Every other Traefik-routed frontend service (admin, web) defines a stripprefix middleware to remove /plane before forwarding to the container. The space service is missing both the middleware assignment and its definition, so requests will arrive at the container with the full /plane/spaces/... path instead of /spaces/..., causing 404s.

🐛 Proposed fix — add stripprefix middleware for `space`
     labels:
       - "traefik.enable=true"
       - "traefik.http.routers.plane-space.rule=PathPrefix(`/plane/spaces`)"
       - "traefik.http.routers.plane-space.priority=80"
       - "traefik.http.routers.plane-space.entrypoints=web"
+      - "traefik.http.routers.plane-space.middlewares=plane-space-stripprefix"
       - "traefik.http.services.plane-space.loadbalancer.server.port=3000"
+      - "traefik.http.middlewares.plane-space-stripprefix.stripprefix.prefixes=/plane"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose-subpath.yml` around lines 75 - 80, The plane-space router is
missing the Traefik stripPrefix middleware so requests keep the leading /plane
and 404; add a middleware label (e.g.,
traefik.http.middlewares.plane-strip.stripprefix.prefixes=/plane) and attach it
to the router via
traefik.http.routers.plane-space.middlewares=plane-strip@docker (use the same
middleware name used by admin/web if one exists) so the container receives paths
starting with /spaces.

Comment on lines +186 to +188
POSTGRES_USER: plane
POSTGRES_PASSWORD: plane
POSTGRES_DB: plane
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hardcoded plaintext credentials across database, message queue, and object storage services.

While this is an example/reference compose file, the hardcoded credentials (plane/plane, access-key/secret-key) could be copy-pasted into production deployments. Consider adding a prominent comment at the top of the file warning users to change credentials, or use environment variable substitution (${POSTGRES_PASSWORD:-plane}) so users can override without editing the file.

🛡️ Suggested approach — use env var substitution with defaults
   environment:
-    POSTGRES_USER: plane
-    POSTGRES_PASSWORD: plane
-    POSTGRES_DB: plane
+    POSTGRES_USER: ${POSTGRES_USER:-plane}
+    POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane}
+    POSTGRES_DB: ${POSTGRES_DB:-plane}

Apply the same pattern to plane-mq and plane-minio credentials.

Also applies to: 204-207, 221-222

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose-subpath.yml` around lines 186 - 188, Replace hardcoded
credentials by using environment variable substitution with sensible defaults
and add a top-of-file warning; specifically change
POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB to use ${POSTGRES_USER:-plane},
${POSTGRES_PASSWORD:-plane}, ${POSTGRES_DB:-plane} and apply the same pattern to
the plane-mq and plane-minio credentials (e.g., replace literal
access-key/secret-key with ${MINIO_ACCESS_KEY:-access-key} /
${MINIO_SECRET_KEY:-secret-key} or analogous MQ vars), and add a prominent
comment at the top of the compose file instructing users to override these vars
for production.

@pesnik pesnik force-pushed the fix/issue-5200-web-base-path branch from f78d7fa to b14d4dd Compare February 23, 2026 07:23
@pesnik
Copy link
Author

pesnik commented Feb 23, 2026

Hi team, this PR adds full subpath deployment support (fixes #5200). Would love a review when you get a chance! cc @pushya22 @SatishGandham

- Add VITE_WEB_BASE_PATH configuration to vite.config.ts and Dockerfile.web
- Configure React Router basename with trailing slash strip
- Fix hardcoded manifest paths in root.tsx and layout.tsx to use joinUrlPath
- Fix hydration mismatch in HydrateFallback component
- Update manifest.json and site.webmanifest.json to use relative icon paths
- Update FileUploadService to use API_BASE_URL for correct upload origin
- Document VITE_WEB_BASE_PATH in .env.example

Fixes makeplane#5200
- Fix manifest paths in root.tsx to use joinUrlPath with base path
- Fix hydration mismatch in HydrateFallback component
- Strip trailing slash from React Router basename
- Add Nginx health check endpoint (location = /) for Docker healthcheck
- Add exact /god-mode location match to prevent Nginx 301 redirects
- Update sidebar help section to use WEB_URL for Plane redirect
- Fix workspace form URL preview to include base path
- Fix workspace list item links to use WEB_URL
- Update site.webmanifest.json to use relative icon paths
- Add VITE_ADMIN_BASE_PATH build arg to Dockerfile
- Document configuration in .env.example
- Fix hydration mismatch in HydrateFallback component
- Strip trailing slash from React Router basename
- Update site.webmanifest.json to use relative icon paths
- Document VITE_SPACE_BASE_PATH in .env.example
- Add APP_BASE_PATH formatting logic to base_host() in plane/utils/host.py
- Apply same fix to plane/authentication/utils/host.py (duplicate utility)
- Add APP_BASE_PATH to S3 endpoint URL in storage.py for correct presigned URLs
- Ensures login/logout redirects and file uploads preserve the subpath prefix
- Document APP_BASE_PATH in .env.example
- Add docker-compose-subpath.yml with full Traefik-based subpath configuration
- Configure routing priorities for admin (100), api (100), live (90), space (80), web (50)
- Add specific Traefik router for /plane/uploads to fix mixed content S3 uploads
- Add BASE_URL to turbo.json globalEnv for build-time env passthrough
- Update live app package.json dependency
@pesnik pesnik force-pushed the fix/issue-5200-web-base-path branch from b14d4dd to e78e322 Compare February 24, 2026 04:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug]: APP_BASE_URL does not change static paths

2 participants