vite build produces dist/ — HTML, JS, CSS — there is no Node server in production. What varies is who serves those files.
Single node — the default
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.
Custom dashboard, single node
The official image contains the stock dashboard. Once you’ve customized yours (your own pages/plugins inapps/dashboard/), bake your build into your own image:
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:
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).
Static host / CDN — the alternative
For edge-served assets or an independent dashboard release cadence, deploydist/ to a static host on its own domain:
- HTTPS on both sides. The refresh cookie is issued with
SameSite=None; Securein production, and browsers dropSecurecookies over HTTP. There is no insecure-production mode. - Allowed Origins. Add the dashboard’s URL (e.g.
https://dashboard.mystore.com) to Settings → Allowed Origins in the Spree admin — the API rejects cross-origin requests until then. - SPA fallback. Every path must rewrite to
index.htmlso the router owns the URL (/* → /index.htmlon your host:_redirectson Netlify,vercel.jsonrewrites,try_files $uri /index.html;on nginx).
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.
Render
The Blueprint thatcreate-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:
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.
Any other static host
The same three ingredients, by hand: build withVITE_SPREE_API_URL set, upload dist/, configure the /* → /index.html rewrite. Then add the origin to Allowed Origins.
Checklist (cross-origin only)
Single-node deployments skip all of this — one origin needs none of it.-
VITE_SPREE_API_URLpoints at the production API (baked at build time — one build per environment) - HTTPS on the dashboard and the API
- Dashboard origin added to Settings → Allowed Origins
- All paths rewrite to
index.html - No secrets in any
VITE_variable

