Back to Blog
Tutorial9 min read2026-07-02

How to Deploy a Vue.js SPA

A practical, end-to-end guide to deploying a Vue.js single-page app: production builds, SPA routing fallbacks, caching headers, environment variables, and custom domains with automatic SSL.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Deploying a Vue.js SPA the right way

A Vue single-page application (SPA) is, at the end of the build, just a folder of static files: an index.html, a hashed JavaScript bundle, CSS, and assets. That sounds trivial to host — and it is, until you hit the three things that bite every Vue developer in production: client-side routing 404s, stale caches, and environment variables that were baked in at the wrong time.

This tutorial walks through deploying a Vite-based Vue 3 app correctly, with the gotchas spelled out. The same approach works for Vue 2 + Vue CLI with minor path changes.

Step 1: Produce a clean production build

With a Vite-scaffolded project (npm create vue@latest), the production build command is:

npm install
npm run build

This emits a dist/ directory. Inspect it before you deploy anything:

ls -la dist
# index.html
# assets/index-a1b2c3d4.js
# assets/index-e5f6g7h8.css

The hashed filenames matter. Vite fingerprints every asset so you can cache them forever and only index.html ever needs to be revalidated. Hold that thought — it drives the caching strategy below.

If you use Vue CLI instead, the command is npm run build and the output folder is also dist/ by default.

Step 2: Fix SPA routing (the #1 production bug)

Vue Router in history mode produces clean URLs like /dashboard/settings. The problem: when a user refreshes that page or shares the link, the browser asks the server for /dashboard/settings — a file that does not exist on disk. Static hosts return a 404.

The fix is a SPA fallback: every unknown route must serve index.html, and Vue Router takes over on the client.

If you self-host with Nginx:

location / {
  try_files $uri $uri/ /index.html;
}

With Apache, a .htaccess rewrite:

RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

On a managed static host like PandaStack, SPA fallback is detected automatically for known frameworks — connect the repo, pick the static app type, and unknown routes resolve to index.html without you writing rewrite rules. If you ever need hash mode instead (createWebHashHistory), URLs become /#/dashboard and no fallback is required, but you lose clean URLs and slightly hurt SEO.

Step 3: Get caching headers right

The single biggest performance lever for a SPA is cache policy, and it follows directly from Vite's hashing:

AssetCache-ControlWhy
assets/*.js, assets/*.csspublic, max-age=31536000, immutableFilename changes on every content change
index.htmlno-cache (or short max-age)Entry point must always reflect the latest bundle
Images/fonts (hashed)public, max-age=31536000, immutableSame fingerprint guarantee

Get this backwards — caching index.html aggressively — and users will load an old HTML file that references deleted, hashed bundles, producing white screens after every deploy. This is the most common "it works on my machine but broke for users" SPA incident.

Step 4: Handle environment variables correctly

Vite exposes env vars prefixed with VITE_ at build time, not runtime:

# .env.production
VITE_API_BASE_URL=https://api.example.com
const api = import.meta.env.VITE_API_BASE_URL

Key implication: these values are compiled into the static bundle. You cannot change them after the build without rebuilding, and you must never put secrets here — anything in VITE_* is shipped to the browser in plain text. Use it only for public configuration (API base URLs, feature flags, public keys). Real secrets belong in your backend.

On a CI/CD platform, set VITE_* variables in the build environment so each deploy bakes in the right config. PandaStack injects environment variables into the build job, so your npm run build sees them and the produced bundle is environment-correct.

Step 5: Deploy

If you are doing it by hand, you rsync dist/ to a server or aws s3 sync dist/ s3://bucket plus a CloudFront invalidation. That works but you own the SPA fallback, the cache headers, the SSL cert renewals, and the CDN.

The git-push path looks like this:

git add .
git commit -m "feat: production build"
git push origin main

Connect the repository to PandaStack as a static site, and it auto-detects Vite, runs npm install && npm run build, serves dist/ from a multi-region CDN behind Kong ingress with Cloudflare DNS, applies the SPA fallback, and issues an automatic SSL certificate for your custom domain. Static builds run inside microVMs on pandastack.ai. You override the install command (npm/yarn/pnpm/bun) if your project needs it.

Step 6: Add a custom domain and SSL

Point a CNAME (or apex ALIAS/flattened record) at your host and let it provision the certificate. Verify HTTPS is enforced and HTTP redirects to HTTPS — Vue apps frequently load resources over mixed protocols if you forget this.

curl -I https://app.example.com
# HTTP/2 200
# cache-control: no-cache

Common pitfalls checklist

  • White screen after deployindex.html was cached; set it to no-cache.
  • 404 on refresh of a sub-route → missing SPA fallback to index.html.
  • API calls hit the wrong environmentVITE_* baked at build with the wrong values.
  • Assets 404 under a subpath → set Vite base in vite.config.js if you deploy under /app/ instead of root.
  • Leaked secret → you put a private key in a VITE_ variable; rotate it now.

References

  • [Vite — Building for Production](https://vite.dev/guide/build.html)
  • [Vite — Env Variables and Modes](https://vite.dev/guide/env-and-mode.html)
  • [Vue Router — Different History Modes](https://router.vuejs.org/guide/essentials/history-mode.html)
  • [MDN — Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)

If you would rather not hand-write rewrite rules and cache policies, PandaStack's free tier hosts static sites with automatic SPA fallback, immutable asset caching, and free SSL out of the box. Spin one up at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also