DEV Community

Jervi
Jervi Subscriber

Posted on

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:

npm run build
php artisan serve --host 0.0.0.0
Enter fullscreen mode Exit fullscreen mode

from my blog: source

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:

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,
        },
    },
});
Enter fullscreen mode Exit fullscreen mode

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:

APP_URL=http://YOUR_VPS_IP:6040    # public ip of the vps
Enter fullscreen mode Exit fullscreen mode

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

➜  APP_URL: http://YOUR_VPS_IP:6040    # public ip of the vps
Enter fullscreen mode Exit fullscreen mode

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):

sudo ufw allow 5173
sudo ufw allow 6040
sudo ufw reload
Enter fullscreen mode Exit fullscreen mode

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):

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

# terminal 2 — Vite
npm run dev
Enter fullscreen mode Exit fullscreen mode

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.


Top comments (0)