Everything so far ran on your own computer with the development server. To let other people use your app, you build it and put the result on a web server. Because a Vite React app is a single-page application, the result is just static files, so hosting is cheap and simple. This lesson walks through the build, the settings that matter, and the common places to publish.
Build for production
npm run buildVite compiles JSX, bundles all your modules and the libraries you use into a few files, minifies them (removes spaces, shortens names), splits out lazily loaded parts, and puts the output in the dist folder. For a small app the output looks like this:
dist/index.html 0.45 kB │ gzip: 0.29 kB
dist/assets/index-nqMpL4T3.css 1.78 kB │ gzip: 0.81 kB
dist/assets/index-BKKMhUUY.js 219.65 kB │ gzip: 68.64 kB
✓ built in 73msThe file names contain a hash of their content. If the content changes, the name changes, so browsers can cache files "forever" and still fetch new versions after a deploy. gzip shows the size sent over the network when the server compresses responses. The folder contains:
dist/
├── index.html
├── favicon.svg
├── icons.svg
└── assets/
├── index-BKKMhUUY.js
└── index-nqMpL4T3.cssOnly the contents of dist go on the server. The src folder, node_modules and package.json are for building, not for serving. The production build has no development overlay, no hot reloading, and no StrictMode double-rendering, and React runs in its faster production mode.
Preview it before you deploy
npm run previewThis serves dist locally (the address is printed, normally http://localhost:4173/). Always look at the production build once: a few bugs appear only there (a missing environment variable, a wrong path, an import that works in development but not after bundling). When I previewed a build with a client-side router, /, /courses and a deep URL all answered with the app, as vite preview already rewrites unknown paths to index.html; your real server must do the same, as described below.
Check the size
A fast site is a small one. Look at the numbers in the build output: JavaScript above a few hundred kilobytes (gzipped) deserves attention. Ways to reduce it: split routes with React.lazy (see the Suspense lesson), avoid importing whole libraries for one function (import debounce from 'lodash/debounce', not all of lodash), compress images, and analyse the bundle with a visualiser plugin such as rollup-plugin-visualizer. Vite warns about chunks larger than 500 kB.
Environment variables
Different environments need different settings: the address of the API in development and in production, a feature flag, an analytics id. Vite reads .env files in the project root:
VITE_API_URL=https://api.example.comand exposes variables whose names start with VITE_ on import.meta.env:
export default function Env() {
return <p>API: {import.meta.env.VITE_API_URL}, mode: {import.meta.env.MODE}, prod: {String(import.meta.env.PROD)}</p>
}Some built-in values are always there: import.meta.env.MODE ("development" or "production"), DEV and PROD (booleans), and BASE_URL. You can have several files: .env.development, .env.production and .env.local (not committed to Git) override one another by mode.
Environment variables are public
This is the point that surprises people. The values are copied into the JavaScript at build time. I built the project above with two variables in .env:
VITE_API_URL=https://api.example.com
SECRET_KEY=do-not-leakand searched the output. The built index-*.js file contained https://api.example.com, exactly once, and the string do-not-leak did not appear (0 matches), because Vite ignores variables without the prefix. But anything with the VITE_ prefix is readable by anyone who opens the browser's developer tools. So:
- Use
VITE_variables only for non-secret configuration (public URLs, public keys designed for browsers, flags). - Never put a database password, a payment secret key or a private API token in React code, whatever the name. If your app needs one, make a small server (or serverless function) that holds the secret and exposes only the operation you need; React calls your server.
- Do not commit
.env.localor files with real values; commit an.env.examplewith fake placeholders. - Environment values are fixed at build time. To change one you rebuild.
The base path
By default the built index.html refers to its files from the root: /assets/index-....js. If the app lives in a sub-folder such as https://example.com/shop/, those paths are wrong. Set base in vite.config.js:
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [react()],
base: '/shop/',
})After rebuilding, the generated page uses the prefix:
<script type="module" crossorigin src="/shop/assets/index-BKKMhUUY.js"></script>
<link rel="stylesheet" crossorigin href="/shop/assets/index-nqMpL4T3.css">For a client-side router, also tell it the same base (<BrowserRouter basename="/shop">; in the Vite code, use import.meta.env.BASE_URL). GitHub Pages project sites (https://user.github.io/repo/) need exactly this.
Making client-side routes work on the server
As the routing lesson explained, with BrowserRouter the server only has index.html, so a direct visit or a refresh on /courses/react would return 404. The server must answer every URL that is not an existing file with index.html. How:
Apache (shared hosting, cPanel): put a .htaccess file next to index.html (add it to the public folder of your project so that the build copies it into dist):
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]Nginx:
location / {
try_files $uri /index.html;
}Netlify: a file public/_redirects containing /* /index.html 200. Vercel: a vercel.json with a rewrite of /(.*) to /index.html. Firebase Hosting: "rewrites": [{ "source": "**", "destination": "/index.html" }]. If you cannot configure rewrites at all, HashRouter puts the route after a # (/#/courses), which the server never sees; the URLs look less nice but need no server setup.
Also set caching headers wisely: files in assets/ have hashed names, so they can be cached for a year; index.html must not be cached for long, or visitors will miss new releases.
Where to deploy
Because the output is static, you have many options:
| Option | How it works | Good for |
|---|---|---|
| Shared hosting / cPanel | upload the contents of dist into public_html (or a sub-folder) with the file manager, FTP or Git |
sites on a hosting plan you already pay for |
| Netlify, Vercel, Cloudflare Pages | connect the Git repository; they run npm run build and publish dist on every push, with preview URLs for branches |
the easiest workflow, with a free tier |
| GitHub Pages | publish dist to a branch or with a workflow; set base for project pages |
free hosting for small projects |
| A VPS / your own Nginx or Apache | copy dist to the web root; configure rewrites and HTTPS |
full control |
| A CDN bucket (S3 + CloudFront, Firebase) | upload files to storage behind a CDN | high traffic, global speed |
For the shared-hosting route, the steps are: run npm run build; open dist; upload everything inside it (not the folder itself) to the web root; add the rewrite rule; visit the site and a deep link to test. The article on deploying from Git to shared hosting shows a repeatable Git-based way to do it, including keeping secrets out of the repository.
A deployment checklist
npm run lintandnpm testpass.npm run buildcompletes with no errors, and the sizes look reasonable.npm run preview: click through the main flows, reload on a deep URL.- Environment variables for production are set (no secrets in
VITE_ones). basematches where the site is hosted.- The server rewrites unknown paths to
index.htmland serves HTTPS. - Update
<title>, favicon and meta description inindex.html. - After deploying, open the live site in a private window, check the console for errors, and test on a phone.
- Keep the previous build around (or use Git tags) so you can roll back quickly.
Limits of a single-page app
Search engines and social previews see the initial HTML, which is nearly empty until JavaScript runs. Google does run JavaScript, but other crawlers and link previews often do not, and a blank first paint can be slow on weak devices. If search traffic or fast first load matters (shops, blogs, marketing pages), look at server rendering or static generation with a framework such as Next.js, or React Router in framework mode. Everything you have learned in this course (components, state, hooks) carries over unchanged.
Common mistakes
- Uploading the
distfolder itself instead of its contents, so URLs gain an extra/dist/. - Forgetting the rewrite rule, then getting 404 errors on refresh.
- Putting secrets in
VITE_variables. - Deploying without a
basesetting when the site is not at the domain root, leaving a blank page and 404s for the JavaScript file. - Not testing the production build locally.
- Caching
index.htmlaggressively, so users keep an old version. - Editing code in
distor on the server instead of rebuilding.
Exercise
- Add a
.envwithVITE_APP_NAME=My App, show it inApp, build, and confirm that the value appears in the built JavaScript file (searchdist/assets). - Set
base: '/practice/', rebuild, and compare the paths indist/index.html. Then servedistfrom a sub-folder on any local web server to prove it works. - Write the
.htaccessrule for your router app, deploy it to any host (or a local Apache), and test refreshing on a deep URL. - Connect a Git repository to Netlify, Vercel or Cloudflare Pages and watch the automatic deployment after a push.
Key points
- npm run build compiles, bundles and minifies your app into the dist folder: plain static files that any web server can host.
- Variables in .env files that start with VITE_ are copied into the code and are public; never put secrets in a React app.
- Set base in vite.config.js when the site lives in a sub-folder, and configure the server to return index.html for unknown paths so client-side routes work on refresh.
- Preview the build locally with npm run preview, then upload the contents of dist to your host or let a service build it from Git.