A Vue 3 app built with Vite is a set of static files. Nginx serves them efficiently; the only Vue-specific requirement is the fallback for Vue Router’s history mode, so deep links and page refreshes don’t 404.
(Using Nuxt with server-side rendering? See deploying Nuxt on a VPS.)
1. Build
npm ci
npm run build # vite build → dist/
Environment variables are build-time
Vite inlines variables prefixed with VITE_ into the bundle when you build:
# .env.production
VITE_API_BASE=https://example.com/api
Changing them requires a rebuild. Everything in the bundle is public — never put secrets in VITE_ variables. If you need one build for several environments, load a small config.json at startup instead.
Base path
Serving from a subdirectory (e.g. https://example.com/app/)? Set it in vite.config.ts and in the router:
// vite.config.ts
export default defineConfig({ base: '/app/' });
// router
createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes });
2. Upload
rsync -avz --delete dist/ [email protected]:/var/www/vue-app/
3. Nginx
server {
listen 80;
server_name app.example.com;
root /var/www/vue-app;
index index.html;
# Hashed assets from Vite
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# History mode: unknown paths → index.html
location / {
try_files $uri $uri/ /index.html;
}
location = /index.html {
add_header Cache-Control "no-cache";
}
}
try_files … /index.html serves the app shell for routes like /users/42; Vue Router then renders the right view. Note that missing pages now return 200 — define a catch-all route in Vue Router to show a “not found” view:
{ path: '/:pathMatch(.*)*', component: NotFound }
For a subdirectory deployment:
location /app/ {
alias /var/www/vue-app/;
try_files $uri $uri/ /app/index.html;
}
4. Proxy the API (no CORS needed)
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
The Vue app calls /api/... on the same origin, so the browser doesn’t need CORS. If the API lives on another domain, configure CORS there — see CORS in Nginx.
5. HTTPS and compression
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d app.example.com
Enable gzip/Brotli for JS and CSS — see Nginx compression.
Troubleshooting
- 404 when refreshing a route — missing
try_files … /index.html. - Blank page, assets 404 —
basedoesn’t match where the app is served. - Old version after deploy —
index.htmlcached; ensureno-cacheon it. - API calls hit the wrong host — check
VITE_API_BASEat build time.
For atomic releases and CI automation, the approach is identical to React — see deploying a React app with Nginx.