Technology Encyclopedia Home >How to Deploy a Vue.js App on a Cloud Server — Build, Serve, and Go Live

How to Deploy a Vue.js App on a Cloud Server — Build, Serve, and Go Live

Vue deployment caught me off guard the first time. I expected it to be complicated. It's actually pretty simple: run npm run build, you get a dist folder, and Nginx serves those files. No Node.js process needed in production.

The one thing that trips people up — and it got me too — is the publicPath setting when the app isn't served from the root URL, and client-side routing when users navigate directly to a deep URL. If history mode is on in Vue Router and Nginx doesn't know about it, you'll get 404 errors on page refresh.

Both issues have clean solutions. This guide covers the full deployment: Vite build, Nginx config with the routing fix, HTTPS, and automated deploys via GitHub Actions.uting support, and automated GitHub Actions deployments.

I run this on Tencent Cloud Lighthouse. A Vue SPA with Nginx needs minimal server resources — even the entry-level plan handles significant traffic since Nginx efficiently serves static files with very low overhead. For projects where the Vue frontend connects to a backend API on the same server, Lighthouse's single fixed monthly pricing covers both the frontend and backend — no additional infrastructure cost. If your app serves an international audience, Tencent Cloud EdgeOne can sit in front to deliver assets from edge nodes closer to your users.


Table of Contents

  1. How Vue Deployment Works
  2. Prerequisites
  3. Part 1 — Build Your Vue App
  4. Part 2 — Set Up the Server
  5. Part 3 — Upload the Build
  6. Part 4 — Configure Nginx
  7. Part 5 — Fix Vue Router with HTML5 History Mode
  8. Part 6 — Enable HTTPS
  9. Part 7 — Automate Deployments
  10. Part 8 — Environment Variables in Vue
  11. The Gotcha: publicPath and Subdirectory Deployment

Key Takeaways

  • Vue production build is static files — Nginx serves them directly, no Node.js at runtime
  • Add try_files $uri $uri/ /index.html; to fix Vue Router 404 on direct page access
  • VITE_ prefix is required for environment variables to be included in the build bundle
  • Set base: '/subpath/' in vite.config.js if not served from domain root
  • GitHub Actions automates build and deployment on every push to main

Frequently Asked Questions {#faq}

Does Vue.js need Node.js running in production?
Not for a standard Vite or Vue CLI production build. npm run build creates static files that Nginx serves directly. Node.js is only needed during the build process, not at runtime.

How do I fix Vue Router navigation errors returning 404?
Add try_files $uri $uri/ /index.html; to your Nginx location block. This routes all non-file requests to index.html, letting Vue Router handle navigation on the client side.

What is the publicPath / base setting in Vue and when do I need it?
If your app isn't served from the root URL (e.g., domain.com/app/), set base: '/app/' in vite.config.js. Without this, asset URLs will be broken.

How do I handle API calls in Vue when the frontend and backend are on different servers?
Configure a proxy in vite.config.js for development. For production, either serve the API from the same domain using Nginx location /api/ proxy rules, or configure CORS on your backend API server.

How do environment variables work in Vue/Vite?
Create .env.production with variables prefixed VITE_. They're baked into the build bundle at compile time. Access them as import.meta.env.VITE_API_URL in your code.

How Vue Deployment Works {#how}

Local development     Production server
npm run dev     →     npm run build → /dist
(dev server)          (static files) → Nginx serves /dist

The npm run build command (via Vite or Vue CLI) compiles your Vue components, optimizes assets, and outputs static files. Nginx serves these files — there's no Node.js runtime needed in production.

Vue Router in HTML5 History mode requires one server configuration step: all routes must fall back to index.html so Vue Router can handle them client-side.


Prerequisites {#prerequisites}

Requirement Notes
Node.js locally For building the app
Cloud server Tencent Cloud Lighthouse Ubuntu 22.04
Nginx on server sudo apt install nginx
Vue 3 project Vite-based (created with npm create vue@latest)

Part 1 — Build Your Vue App {#part-1}

On your local machine:

# Install dependencies
npm install

# Build for production
npm run build

# Output is in the /dist directory
ls dist/
# index.html  assets/  favicon.ico

Vite automatically:

  • Minifies HTML, CSS, and JavaScript
  • Adds content hashes to filenames (for cache busting)
  • Tree-shakes unused code
  • Splits code into chunks

Part 2 — Set Up the Server {#part-2}

ssh ubuntu@YOUR_SERVER_IP

sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx

sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable

# Create web root
sudo mkdir -p /var/www/myvueapp
sudo chown ubuntu:www-data /var/www/myvueapp
sudo chmod 755 /var/www/myvueapp

Part 3 — Upload the Build {#part-3}

SCP (simple)

# On your local machine
scp -r dist/* ubuntu@YOUR_SERVER_IP:/var/www/myvueapp/

rsync (better for updates)

rsync -avz --delete dist/ ubuntu@YOUR_SERVER_IP:/var/www/myvueapp/
# --delete removes old files that no longer exist

Verify the upload:

# On the server
ls /var/www/myvueapp/
# index.html  assets/

Part 4 — Configure Nginx {#part-4}

sudo nano /etc/nginx/sites-available/myvueapp
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    root /var/www/myvueapp;
    index index.html;

    access_log /var/log/nginx/myvueapp_access.log;
    error_log  /var/log/nginx/myvueapp_error.log;

    # All routes fall back to index.html (required for Vue Router)
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Cache hashed assets aggressively (Vite adds hashes to filenames)
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml;
    gzip_vary on;
}
sudo ln -s /etc/nginx/sites-available/myvueapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Visit http://YOUR_SERVER_IP — your Vue app should load.


Part 5 — Fix Vue Router with HTML5 History Mode {#part-5}

If you use Vue Router with createWebHistory() (HTML5 History mode), you need the try_files fallback. This is already included in the Nginx config above.

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        { path: '/', component: HomeView },
        { path: '/about', component: AboutView },
        { path: '/user/:id', component: UserView },
    ]
})

With try_files $uri $uri/ /index.html:

  • / → serves index.html → Vue Router handles it
  • /about → no file found → falls back to index.html → Vue Router handles /about
  • /assets/main.js → actual file exists → served directly

If you use createWebHashHistory() instead (hash-based routing like /#/about), you don't need the fallback — but hash URLs are less clean and not recommended for new projects.


Part 6 — Enable HTTPS {#part-6}

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Part 7 — Automate Deployments {#part-7}

Create .github/workflows/deploy.yml in your Vue project:

name: Deploy Vue App

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install and build
        run: npm ci && npm run build
        env:
          VITE_API_URL: ${{ secrets.VITE_API_URL }}

      - name: Deploy to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ubuntu
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "dist/"
          target: "/var/www/myvueapp"
          strip_components: 1

Add to GitHub repo secrets:

  • SERVER_IP — your server's public IP
  • SSH_PRIVATE_KEY — your private key for SSH access
  • VITE_API_URL — your API URL (if needed)

Part 8 — Environment Variables in Vue {#part-8}

Vue (Vite) environment variables are embedded at build time:

# .env.production (used during `npm run build`)
VITE_API_URL=https://api.yourdomain.com
VITE_APP_TITLE=My Vue App

Access in your Vue code:

const apiUrl = import.meta.env.VITE_API_URL
const title = import.meta.env.VITE_APP_TITLE

Important rules:

  • Only variables prefixed with VITE_ are exposed in the browser
  • These values are embedded in the built JavaScript — don't put secrets here
  • Run npm run build after changing .env.production for changes to take effect
  • In GitHub Actions, set these as secrets and pass via env: in the build step

The Gotcha: publicPath and Subdirectory Deployment {#gotcha}

If you deploy your Vue app to a subdirectory (e.g., yourdomain.com/app/ instead of the root), the asset paths in the built files will be wrong — they'll point to /assets/... instead of /app/assets/....

Fix in vite.config.js:

export default defineConfig({
    base: '/app/',  // Set to your subdirectory path
    // ...
})

And update Nginx to serve from that path:

location /app/ {
    root /var/www/myvueapp;
    index index.html;
    try_files $uri $uri/ /app/index.html;
}

For root deployments (the most common case), leave base as the default ('/') and don't change the Nginx config.

Troubleshooting {#troubleshooting}

Issue Likely Cause Fix
Connection refused Service not running or wrong port Check systemctl status SERVICE and verify firewall rules
Permission denied Wrong file ownership or permissions Check file ownership with ls -la and use chown/chmod to fix
502 Bad Gateway Backend service not running Restart the backend service; check logs with journalctl -u SERVICE
SSL certificate error Certificate expired or domain mismatch Run sudo certbot renew and verify domain DNS points to server IP
Service not starting Config error or missing dependency Check logs with journalctl -u SERVICE -n 50 for specific error
Out of disk space Logs or data accumulation Run df -h to identify usage; clean logs or attach CBS storage
High memory usage Too many processes or memory leak Check with htop; consider upgrading instance plan if consistently high
Firewall blocking traffic Port not open in UFW or Lighthouse console Open port in Lighthouse console firewall AND sudo ufw allow PORT

Deploy your Vue app today:
👉 Tencent Cloud Lighthouse — Fast Ubuntu VPS for Vue apps
👉 View current pricing and promotions
👉 Explore all active deals and offers