DEV Community

mohit
mohit

Posted on

Deploy a Vite + React App to Shared Hosting (2026 Guide)

Most guides for this are built on create-react-app, which the React team deprecated on 14 February 2025 — the announcement recommends migrating to a framework, or to a build tool like Vite, Parcel or Rsbuild. CRA has no active maintainers.

So the homepage field in package.json that those guides tell you to set does not exist in Vite. Neither does the build/ folder. If you followed one and ended up with a 403 or a white page, that is why.

Here is the current path, plus the three failures that fill the comment sections of every older guide.

1. Get the base path right before you build

This is the step that decides whether your app loads at all.

Vite injects asset paths into index.html at build time using the base option. If base is wrong, the HTML loads and then every script and stylesheet 404s — which renders as a completely blank page with no error on screen.

Deploying to a domain root (example.com) — the default is already correct:

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  base: '/',
})
Enter fullscreen mode Exit fullscreen mode

Deploying to a subdirectory (example.com/app/) — you must say so:

export default defineConfig({
  plugins: [react()],
  base: '/app/',
})
Enter fullscreen mode Exit fullscreen mode

Leading and trailing slashes both matter. /app will not work; /app/ will.

2. Build

npm run build
Enter fullscreen mode Exit fullscreen mode

Vite writes to dist/, not build/. That difference causes more failed deploys than anything else on this page, because people follow a CRA guide and go looking for a folder that was never created.

Check it locally before you upload anything:

npm run preview
Enter fullscreen mode Exit fullscreen mode

That serves the real production build the way a static host will. If it is broken here, uploading will not fix it.

3. Upload the CONTENTS of dist/, not the folder

This is the single most common cause of a 403 or a blank page on shared hosting, and it is almost never explained.

Your files need to land like this:

public_html/
├── index.html
├── assets/
│   ├── index-a1b2c3d4.js
│   └── index-e5f6g7h8.css
└── vite.svg
Enter fullscreen mode Exit fullscreen mode

Not like this:

public_html/
└── dist/
    ├── index.html
    └── assets/
Enter fullscreen mode Exit fullscreen mode

In the second case Apache finds no index.html at the document root. Depending on whether directory listing is enabled, you get either a 403 Forbidden or an empty page — and both look like a server problem when they are a path problem.

If you zip before uploading, open dist/ first and zip the files inside it, not the folder. In your host's file manager, extract into public_html, then confirm index.html sits directly there. SFTP works equally well; just drag the contents.

4. Add .htaccess for client-side routing

Without this, your app works until someone refreshes on /about or opens a deep link, and then the server returns 404. The server is looking for a real file at that path. There isn't one — React Router resolves it in the browser.

Create .htaccess in public_html:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>
Enter fullscreen mode Exit fullscreen mode

The two RewriteCond lines are the important part: they say "if the request is not a real file and not a real directory, hand it to index.html". Without them you would rewrite your JS and CSS requests into the HTML too, and get MIME type errors in the console instead.

If you deployed to a subdirectory, change RewriteBase / and the final /index.html to match — /app/ and /app/index.html.

LiteSpeed, which most budget shared hosts run instead of Apache, reads the same .htaccess directives, so this block works on both.

Troubleshooting, by symptom

403 Forbidden. Files are one directory too deep. Check that index.html is directly inside public_html, not inside public_html/dist. If the path is right and you still get 403, check the file permissions — 644 for files, 755 for directories.

Completely blank page, no errors on screen. Open DevTools and look at the Network tab. If the assets/*.js requests are 404ing, your base is wrong — rebuild with the correct value. If they load fine, check the Console for a runtime error instead; a blank render is usually a crash during mount.

Routes 404 on refresh, but work when clicking links. Missing or ignored .htaccess. Confirm the file uploaded — many FTP clients hide dotfiles by default, so it is easy to think you sent it when you didn't.

MIME type errors on .js. Usually a rewrite rule that is catching real files because the RewriteCond lines are missing.

"The project was built assuming it is hosted at /". This is a CRA message, not a Vite one, and when you are deploying to a domain root it is not an error — it is the build telling you it did the correct thing. Two people in the comments of the most popular guide on this topic have been stuck on that line for three years. If you see it, you are on CRA, and it is fine.

One security note before you ship

Vite only exposes environment variables prefixed with VITE_, and it inlines them into the bundle at build time:

const key = import.meta.env.VITE_API_URL
Enter fullscreen mode Exit fullscreen mode

That means anything with a VITE_ prefix is readable by anyone who opens your JavaScript. It is fine for a public API base URL. It is not fine for an API secret, a database credential, or a private key. If a value must stay secret, it belongs behind a server you control, not in a static bundle.

When shared hosting is the wrong answer

Worth saying plainly, because the happy path above does not cover everything.

Shared hosting serves static files. That is all a built SPA is, so it works well. What it cannot do:

  • Server-side rendering. Next.js with SSR, or any app that renders on the server, needs a Node process. Static hosting has none.
  • API routes. If your app needs a backend, you need somewhere to run it.
  • Background jobs, cron, WebSockets. Same problem.

If any of those apply, a VPS is the honest answer, and the specs that matter for a Node workload are different from the ones hosting companies advertise — I wrote up which ones actually matter here.

And if you are only serving a built SPA, do not overbuy. A static bundle needs very little; the entry tier of almost any host handles it, and the thing worth checking is the renewal rate rather than the intro price — I keep a breakdown of what the cheap plans actually cost here.

The short version

  1. Set base correctly before building — / for a root deploy, /subdir/ otherwise
  2. npm run build outputs to dist/, and npm run preview tests it locally
  3. Upload the contents of dist/, so index.html lands in public_html
  4. Add the .htaccess block, or every refresh on a sub-route 404s
  5. Never put a secret behind a VITE_ variable

That is the whole thing. Most of the pain people hit with this is step 3.


I'm a full stack developer from India. I write about hosting, performance and technical SEO at mohitkoli.in.

Top comments (0)