One of the most common questions for new developers, is how to host their Node.js or React project for the world to see. After all, your PWA doesn’t do much good if it’s only accessible at localhost:3000!

The most common solution is using nginx with proxy_pass http://127.0.0.1:3000. There are tons of Stack Overflow questions and articles on the how and why of this.

However, I wanted to kick this up a notch and see if we could squeeze some extra performance and modernization out of nginx. So I decided 2 things — now that Http/3 has been standardized, let’s use that — as well as, using sockets for IPC (inter process communication). This engineering design choice minimizes reliance on verbose networking protocols in favor of lean and modern improvements.

First – nginx has only recently mainlined Http/3 (QUIC), with version 1.25 as of 2023, and it’s not built by default. Which means you must build from source and include the extra config parameters to enable that module. There are a few articles about how to patch older versions (1.16 or 1.19) with Google’s “quiche” library for QUIC support, but we’re looking to test the built-in support. Besides, some of the configuration directives have changed.

So, we follow the official nginx docs on configuration and build from source. Release notes for version 1.25.1 refer to an OpenSSL bug that was fixed, but it didn’t say if this was for the build process which requires an alternate library (I used BoringSSL).

Once you’ve ‘make install’ed, then nginx is an executable (on most systems, ./usr/local/nginx/sbin/nginx), but you can easily daemonize it with systemd, so you can more easily reload / restart the service. In my case, I didn’t bother with it for proof of concept and instead just used pidof and kill to quickly restart.

Now for the nginx.conf key directives. Took a little bit of testing to get this to work, as the last time I tinkered with this, it was using ‘http3’ versus ‘quic’. This is not the full config, only the relevant parts.

http {
    http3 on; #this should be default, but good to explicitly define

    server {
        listen 443 ssl;
        listen [::]443 ssl;
        listen 443 quic reuseport;
        listen [::]443 quic reuseport;

        # ssl_cert and key (via LetsEncrypt with domain challenge)

        ssl_protocols TLSv1.3; #required, minimum version for quic

        # ssl_ciphers - include chacha20, new QUIC cipher suite
        #ssl_prefer_server_ciphers on;

        location {
            add_header Alt-Svc 'h3=":443"; ma=86400';
            proxy_pass http://127.0.0.1:3000; #basic redirect
        }
    }
}

Pretty straightforward.

To test, you can use curl built with http3, or as most major browsers support it, just use that. Check your Developer Tools > Network tab. Under the list of resources, right click on header to show protocol. You may see http/1.1 for the first time(s); I had better luck with Firefox picking up h3 on the first try, versus chrome I had to hard refresh to get it to work. One of the things may be using the same ports for both ‘ssl’ and ‘quic’ in the listen directives; perhaps reordering or removing would help.

Regardless, we now have our basic nginx proxy server with Http/3. But, why stop there? Why not optimize the pipeline between your backend server and your proxy? Enter the socket.

First of all, don’t be confused by terminology — WebSocket and unix sockets are two completely different things. We’re focusing on using unix sockets, a faster way to handle IPC (inter-process communication) instead of relying on localhost / loopback networking (with it’s heavy TCP overhead!)

With Node.js, it’s pretty easy to create this via the net module by binding path (e.g. /tmp/yourAppSocket) to the net.server.listen.

However, my project was actually a Rails project. I wasn’t intricately familiar with the middleware Rails uses to host dev servers — I’ve always just ran rails s and let it do it’s thing, as it’s been for internal CRMs or development, never production. So a little looking, and we see the default Rails server is puma, which has it’s own executable linked in your /bin folder in a rails project. By using puma -b unix:///path/to/socket.sock, we can manually tell it to use the socket.

Then back to the nginx.conf to update that; in my case: proxy_pass http://unix:///var/run/puma.sock;

Voila. Rails backend application, via IPC unix socket, to an Http/3 loadbalancing server.

There are some small issues here. In Chrome, for example, you must have correct SSL setup in order for Http/3 to work. Using a self-signed cert, or an expired or mismatched domain name, will prevent the connection from upgrading. However, Firefox still allows h3 connection despite SSL warnings. In addition, the h3 Alt-Svc header is an upgrade; but how to get clients to use it natively first, without requiring a refresh? And of course, the host of TLAs (Three Letter Acronyms) and deeper configurations — such as QUIC v2, 0-RTT and handshake loss, using CHACHA20 for the header protection instead of default AES 128.

As web technology continues to change and improve, we must continue to grow and change with it!

More Articles