Jervi
Back to all articles
laravelviteinertiadevops

I thought you couldn't run Vite HMR Laravel on a remote VPS. I was wrong.

So here's the thing. I've been building a Laravel + Inertia + React project and deploying it on a VPS. My workflow was:

bash
npm run build
php artisan serve --host 0.0.0.0

The problem

When you run npm run dev, Vite starts a dev server and a WebSocket for HMR. By default it binds to localhost — meaning only processes on the same machine can reach it. The browser, sitting on your laptop, can't connect.

But Vite has a server.host option that makes it bind to all interfaces, including the public one. And a server.hmr.host option that tells the browser where to connect for the WebSocket. That's the key piece most tutorials miss.


Step 1 — update vite.config.ts

Add a server block to your existing config. Everything else stays the same:

ts
import { wayfinder } from '@laravel/vite-plugin-wayfinder';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import laravel from 'laravel-vite-plugin';
import { defineConfig } from 'vite';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.tsx'],
            ssr: 'resources/js/ssr.tsx',
            refresh: true,
        }),
        react({
            babel: { plugins: ['babel-plugin-react-compiler'] },
        }),
        tailwindcss(),
        wayfinder({ formVariants: true }),
    ],
    esbuild: { jsx: 'automatic' },

    // 👇 add this block
    server: {
        host: '0.0.0.0',
        port: 5173,
        hmr: {
            host: 'YOUR_VPS_IP', // e.g. '123.123.123.123'
            port: 5173,
        },
    },
});

Why hmr.host matters: Without it, the browser tries to open the WebSocket on localhost — which is your laptop, not the VPS. It silently fails and you get no live updates.


Step 2 — update .env

The Laravel Vite plugin reads APP_URL to know where the backend is. Set it to your real VPS IP and port:

env
APP_URL=http://YOUR_VPS_IP:6040    # public ip of the vps

You'll know it's working when npm run dev shows:

➜  APP_URL: http://YOUR_VPS_IP:6040    # public ip of the vps

Step 3 — open the ports on your firewall

This is where I got stuck the longest. UFW alone isn't enough if you're on a cloud provider.

OS firewall (UFW):

bash
sudo ufw allow 5173
sudo ufw allow 6040
sudo ufw reload

Cloud firewall (Hetzner / DigitalOcean / AWS):

Most VPS providers have a separate cloud-level firewall that sits in front of your server. UFW never even sees the traffic. You need to open the port there too.

For Hetzner: go to console.hetzner.cloud → Firewalls → your firewall → Add inbound rule → TCP port 5173.

💡 Security tip: Instead of allowing 0.0.0.0/0, restrict port 5173 to your own IP only. The dev server has no auth — you don't want it public.


Step 4 — run both servers

Open two terminal sessions on your VPS (or use tmux):

bash
# terminal 1 — Laravel
php artisan serve --host 0.0.0.0 --port 6040

# terminal 2 — Vite
npm run dev

Summary

| What | Command | Port | |---|---|---| | Laravel backend | php artisan serve --host 0.0.0.0 | 6040 | | Vite HMR dev server | npm run dev | 5173 |

Edit the vite.config.json

Now open http://YOUR_VPS_IP:6040 in your browser, edit a React component, and watch it update instantly. No rebuild. No refresh. Full HMR on a remote VPS.