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 — base doesn’t match where the app is served.
  • Old version after deploy — index.html cached; ensure no-cache on it.
  • API calls hit the wrong host — check VITE_API_BASE at build time.

For atomic releases and CI automation, the approach is identical to React — see deploying a React app with Nginx.

  • Vue
  • Nginx
  • Deployment