- Added a new Vite application (`apps/vite`) to serve as a standalone SPA for the Dokploy dashboard, leveraging TanStack Router for routing. - Implemented server-side handling with a custom Node.js server to manage API requests and serve static files. - Integrated existing components and utilities from the Dokploy monorepo, ensuring no duplication of code. - Updated `.dockerignore` and `.gitignore` to include new build artifacts and environment files. - Enhanced `pnpm-lock.yaml` with new dependencies for the Vite application. - Created configuration files for Vite, TypeScript, and ESBuild to support the new application structure.
dokploy-vite
Vite + TanStack Router migration of the Dokploy dashboard UI. The Next.js app (apps/dokploy) stays intact and keeps serving the backend (tRPC, better-auth, websockets, webhooks). This app is a pure SPA that reuses the existing UI code directly from apps/dokploy — components are NOT duplicated.
How it works
- Aliases (
vite.config.ts):@/*resolves intoapps/dokploy/*, so all 380+ components, hooks, lib and utils are consumed from their original location.~/*resolves to this app'ssrc/. - Next.js shims (
src/shims/):next/link,next/router,next/navigation,next/head,next/dynamic,next/scriptandnextjs-toploaderare aliased to thin adapters over TanStack Router, so shared components run unmodified. - tRPC:
@/utils/apiis re-aliased tosrc/utils/api.ts(createTRPCReactinstead ofcreateTRPCNext), same links (ws split + FormData split + superjson). - Auth: SSR
getServerSidePropsguards were replaced bybeforeLoadguards using the existing better-auth client (@/lib/auth-client) with a 30s session cache (src/utils/session.ts). - Routes (
src/routes/): file-based TanStack routes mirroringapps/dokploy/pages./dashboardis a shared layout route renderingDashboardLayoutonce (it persists across navigations, unlike the pages-router setup). - Dev proxy:
/apiand every websocket endpoint (/drawer-logs,/terminal,/docker-container-*,/listen-*) proxy to the Next custom server onlocalhost:3000.
Standalone server (no Next.js)
server/server.ts is a Next-free replacement for apps/dokploy/server/server.ts. It reuses the entire existing backend from the monorepo:
- tRPC (
/api/trpc/*) and OpenAPI REST (/api/*catch-all) — the exact same handlers fromapps/dokploy/pages/api, mounted through a thin Next-API compat adapter (server/next-compat.ts) that providesreq.query/body/cookiesandres.status/json/send/redirecton plain Node req/res. - better-auth (
/api/auth/*) —toNodeHandler(auth.handler), already framework-agnostic. - Webhooks/callbacks — deploy (github, refreshToken, compose), Stripe (raw body preserved for signature check), GitHub setup/webhook, GitLab/Gitea OAuth callbacks: all reused unchanged via the adapter.
- Websockets — same
setup*WebSocketServerfunctions fromapps/dokploy/server/wss. - Bootstrapping — same production init (Traefik config, cron jobs, schedules, volume backups, deployment worker) as the original server.
- Static SPA — in production it serves
dist/with SPA fallback toindex.html(server/static.ts).
Development
One command — the server embeds Vite in middleware mode, so API + websockets + UI (with HMR) all run same-origin on :3000:
pnpm --filter dokploy-vite dev
# → http://localhost:3000 (no Next involved)
Alternative split mode (Vite dev server on :5173 proxying to whichever backend runs on :3000 — the Next custom server or this one):
pnpm --filter dokploy dev # or: nothing, if dev above is running
pnpm --filter dokploy-vite dev:client # → :5173
The /api proxy rewrites the origin header to :3000 so better-auth's trusted-origin check passes in split mode.
Production: pnpm --filter dokploy-vite build && pnpm --filter dokploy-vite start — one process serving API + websockets + static SPA on :3000, no Next.js at runtime.
The server loads .env from apps/vite first, then falls back to apps/dokploy/.env.
Dropped vs Next (known gaps)
- SSR prefetching (
createServerSideHelpers) — the SPA fetches on mount instead. - Per-page IS_CLOUD / role / permission SSR redirects beyond session checks — components and tRPC procedures still enforce them; route-level guards can be added incrementally in
beforeLoad.