How to configure ngnix, vite and laravel for use with HMR via a reverse proxy?

6.9k views Asked by At

I've only just got into Laravel/Vue3 so I'm working off the basics. However, I have an existing Docker ecosystem that I use for local dev and an nginx reverse proxy to keep my many projects separate.

I'm having trouble getting HMR working and even more trouble finding appropriate documentation on how to configure Vite and Nginx so I can have a single HTTPS entry point in nginx and proxy back to Laravel and Vite.

The build is based on https://github.com/laravel-presets/inertia/tree/boilerplate.

For completeness, this is the package.json, just in case it changes:

{
    "private": true,
    "scripts": {
        "dev": "vite",
        "build": "vite build"
    },
    "devDependencies": {
        "@vitejs/plugin-vue": "^2.3.1",
        "@vue/compiler-sfc": "^3.2.33",
        "autoprefixer": "^10.4.5",
        "postcss": "^8.4.12",
        "tailwindcss": "^3.0.24",
        "vite": "^2.9.5",
        "vite-plugin-laravel": "^0.2.0-beta.10"
    },
    "dependencies": {
        "vue": "^3.2.31",
        "@inertiajs/inertia": "^0.11.0",
        "@inertiajs/inertia-vue3": "^0.6.0"
    }
}

To keep things simple, I'm going to try and get it working under HTTP only and deal with HTTPS later.

Because I'm running the dev server in a container, I've set server.host to 0.0.0.0 in vite.config.ts and server.hmr.clientPort to 80. This will allow connections to the dev server from outside the container and hopefully realize that the public port is 80, instead of the default 3000.

I've tried setting the DEV_SERVER_URL to be the same as the APP_URL so that all traffic from the public site goes to the same place. But I'm not sure what the nginx side of things should look like.

I've also tried setting the DEV_SERVER_URL to be http://0.0.0.0:3000/ so I can see what traffic is trying to be generated. This almost works, but is grossly wrong. It fails when it comes to ws://0.0.0.0/ communications, and would not be appropriate when it comes to HTTPS.

I have noticed calls to /__vite_plugin, which I'm going to assume is the default ping_url that would normally be set in config/vite.php.

Looking for guidance on which nginx locations for should forward to the Laravel port and which locations should forward to the Vite port, and what that should look like so that web socket communications is also catered for.

I've seen discussions that Vite 3 may make this setup easier, but I'd like to deal with what is available right now.

1

There are 1 answers

0
Reuben On

The answer appears to be in knowing which directories to proxy to Vite and being able to isolate the web socket used for HMR.

To that end, you will want to do the following:

  • Ensure that your .env APP_URL and DEV_SERVER_URL match.
  • In your vite.config.ts, ensure that the server.host is '0.0.0.0' so that connections can be accepted from outside of the container.
  • In your vite.config.ts specify a base such as '/app/' so that all HMR traffic can be isolated and redirected to the Vite server while you are running npm run dev. You may wish to use something else if that path may clash with real paths in your Laravel or Vite app, like /_dev/ or /_vite'.
  • In your config/vite.php set a value for ping_url as http://localhost:3000. This allows Laravel to ping the Vite server locally so that the manifest should not be used and the Vite server will be used. This also assumes that ping_before_using_manifest is set to true.
  • Lastly, you want to configure your nginx proxy so that a number of locations are specifically proxied to the Vite server, and the rest goes to the Laravel server.

I am not an Nginx expert, so there may be a way to declare the following succinctly.

Sample Nginx server entry

# Some standard proxy variables 
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
  default $http_x_forwarded_proto;
  ''      $scheme;
}

map $http_x_forwarded_port $proxy_x_forwarded_port {
  default $http_x_forwarded_port;
  ''      $server_port;
}

map $http_upgrade $proxy_connection {
  default upgrade;
  '' close;
}

map $scheme $proxy_x_forwarded_ssl {
  default off;
  https off;
}

server {
  listen *:80;
  server vite-inertia-vue-app.test;
  
  /* abridged version that does not include gzip_types, resolver, *_log and other headers */

  location ^~ /resources/ {
    proxy_pass            http://198.18.0.1:3000;
    include               /etc/nginx/vite-inertia-vue-app.test.include;
  }

  location ^~ /@vite {
    proxy_pass            http://198.18.0.1:3000;
    include               /etc/nginx/vite-inertia-vue-app.test.include;
  }

  location ^~ /app/ {
    proxy_pass            http://198.18.0.1:3000;
    include               /etc/nginx/vite-inertia-vue-app.test.include;
  }

  location / {
    proxy_pass            http://198.18.0.1:8082;
    include               /etc/nginx/vite-inertia-vue-app.test.include;
  }     
}  

vite-inertia-vue-app.test.include to include common proxy settings

proxy_read_timeout    190;
proxy_connect_timeout 3;
proxy_redirect        off;
proxy_http_version    1.1;
proxy_set_header      Host $host;
proxy_set_header      Upgrade $http_upgrade;
proxy_set_header      Connection $proxy_connection;
proxy_set_header      X-Real-IP $remote_addr;
proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header      X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header      X-Forwarded-Ssl $proxy_x_forwarded_ssl;
proxy_set_header      X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header      Proxy "";

My Nginx instance runs in a local Docker Swarm and I use a loopback interface (198.18.0.1) to hit open ports on my machine. Your mileage may vary. Port 3000 is for the Vite server. Port 9082 is for the Laravel server.

At some point, I may investigate using the hostname as it is declared in the docker-compose stack, though I'm not too sure how well this holds up when communicating between Docker Swarm and a regular container stack. The point would be not to have to allocate unique ports for the Laravel and Vite servers if I ended up running multiple projects at the same time.

Entry points /@vite and /resources are for when the app initially launches, and these are used by the script and link tags in the header. After that, all HMR activities use /app/.

The next challenge will be adding a self-signed cert, as I plan to integrate some Azure B2C sign in, but I think that may just involve updating the Nginx config to cater for TLS and update APP_URL and DEV_SERVER_URL in the .env to match.