Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Deploying a Vue.js App to Production: The Build, the SPA Fallback, and the Parts the Docs Skip

Sean

Platform Writer

Sep 12, 2026
8 min read

Deploying a Vue.js app to production is npm run build, then serving the dist folder from a static host with a fallback rule that sends every unknown path to index.html. That is the whole recipe for a single-page app. The bugs people hit are the four things around it: the SPA fallback, the base path, environment variables that only exist at build time, and caching headers that fight the hashed filenames.

Deploying a Vue.js App to Production: The Build, the SPA Fallback, and the Parts the Docs Skip

The official guide covers the build. The platform guides cover the click path on a handful of hosts. What sits between them is the set of things that work on localhost and break on the first deploy, which is what this post is about.

Table of contents

What npm run build actually produces

With Vite, which is the default for any Vue project created in the last few years, the production build writes a dist directory containing an index.html, a handful of hashed JavaScript and CSS files under assets, and whatever was in public copied across untouched.

npm run build
npm run preview   # serves dist/ locally on port 4173, the closest thing to production you get on your machine

Two things follow from that. First, the output is static files. There is no Node process to run in production unless you are doing server-side rendering, which for Vue usually means Nuxt and is a different deployment shape entirely. Second, the filenames carry a content hash, so index.html is the only file whose name stays the same between deploys, and everything it references is unique to this build.

If your project still uses the older Vue CLI, the command is the same and the output is the same shape. The config file differs, which matters for the base path section below.

The SPA fallback is the bug you will hit first

Vue Router in history mode produces clean URLs like /about and /orders/42. On your machine the dev server knows to serve index.html for all of them. A production static host does not, so the home page works, navigation works, and the first time someone refreshes on /about or opens a shared link, they get a 404 from the host because there is no file called about.

The fix is a rewrite rule: any path that does not match a real file is served index.html, with a 200 status, and the router takes over from there. Each host spells it differently.

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

On RunxBuild it is a single toggle, described in the SPA fallback docs. Whatever the host, the test is the same: deploy, open a deep link in a private window, refresh. If that works, history mode is configured. If it does not, nothing else you do will matter to the person who clicked a shared link.

The alternative is hash mode, which puts the route after a # and never hits the server. It works everywhere with no configuration, and it looks like 2012. Use it only when you genuinely cannot control the host.

Base path, env vars and the build-time trap

If the app is served from a subpath rather than the root of a domain, the base option in vite.config tells the build where the assets live. Miss it and the HTML loads but every asset request 404s.

// vite.config.js
export default {
  base: '/app/',   // only if the site lives at example.com/app/
}

Environment variables are the bigger trap. Vite exposes variables prefixed with VITE_ through import.meta.env, and it does so at build time. The value is baked into the JavaScript bundle. Changing the variable on the server afterwards does nothing, because there is no server process reading it. The consequences:

  • Set the variables in the host’s build environment, not just in a local .env file that is not in the repository.
  • Never put a secret in a VITE_ variable. It ships to every browser. API keys that must stay private belong in a backend, not in the bundle.
  • A different API URL per environment means a different build per environment. Staging and production are two builds, not one build with two configs.

Caching headers and the hashed-asset contract

The content hashes exist so the browser can cache aggressively. The contract is: everything under assets is immutable and can be cached for a year, and index.html must never be cached, because it is the file that points at the current hashes.

Get it backwards and you get the classic post-deploy bug: users see the old index.html from cache, it requests asset files that no longer exist, and the site is blank until they hard-refresh. Set the headers explicitly rather than trusting the host’s defaults. On RunxBuild they go in the headers configuration for the site: a long max-age with immutable on the assets path, and no-cache on index.html.

Tracking runtime errors and the production checklist

Vue exposes a global error handler, and production is the place to use it. Without one, an exception in a component fails silently for the user and invisibly for you.

import { createApp } from 'vue'
const app = createApp(App)
app.config.errorHandler = (err, instance, info) => {
  // send err.message, info and the current route to wherever you collect errors
}

The rest of the checklist, in the order it saves you from something:

  1. Deep-link refresh works on a deployed URL, in a private window.
  2. Assets load over HTTPS with no mixed-content warnings. Hardcoded http:// URLs to an API are the usual cause.
  3. Build-time variables are set in the host and the production API URL is the one in the bundle. Check the network tab, not the config file.
  4. Source maps: either off in production, or uploaded to your error tracker and not served publicly.
  5. A rollback exists. The deploy that breaks will not be the one you were worried about.

Static site or a server: choosing before you deploy

A plain Vue SPA is a static site and should be hosted as one. It needs no runtime, it is served from a CDN, and the bandwidth is the only variable cost. The moment you need server-side rendering for SEO or first-paint, you are running Nuxt or an equivalent, which is a Node service with a process, memory and a plan size. Decide which you are before you pick the host, because the two are different products with different bills.

On RunxBuild the SPA path is a static site built from the repository: build command, output directory, the SPA fallback toggle, headers and redirects, custom domains with the certificate handled, and 120GB of bandwidth included with $0.10 per gigabyte after. The SSR path is a Node service on the general ladder from the $4 Dev plan, with build logs, runtime logs and rollback. The step-by-step Vue deployment guide walks the static path from an empty dashboard to a live domain.

How this fits the rest of the stack

The static build is nearly free to host, and the moment it stops being static the bill changes shape entirely. Before choosing, the RunxBuild hosting calculator shows both shapes side by side: bandwidth alone for the SPA, or a service plan, a database and storage for the rendered version, each as its own line item.

Useful related references:

FAQ

Which command builds a Vue app for production?

npm run build. With Vite it writes a dist directory containing index.html and hashed asset files. npm run preview serves that directory locally so you can check the production build before deploying it.

Why does my Vue app show a 404 when I refresh a page?

Vue Router in history mode produces paths like /about that do not exist as files on the server. The host needs a fallback rule that serves index.html for any unknown path. Add that rule, or switch the router to hash mode if you cannot configure the host.

Where should I host a Vue app?

A plain single-page Vue app is static files and belongs on a static host with a CDN and an SPA fallback. If you need server-side rendering, you are running Nuxt or similar, which needs a Node service. Pick the host after you know which of the two you are.

How do environment variables work in a production Vue build?

Variables prefixed with VITE_ are read at build time and baked into the bundle. Set them in the host’s build environment, never put secrets in them because they ship to the browser, and remember that a different value means a different build.

Do I need a server to run a Vue app in production?

Not for a standard single-page app. The build output is static files that any web server or static host can serve. You need a running server only for server-side rendering or for a backend API, and the API is a separate service from the Vue app either way.

#vue js deploy to production#vue production build#vite build#vue router history mode#deploy vue app