The problem: Running npm run dev inside Laravel Sail starts Vite fine, but the browser never hot-reloads — every change requires a manual full page refresh, and the console shows a failed WebSocket connection.
Environment: Laravel 11, Laravel Sail, Docker Desktop, Vite 5, Node 20 (containerized)
The Error
[vite] failed to connect to websocket.
your current setup:
(browser) localhost:5173/ <--[HTTP]--> localhost:5173/ (server)
(browser) localhost:5173/ <--[WebSocket (failing)]--> localhost:5173/ (server)
WebSocket connection to 'ws://localhost:5173/' failed
Why It Happens
Vite's dev server runs inside the Docker container, but your browser lives on the host machine. By default, Vite binds to localhost inside the container — which, from the container's own network namespace, is not the same localhost your browser is using. The initial HTTP request for assets often still works because Sail's Docker Compose config maps the port through, but the WebSocket connection Vite uses for HMR needs an extra explicit configuration to know what host/port to advertise back to the browser.
Two separate settings need to agree for this to work: what Vite binds to inside the container, and what it tells the browser to connect to for the HMR WebSocket (the "HMR client" config) — these aren't automatically the same thing in a Dockerized setup.
The Fix
Update vite.config.js to bind to all interfaces and explicitly set the HMR host the browser should use:
export default defineConfig({
plugins: [laravel(['resources/css/app.css', 'resources/js/app.js'])],
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
hmr: {
host: 'localhost',
},
},
});
Confirm Sail's docker-compose.yml actually exposes port 5173 (recent Sail installs include this, but older projects upgraded from Laravel 8/9 often don't):
laravel.test:
ports:
- '${APP_PORT:-80}:80'
- '${VITE_PORT:-5173}:5173'
Then restart Sail fully (not just the Vite process) so the port mapping takes effect:
./vendor/bin/sail down
./vendor/bin/sail up -d
./vendor/bin/sail npm run dev
Step-by-Step
- Open
vite.config.jsand add theserver.host: '0.0.0.0'andserver.hmr.host: 'localhost'settings shown above. - Open
docker-compose.ymland confirm the5173port mapping exists under your app service'sportslist. - Run
./vendor/bin/sail downthen./vendor/bin/sail up -dto fully recreate the container with the new port mapping. - Start Vite through Sail specifically:
./vendor/bin/sail npm run dev, not a locally-installed npm. - Open the browser console and confirm the WebSocket connects (no more "failed to connect" message).
- Edit a Blade or Vue file and confirm the change hot-reloads without a manual refresh.
Edge case: If you're accessing your Sail site through a custom domain (via /etc/hosts or Valet-style proxying) instead of plain localhost, the hmr.host value must match that custom domain exactly, not localhost — otherwise the browser tries to open a WebSocket to a host it can't resolve, and HMR fails silently with no clear error beyond the generic connection failure.
Still no hot-reload after checking ports and config? Reach out on the Contact page and I'll help debug your Docker setup.