Stop Vite Hot Reload Breaking in Docker
Learn: Stop Vite Hot Reload Breaking in Docker
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
Solution 1: Explicit HMR Configuration (Recommended for 2026)
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.hostto 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.