docs/developer/dashboard/deployment.mdx
The dashboard deploys as static files. vite build produces dist/ — HTML, JS, CSS — there is no Node server in production. What varies is who serves those files.
The Spree server serves the dashboard itself at /dashboard: one container, one origin, zero CORS or cookie configuration. The official Spree Docker image ships with the stock dashboard baked in — deploy the image anywhere Docker runs (Render, Railway, AWS, Azure, your own host) and https://your-api-host/dashboard just works.
Under the hood, the spree_dashboard gem (in the Gemfile of the starter and the official image) serves the directory in Spree::Dashboard.dist_path — or the SPREE_DASHBOARD_DIST_PATH env var — with SPA semantics: every dashboard route falls back to index.html, hashed assets get immutable caching. The bundle is built with a relative API base (VITE_SPREE_API_URL unset) and base: /dashboard/, so the same image works on every host and environment with no rebuild. API-only deployments simply omit the gem.
The official image contains the stock dashboard. Once you've customized yours (your own pages/plugins in apps/dashboard/), bake your build into your own image:
spree build --production # optionally --tag registry/repo:tag
The Dockerfile normalizes its own build context: built from the project root it detects the layout (backend/ = the Rails app, apps/dashboard/ = your dashboard) and builds your dashboard into the image — no flags, no named contexts. Without the CLI, the equivalent is:
docker build . -f backend/Dockerfile -t my-shop-spree:latest
Inside the dashboard stage the build runs with VITE_BASE_PATH=/dashboard/ (asset URLs resolve under the mount) and VITE_SPREE_API_URL unset (API calls stay origin-relative); the Node toolchain lives only in that throw-away stage. Projects without apps/dashboard/ get the stock dashboard baked instead — and if your backend/Dockerfile predates this layout support, the command warns instead of silently shipping the stock dashboard over your customized one (update it from the spree-starter template).
For edge-served assets or an independent dashboard release cadence, deploy dist/ to a static host on its own domain:
cd apps/dashboard
VITE_SPREE_API_URL=https://api.mystore.com pnpm build
This is the cross-origin topology, with three requirements:
SameSite=None; Secure in production, and browsers drop Secure cookies over HTTP. There is no insecure-production mode.https://dashboard.mystore.com) to Settings → Allowed Origins in the Spree admin — the API rejects cross-origin requests until then.index.html so the router owns the URL (/* → /index.html on your host: _redirects on Netlify, vercel.json rewrites, try_files $uri /index.html; on nginx).A middle ground also works: serve dist/ and proxy /api/* + /rails/* to Rails from one domain behind your own nginx/Caddy/ingress — same-origin simplicity without baking the bundle into the image.
The Blueprint that create-spree-app places at the project root deploys the backend as a Docker service built from your repo — the same Dockerfile as everywhere else, with the repo root as context:
runtime: docker
dockerfilePath: ./backend/Dockerfile
dockerContext: .
That's the whole setup — one service, one origin, your customized dashboard (it builds from your apps/dashboard, not the stock bundle) at https://your-service.onrender.com/dashboard. No VITE_SPREE_API_URL, no Allowed Origins entry, no extra service, no build commands to maintain — migrations run from the image entrypoint on boot. Redeploys rebuild on every push, and what Render builds is byte-for-byte what spree build --production builds locally.
If you prefer the dashboard on Render's CDN instead, add a static-site service by hand using the cross-origin recipe above.
The same three ingredients, by hand: build with VITE_SPREE_API_URL set, upload dist/, configure the /* → /index.html rewrite. Then add the origin to Allowed Origins.
Single-node deployments skip all of this — one origin needs none of it.
VITE_SPREE_API_URL points at the production API (baked at build time — one build per environment)index.htmlVITE_ variable