Skip to main content

Command Palette

Search for a command to run...

Stop Vite Hot Reload Breaking in Docker

Learn: Stop Vite Hot Reload Breaking in Docker

Updated
5 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

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

Update your vite.config.js:

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

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)

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

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:

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:

ENV DOCKER_ENV=true

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

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

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

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:

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

Load in vite.config.js:

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

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:

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

For Linux, use bridge networks (shown above).

4. Health Checks

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:

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:

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

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:

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.