VueJS dev serve with reverse proxy

MrManafon
Homullus
Published in
6 min readSep 18, 2019

--

The article was updated with a VERY simplified setup, thanks to the magic science of looking at source code. You can find it at the bottom.

I came upon a silly problem as i was setting up a new project for my teammates. The project is an open source weather/clothes up, and if that doesn’t explain much, just visit the repo or website: http://kaputtweather.com

VueJS setup

VueJS in 2019 has a pretty set up process, you use a cli command to bootstrap a new project in a particular folder, with all needed dependencies. Its cute, and it works really good, especially for younger developers that are not heavily into sysAdmin side of our jobs. :D

It literally spawns its own UI, and i’m not kidding here. Talk about useless overhead instead of putting efforts into quality.

Anyways, you just create a project and later run the ubiquitous yarn serveor yarn build. The problem is right there, you just don’t see it yet.

There is no watch…

Yeah, someone decided that nobody needs watch, and that if its going to exist, it will be wrapped in a bundle/server/watcher/HMR client of their own making (because why would anyone want to run their own server, thats just silly, everything needs to be written in JS and also fuck standards). I guess there is a GitHub issue somewhere saying something along the lines of:

Boo. We don’t want to wait for 500ms polling rate like peasants, lets make our own server && our own watch system.

The serve command was made in such a way that it spawns Vue’s own local server on port 8080, and builds no visible files (all files are generated on runtime). If you want to reap the benefits of HMR or even auto-reload you will have to use their server. I have nothing against this, but if you’re going to limit us in such a way, you must create flexible command line flags, which they did not. This is stupid, as no two infrastructure setups are the same nor they have the same needs. But i guess thats what you get for using a noob friendly tool for a supposedly simple task.

Anyways, my setup required me to serve stuff over Nginx in both dev and prod, so i did the following:

  1. Split dev/prod server config targets in my Dockerfile. (will explain better in the next post) (tldr; use a slightly different .conf for nginx setup on dev.)
  2. Dev config would not use default / block, which usually points to the build/dist folder, but will instead define a proxy to the node server on the other container.

1. a simple Nginx Proxy

Ok, we can solve this, i said. We do request proxying all the time with backend micro-services. There is even a set of unwritten rules about it. This is a pretty common practice with backend technologies:

location / {
proxy_pass http://localhost:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect off;
proxy_connect_timeout 90s;
proxy_read_timeout 90s;
proxy_send_timeout 90s;
}

This will run my Nginx as it usually would, with all other servers and blocks intact, but will proxy all requests that hit this particular block through Nginx and to the Vue server found at localhost:8080. So from the viewpoint of Vue, it is indeed running on kaputtweather.com domain’s root.

You can find a similar example here:

2. Invalid Host header

Did not take me long to find out it won’t work. For some even stupider reason, Webpack-dev-server guys seem to have locked everyone into localhost usage (or prividing a serve cli flag) -.- ¿what the actual fuck?

location / {
proxy_pass http://localhost:8080/;
proxy_set_header Host localhost;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host localhost;
proxy_set_header X-Forwarded-Server localhost;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect off;
proxy_connect_timeout 90s;
proxy_read_timeout 90s;
proxy_send_timeout 90s;
}

What we usually do on backend is that our apps watch out for X-Forwarded-Host (and similar) headers that can be hardcoded in nginx. Just swap all the Host references and hardcode them to localhost. Vue server will start to think that we are using it in localhost domain, and will allow access. Yay.

3. Sockets and Hot Module Replacement

HMR did not work at first. At least it was trying to work. But it could not get the “Connection Upgrade” request to pass through to the NodeJS server. I noticed this by chance, in the Chrome network console. We just define a cool new little block specific to Sockets…

location /sockjs-node/ {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
# And now all the stuff from above, again. proxy_set_header Host localhost;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host localhost;
proxy_set_header X-Forwarded-Server localhost;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect off;
proxy_connect_timeout 90s;
proxy_read_timeout 90s;
proxy_send_timeout 90s;
}

Needless to say, this should only be used in dev environment, if you want to use it in prod, i would suggest properly learning Webpack configuration instead. If you need more info i would suggest this read:

A note about CORS & CORB on Nginx Proxy

In normal circumstances, the server itself would use the provided Origin for CORS/B. But since we are overriding the thing with localhost, we are getting back a CORS header that only allows requests to localhost domain.

proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin "http://kaputtweather.com";

By adding this to our server block, we are removing header that was received from the proxy, and replacing it with our own hardcoded domain name.

Host Header + HMR: UPDATE (28.09.2019)

I figured that something is wrong, as the setup was a bit wonky and we had quite a few dropped ajax requests. Also HMR did not work as expected (every second request would fail) which was a pain in the butt, but i hadn’t noticed at the time of writing.

I could smell some non-standard behaviour specific to JS ecosystem so i jumped into Webpack Dev server code. As you can observe, someone knew about the standard usage but didn’t really care to cover all use cases. They were checking only the Origin header (which btw is not usually used for this type of thing). This simplifies our setup quite a bit. If WebpackDevServer is not really looking at all the standard headers anyway, i can just kill them off:

location / {
proxy_pass http://kaputt_node:8080/;
proxy_set_header Host localhost;
proxy_set_header Origin localhost;
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin "http://kaputtweather.com";
}

location /sockjs-node/ {
proxy_pass http://kaputt_node:8080;
proxy_set_header Host localhost;
proxy_set_header Origin localhost;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin "http://kaputtweather.com";
}

Our server now still thinks it is run from localhost. Sockets do too.

You can see the full conf file on the GitHub repo:

--

--