Last Updated Mar 5, 2025

Proxying Vite HMR for Nuxt 3 via nginx

Categories: Frontend Development

Tags: #Vite , #Nuxt , #nginx

It's been a while since I started a tiny toolbox project and I finally picked it back up recently. I just called it Frontend Stuff.

It's meant to be dumb/simple tools that I find myself needing for weird use cases.

It started simple...

Originally, I was just doing the normal yarn dev and opening localhost:3000 in my browser. So it worked perfectly.

But being the stubborn pro-HTTPS-and-hostname-in-local-dev guy that I am, I decided to set up an nginx config for it called festuff.local.

Here's where I landed at first:

# lives in /etc/nginx/sites-available/festuff.local
# symlinked to /etc/nginx/sites-enabled/festuff.local
server {
  server_name festuff.local www.festuff.local;

  add_header X-Frame-Options              "SAMEORIGIN";
  add_header X-XSS-Protection             "1; mode=block";
  add_header X-Content-Type-Options       "nosniff";

  charset utf-8;
  
  location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-NginX-Proxy true;

    proxy_pass https://127.0.0.1:3000;
    proxy_redirect off;
  }

  listen 443 ssl;
  ssl_certificate /var/www/festuff.local/.config/local/festuff.local.pem;
  ssl_certificate_key /var/www/festuff.local/.config/local/festuff.local-key.pem;
}
server {
  if ($host = www.festuff.local) {
    return 301 https://$host$request_uri;
  }

  if ($host = festuff.local) {
    return 301 https://$host$request_uri;
  }

  server_name festuff.local www.festuff.local;
  listen 80;
  return 404;
}

This is basically how I've had my Nuxt config up until now (abstractions/.env stuff removed/inlined):

export default defineNuxtConfig({
  vite: {
    server: {
      hmr: {
        clientPort: '3000',
        host: 'festuff.local',
      },
    },
  },

  devServer: {
    host: '127.0.0.1',
    https: {
      cert: '.config/local/festuff.local.pem',
      key: '.config/local/festuff.local-key.pem',
    },
  },
})

Oh yeah and, if you're curious, I'm using mkcert for local dev certs. It's so nice and simple to use.

Boom goes the websocket

I'm not sure if I just didn't notice at first (and I wasn't working on this regularly), but Vite's websocket connection broke once I started using nginx instead of opening localhost:3000 directly in my browser. I tested again today and, sure enough, the old way still worked.

Luckily, the solution wasn't too far away. I just had to look through a couple of Stack Overflow posts like this one and a GitHub issue conversation to figure out what I needed to do.

The fix is in

Since this is a Nuxt 3 project, Vite sets up a websocket that listens for connections at /_nuxt/ and its client connects there.

In my nginx config, I added basically the same code block from that SO answer, but for the root /_nuxt/ websocket route itself instead of changing where Vite was looking:

server {
# `location /` block above
	
  location /_nuxt/ {
    proxy_pass https://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }
	
# `listen 443 ssl;` line below
}
# `server {}` forwarding block below for redirecting http to https...

The last thing was to tell the Vite client to connect on port 443 directly:

// in nuxt.config.ts
export default defineNuxtConfig({
	// nuxt module config stuff up here...
	
  vite: {
    server: {
      hmr: {
        clientPort: '443',
        // other client config...
      },
    },
 },

  // dev server config stuff...
})

After doing all of that I restarted nuxt and it was working! No more console errors about not connecting to the websocket, and hot-module reloading was happening on save.

Please confirm whether you would like to allow tracking cookies on this website, in accordance with its privacy policy.