DeployWise
HomeGuidesDeploy Vite to VPS
Build ToolStatic HostingIntermediate

Deploy Vite App to VPS: React, Vue, Svelte Guide 2026

Master the complete process of deploying Vite-built applications (React, Vue, Svelte) to a VPS with Nginx static hosting, proper SPA routing, gzip compression, and production-grade caching. This guide covers everything from build optimization to common mistakes and automating with DeployWise.

10 min read
Updated February 28, 2026

Understanding Vite Build Output (dist/ Directory)

When you build a Vite application, it generates optimized static files in the dist/ directory. Understanding this output structure is crucial for proper VPS deployment.

Typical Vite Build Output

bash
dist/
├── index.html           # Entry HTML file (always served for SPA)
├── assets/              # Hashed asset files (CSS, JS, images)
│   ├── main-a1b2c3d4.js    # Main JavaScript bundle (hashed)
│   ├── main-e5f6g7h8.css   # CSS bundle (hashed)
│   ├── vendor-i9j0k1l2.js  # Vendor bundle (hashed)
│   └── logo-m3n4o5p6.png   # Images (hashed)
├── favicon.ico          # Favicon
└── robots.txt           # SEO robots file

The key characteristics of Vite's build output:

  • Hashed filenames - Assets like main-a1b2c3d4.js include content hashes for cache busting
  • Single index.html - All routes served through this file for SPAs
  • assets/ directory - Contains all bundled and optimized JavaScript and CSS
  • Zero runtime dependencies - Vite outputs pure static files, no server needed

The hashed filenames enable aggressive caching - old versions never conflict with new ones.

Building for Production (npm run build)

Before deploying to your VPS, build your Vite application for production. This command optimizes your code, minifies assets, and generates the deployment-ready dist/ directory.

Running the Build

bash
npm run build

This command reads your Vite config and generates optimized static files. For most projects, this is all you need.

Typical vite.config.js

javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    minify: 'terser',
    sourcemap: false,
  },
})

Key Build Options

  • outDir: Output directory name, default 'dist'
  • assetsDir: Subdirectory for assets, default 'assets'
  • minify: Minification method - 'terser' (default), 'esbuild', or false
  • sourcemap: Generate source maps for debugging. Set to false for production
  • rollupOptions: Advanced Rollup configuration for code splitting and optimization

Previewing the Build Locally

bash
# Build and preview locally
npm run build
npm run preview

# The preview server shows exactly how your app will look on production

Nginx Configuration for Static Sites

Nginx serves your Vite-built static files with proper caching, compression, and security headers. This configuration handles the entire request flow from your domain to the VPS.

Step 1: Install Nginx

bash
# Ubuntu/Debian
sudo apt update
sudo apt install nginx

# Start and enable Nginx
sudo systemctl start nginx
sudo systemctl enable nginx

Step 2: Create Nginx Configuration

Create /etc/nginx/sites-available/vite-app.conf:

nginx
server {
  listen 80;
  server_name yourdomain.com www.yourdomain.com;

  # Redirect HTTP to HTTPS
  return 301 https://$server_name$request_uri;
}

server {
  listen 443 ssl http2;
  server_name yourdomain.com www.yourdomain.com;

  # Root directory - path to your dist folder
  root /var/www/vite-app/dist;
  index index.html;

  # SSL certificates (set up with Certbot)
  ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

  # SSL configuration
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;

  # Security headers
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
  add_header X-Frame-Options "SAMEORIGIN" always;
  add_header X-Content-Type-Options "nosniff" always;
  add_header X-XSS-Protection "1; mode=block" always;

  # Gzip compression
  gzip on;
  gzip_types text/plain text/css text/javascript application/json application/javascript;
  gzip_min_length 1000;
  gzip_vary on;

  # SPA routing - redirect 404s to index.html
  location / {
    try_files $uri $uri/ /index.html;
  }

  # Cache assets with hash for long periods
  location ~* /assets/ {
    expires 30d;
    add_header Cache-Control "public, immutable";
    access_log off;
  }

  # Cache other static files
  location ~* .(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
    expires 7d;
    add_header Cache-Control "public, must-revalidate";
    access_log off;
  }

  # Don't cache HTML
  location ~* .html$ {
    expires -1;
    add_header Cache-Control "public, must-revalidate";
  }
}

Step 3: Enable Configuration

bash
# Create symlink to enable the config
sudo ln -s /etc/nginx/sites-available/vite-app.conf /etc/nginx/sites-enabled/

# Test Nginx configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

SPA Routing with try_files

Single Page Applications (React, Vue, Svelte) handle their own routing on the client side. The critical Nginx directive for this is try_files.

How try_files Works

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

# This directive tells Nginx:
# 1. Try to serve the requested file ($uri)
# 2. If not found, try as a directory ($uri/)
# 3. If still not found, serve index.html

Without this configuration, navigating to /products/123 would result in a 404 error because Nginx would look for a physical file at that path. With try_files, Nginx serves index.html, allowing your SPA router to handle the route.

Why This Matters

Without try_files:

  • • User navigates to /products/123
  • • Nginx looks for file at /products/123
  • • File doesn't exist, returns 404 error
  • App breaks on page refresh

With try_files:

  • • User navigates to /products/123
  • • Nginx looks for file at /products/123
  • • File doesn't exist, serves /index.html instead
  • • JavaScript router processes the URL and shows correct page
  • App works perfectly on refresh

Gzip Compression and Performance

Gzip compression dramatically reduces file sizes, improving load times. Most modern browsers support it, and Nginx can compress content on-the-fly with minimal CPU overhead.

Nginx Gzip Configuration

nginx
# In server block or http block
gzip on;
gzip_vary on;  # Add Vary header
gzip_proxied any;
gzip_comp_level 6;  # 1-9, balance of speed vs compression
gzip_types text/plain text/css text/javascript application/json application/javascript;
gzip_min_length 1000;  # Don't compress files smaller than 1KB

# Don't gzip already compressed files
gzip_disable msie6;

Performance Impact

Uncompressed

  • • JavaScript bundle: ~250 KB
  • • CSS bundle: ~80 KB
  • • Total: ~330 KB

With Gzip

  • • JavaScript bundle: ~70 KB
  • • CSS bundle: ~20 KB
  • • Total: ~90 KB (73% reduction)

For a typical Vite React app, gzip compression reduces the initial payload by 60-80%. This has a massive impact on page load time, especially on slower connections.

Cache Headers for Assets

Vite's hashed filenames enable aggressive caching. When you deploy a new version, hashes change, so browsers fetch new files. Old files can stay cached forever.

Cache Strategy

nginx
# Vite generates files like: main-a1b2c3d4.js
# Since filename changes when content changes, cache aggressively
location ~* /assets/ {
  expires 30d;
  add_header Cache-Control "public, immutable";
}

# index.html should NOT be cached (or cached briefly)
# Because it references the asset filenames
location ~* .html$ {
  expires -1;
  add_header Cache-Control "public, must-revalidate";
}

# Other static files: cache for 7 days
location ~* .(jpg|jpeg|png|gif|ico|svg|woff|woff2)$ {
  expires 7d;
  add_header Cache-Control "public, must-revalidate";
}

How This Works

When You Deploy Version 1:

  • index.htmlmain-abc123.js
  • • Browser caches main-abc123.js for 30 days

When You Deploy Version 2:

  • • Code changes → content hash changes
  • index.htmlmain-xyz789.js (different hash)
  • • Browser fetches new file (different filename)
  • • Old main-abc123.js stays cached, never used

This ensures users always get the latest version while maximizing cache efficiency.

Custom Base Path Configuration

If you're deploying your Vite app to a subdirectory (e.g., yourdomain.com/myapp instead of a root domain), you need to configure the base path.

Scenario: Subdirectory Deployment

If serving from yourdomain.com/myapp:

javascript
// vite.config.js
export default defineConfig({
  base: '/myapp/',  // Must end with /
  plugins: [react()],
})

The base option ensures all asset references are relative to your subdirectory. Without it, the app looks for assets at the root domain and breaks.

Nginx Configuration for Subdirectory

nginx
server {
  listen 443 ssl http2;
  server_name yourdomain.com;

  root /var/www/myapp;

  # Main app subdirectory
  location /myapp/ {
    alias /var/www/myapp/dist/;
    try_files $uri $uri/ /index.html;
  }

  # Other app endpoints (optional)
  location /api/ {
    proxy_pass http://backend-server:3000;
  }
}

Always use trailing slashes for base paths: /myapp/ not /myapp

Environment Variables in Vite

Vite exposes environment variables through import.meta.env. Variables prefixed with VITE_ are available in the browser; others are server-only.

Setting Up Environment Variables

bash
# .env (used in all environments)
VITE_API_URL=https://api.example.com
DATABASE_URL=postgres://...  # Not exposed to browser

# .env.production (used only in production)
VITE_API_URL=https://api-prod.example.com
VITE_ANALYTICS_KEY=abc123xyz

# .env.development (used in dev)
VITE_API_URL=http://localhost:3000
VITE_DEBUG=true

Using Variables in Code

javascript
// Only VITE_* variables are available to browser JavaScript
const apiUrl = import.meta.env.VITE_API_URL;
const isProduction = import.meta.env.PROD;  // true in production build
const isDevelopment = import.meta.env.DEV;   // true in development

// This will be undefined - not exposed to browser
console.log(import.meta.env.DATABASE_URL);  // undefined

export function fetchData() {
  return fetch(`${apiUrl}/data`);
}

For VPS Deployment

bash
# On your VPS, create .env.production file
# in your project directory before building

VITE_API_URL=https://yourdomain.com/api
VITE_APP_NAME=Production App

# Build with production environment
npm run build  # Automatically uses .env.production

# Files are baked into the build - they're not runtime configurable
# If you need to change them, rebuild with new .env values

Environment variables are baked into the build. Never commit sensitive keys to version control.

Common Mistakes and How to Avoid Them

Mistake 1: SPA 404 on Page Refresh (Missing try_files)

Without proper Nginx configuration, users get 404 when refreshing a page with a client-side route.

nginx
# ❌ WRONG - No try_files
location / {
  root /var/www/vite-app/dist;
  index index.html;
}

# ✅ CORRECT - Redirect 404s to index.html
location / {
  root /var/www/vite-app/dist;
  try_files $uri $uri/ /index.html;
}

Mistake 2: Caching index.html Forever

If you cache index.html with Cache-Control: max-age=31536000, users won't see updates for a year!

nginx
# ❌ WRONG - HTML cached too long
add_header Cache-Control "public, max-age=31536000";

# ✅ CORRECT - HTML not cached (or cached briefly)
add_header Cache-Control "public, must-revalidate";

# Why? Because index.html changes on every build
# It references asset files with new hashes
# If HTML is cached, users won't fetch the new asset references

Mistake 3: Wrong Base Path or Missing Trailing Slash

Incorrect base path configuration breaks asset loading and routing.

javascript
// ❌ WRONG - Missing trailing slash
export default defineConfig({
  base: '/myapp',  // Will cause issues
})

// ✅ CORRECT - Trailing slash included
export default defineConfig({
  base: '/myapp/',
})

// If deployed at root:
// ✅ CORRECT - Empty string or /
export default defineConfig({
  base: '/',
})

Mistake 4: Serving Wrong Directory

Point Nginx to your build output directory, not the project root.

bash
# ❌ WRONG - Points to project root
root /var/www/vite-app;

# ✅ CORRECT - Points to built output
root /var/www/vite-app/dist;

# Directory structure:
# /var/www/vite-app/
#   ├── src/
#   ├── dist/          ← Nginx should point here
#   │   ├── index.html
#   │   ├── assets/
#   │   └── ...
#   ├── vite.config.js
#   └── package.json

Mistake 5: Environment Variables Not Baked Into Build

Building locally with development .env, then deploying to production won't use production environment variables.

bash
# ❌ WRONG - Build locally with dev env
npm run build  # Uses .env and .env.development

# Then upload dist/ to VPS
# Production variables are never used!

# ✅ CORRECT - Build on VPS or CI/CD with production env
# 1. Create .env.production on VPS
VITE_API_URL=https://yourdomain.com/api

# 2. Build on VPS with production variables
NODE_ENV=production npm run build

# 3. Upload built dist/ folder
# Variables are now baked into the build

Mistake 6: Forgetting to Disable SourceMaps in Production

Source maps expose your original source code to anyone who downloads them. Disable them in production.

javascript
// ❌ WRONG - Source maps included in production
export default defineConfig({
  build: {
    sourcemap: true,
  },
})

// ✅ CORRECT - Disable source maps for production
export default defineConfig({
  build: {
    sourcemap: false,
  },
})

// Or only enable them for staging/internal use
export default defineConfig({
  build: {
    sourcemap: process.env.NODE_ENV === 'staging',
  },
})

Automating with DeployWise

DeployWise automates the entire Vite deployment process, from building your app to configuring Nginx with proper caching, compression, and SPA routing. Deploy in minutes instead of hours.

What DeployWise Handles Automatically

Detects Vite projects automatically
Builds for production with optimizations
Configures Nginx for static hosting
Sets up SPA routing (try_files)
Enables gzip compression
Configures intelligent caching
Handles environment variables
Installs SSL certificates
Manages deployments and rollbacks
Monitors application health

Quick Start with DeployWise

bash
# 1. Sign up and connect your VPS to DeployWise
# 2. Create a new project and select your Git repository
# 3. DeployWise detects your Vite project
# 4. Configure deployment settings:
#    - Domain (yourdomain.com)
#    - Environment variables (VITE_API_URL, etc.)
#    - Build command (default: npm run build)
# 5. Deploy with a single click

# That's it! DeployWise handles:
# - Building your Vite project
# - Uploading to VPS
# - Configuring Nginx (with try_files for SPA)
# - Setting up gzip and caching
# - Installing SSL certificates
# - Monitoring and logs

DeployWise Benefits for Vite

  • Zero configuration - handles all Vite-specific setup
  • Intelligent caching based on hashed filenames
  • One-click deployments with rollback capability
  • Automatic gzip compression and optimization
  • Git integration with automatic deployments on push
  • Real-time logs and performance monitoring

Ready to Deploy Your Vite App?

Skip the manual Nginx configuration and deploy your Vite application to VPS in minutes with DeployWise. Perfect for React, Vue, Svelte, and other Vite-based projects.

Get Started with DeployWise

Quick Reference: Deployment Checklist

  • Build project locally with npm run build
  • Preview build with npm run preview
  • Upload dist/ directory to VPS
  • Install and configure Nginx
  • Add try_files for SPA routing
  • Enable gzip compression
  • Configure cache headers
  • Install SSL certificate with Certbot
  • Test SPA routing and asset loading
  • Monitor logs and performance

Related Deployment Guides