# Stop Vite Hot Reload Breaking in Docker

# Stop Vite Hot Reload Breaking in Docker: A Modern Tooling Guide

## Problem

You're running Vite inside Docker. Everything works fine until you save a file. The browser doesn't refresh. You check the console—nothing. You check the network tab—WebSocket connection failed. You restart the container. It works for 30 seconds. Then it breaks again.

This is the Vite hot module replacement (HMR) problem in Docker, and it's one of the most frustrating developer experience issues in 2025-2026 containerized development workflows.

## Cause

Vite's HMR system relies on WebSocket connections between your browser and the dev server. In Docker, three things go wrong:

**1. Network Isolation**
Docker containers have their own network namespace. When Vite runs inside a container, it binds to `localhost:5173` (or similar). Your browser, running on your host machine, tries to connect to `localhost:5173`—but that's the *host's* localhost, not the container's. The connection fails silently.

**2. Host Resolution**
Even if you use `docker run -p 5173:5173`, the HMR WebSocket still tries to connect to `localhost` by default. The browser sees `localhost` as the host machine, but Vite inside the container thinks `localhost` means the container itself. Asymmetric routing breaks the connection.

**3. CORS and Protocol Mismatch**
If you're running HTTPS on the host but HTTP in the container (or vice versa), browsers block the WebSocket upgrade. Mixed-content policies kill the connection before it even starts.

## Fix: Configuration Examples

### Solution 1: Explicit HMR Configuration (Recommended for 2026)

Update your `vite.config.js`:

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    host: '0.0.0.0', // Listen on all interfaces inside container
    port: 5173,
    hmr: {
      host: 'localhost', // Browser connects here
      port: 5173,
      protocol: 'ws'
    }
  }
})
```

**For remote development** (VPS, cloud VM):

```javascript
export default defineConfig({
  plugins: [react()],
  server: {
    host: '0.0.0.0',
    port: 5173,
    hmr: {
      host: process.env.VITE_HMR_HOST || 'localhost',
      port: parseInt(process.env.VITE_HMR_PORT || '5173'),
      protocol: process.env.VITE_HMR_PROTOCOL || 'ws'
    }
  }
})
```

### Solution 2: Docker Compose Setup (Production-Ready)

```yaml
version: '3.9'

services:
  app:
    build: .
    ports:
      - "5173:5173"
    environment:
      - VITE_HMR_HOST=localhost
      - VITE_HMR_PORT=5173
      - VITE_HMR_PROTOCOL=ws
    volumes:
      - .:/app
      - /app/node_modules
    command: npm run dev
    networks:
      - dev-network

networks:
  dev-network:
    driver: bridge
```

### Solution 3: Dockerfile Optimization

```dockerfile
FROM node:20-alpine

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci

# Copy source
COPY . .

# Expose Vite port
EXPOSE 5173

# Set HMR environment variables
ENV VITE_HMR_HOST=localhost
ENV VITE_HMR_PORT=5173
ENV VITE_HMR_PROTOCOL=ws

# Run dev server
CMD ["npm", "run", "dev"]
```

### Solution 4: Advanced—Dynamic Host Detection

For teams using multiple environments, create a `vite.config.js` that auto-detects:

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

const getHmrConfig = () => {
  const isDev = process.env.NODE_ENV !== 'production'
  
  if (!isDev) return false

  const inDocker = process.env.DOCKER_ENV === 'true'
  
  if (inDocker) {
    return {
      host: process.env.VITE_HMR_HOST || 'localhost',
      port: parseInt(process.env.VITE_HMR_PORT || '5173'),
      protocol: process.env.VITE_HMR_PROTOCOL || 'ws'
    }
  }
  
  // Local development
  return {
    host: 'localhost',
    port: 5173,
    protocol: 'ws'
  }
}

export default defineConfig({
  plugins: [react()],
  server: {
    host: '0.0.0.0',
    port: 5173,
    hmr: getHmrConfig()
  }
})
```

Add to your Dockerfile:

```dockerfile
ENV DOCKER_ENV=true
```

### Solution 5: Using Vite's Built-in Middleware (2026 Approach)

For monorepos and complex setups, use Vite's middleware mode:

```javascript
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    middlewareMode: true,
    hmr: {
      host: process.env.VITE_HMR_HOST || 'localhost',
      port: process.env.VITE_HMR_PORT || 5173
    }
  }
})
```

Then in your Express/Node server:

```javascript
import express from 'express'
import { createServer as createViteServer } from 'vite'

const app = express()

const vite = await createViteServer({
  server: { middlewareMode: true }
})

app.use(vite.middlewares)

app.listen(3000, () => {
  console.log('Server running on port 3000')
})
```

## Best Practices for 2026

### 1. **Environment Variable Strategy**
Always externalize HMR config:

```bash
# .env.docker
VITE_HMR_HOST=localhost
VITE_HMR_PORT=5173
VITE_HMR_PROTOCOL=ws
```

Load in `vite.config.js`:

```javascript
import { loadEnv } from 'vite'

export default defineConfig({
  server: {
    hmr: {
      host: loadEnv('', process.cwd()).VITE_HMR_HOST,
      port: loadEnv('', process.cwd()).VITE_HMR_PORT,
      protocol: loadEnv('', process.cwd()).VITE_HMR_PROTOCOL
    }
  }
})
```

### 2. **Volume Mounting Best Practices**

```yaml
volumes:
  - .:/app                    # Source code
  - /app/node_modules         # Don't sync node_modules
  - /app/.vite                # Cache directory
```

This prevents node_modules conflicts and speeds up HMR.

### 3. **Network Mode Considerations**

For Mac/Windows Docker Desktop:

```yaml
services:
  app:
    network_mode: "host"  # Simplifies networking on desktop
```

For Linux, use bridge networks (shown above).

### 4. **Health Checks**

```yaml
services:
  app:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5173"]
      interval: 10s
      timeout: 5s
      retries: 3
```

### 5. **Debugging HMR Issues**

Add this to your `vite.config.js`:

```javascript
server: {
  middlewareMode: false,
  hmr: {
    host: 'localhost',
    port: 5173,
    protocol: 'ws'
  },
  watch: {
    usePolling: true,  // For some Docker setups
    interval: 100
  }
}
```

Check browser console for WebSocket errors:

```javascript
// In your app entry point
if (import.meta.hot) {
  import.meta.hot.on('vite:beforeUpdate', () => {
    console.log('HMR update incoming...')
  })
}
```

### 6. **Production Readiness**

Never expose HMR in production:

```javascript
export default defineConfig({
  server: {
    hmr: process.env.NODE_ENV === 'development' ? {
      host: 'localhost',
      port: 5173
    } : false
  }
})
```

## Takeaway

Vite's HMR breaking in Docker isn't a Vite bug—it's a networking reality. The fix is straightforward: **explicitly configure HMR to use the host machine's address from the browser's perspective**.

The 2026 best practice is:
- Use environment variables for HMR config
- Set `host: '0.0.0.0'` inside the container
- Set `hmr.host` to the address the browser uses
- Mount volumes correctly to avoid file sync issues
- Test with health checks

For 90% of teams, Solution 2 (Docker Compose with environment variables) solves the problem completely. For complex setups, Solution 4 (dynamic detection) provides flexibility.

**One-liner fix for quick testing:**

```bash
docker run -p 5173:5173 -e VITE_HMR_HOST=localhost -e VITE_HMR_PORT=5173 your-image
```

Save this. Share it. Your team will thank you when HMR just works.
