Node-red behind reverse proxy with http-proxy-middleware

Hi.

I'm trying to put Node-red and Grafana behind a reverse proxy with nodejs express and http-proxy-middleware. My situation:

I have a 4G router that sits between a linux machine running Node-red and Grafana and the WAN net.
I portforward :8080 -> LAN_IP:9000.

Grafana listens on: 3000. http
Node-red listens on: 1882. http

My proxymiddleware.js file:

const express = require('express')
const { createProxyMiddleware } = require('http-proxy-middleware')

const applicationPort = 9000

const app = express()

// redirect for grafana
app.use('/grafana/', createProxyMiddleware({
    target: 'http://localhost:3000',
    changeOrigin: true
}))

// redirect for node-red dashboard
app.use('/dashboard/', createProxyMiddleware({
    target: 'http://localhost:1882',
    changeOrigin: true
}))

// redirect for node-red editor
app.use('/editor/', createProxyMiddleware({
   target: 'http://localhost:1882',
   changeOrigin: true,
   pathRewrite: {
        '^/editor': ''
    }
}));

app.listen(applicationPort, () => {
    console.log(`Reverse proxy listening on ${applicationPort}`)
})

Grafana.ini file:
root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/
server_from_sub_path = true

Node-red settings.js file:
httpAdminRoot: '/editor'
httpNodeRoot: '/dashboard'

This works like a charm with Grafana.
WAN:IP:8080/grafana

But with Node-red
WAN_IP:8080/editor or /dashboard
I get this error:
Error occurred while trying to proxy: WAN_IP:8080/

I'm specifically targeting 'http://localhost:1882'. Why is it complaining about WAN_IP?
Am I doing this wrong? Some online resources and AI told me to do it this way.

I have tried any number of different configurations of this.

Best regards
Steffen

Couple of things.

  1. You need to tell Node-RED to trust your proxy.
  2. You need to proxy websockets not just HTTP for Node-RED.
  3. Not sure why you would try to use a Node.js/ExpressJS tool for the proxy? Using something like NGINX, Caddy or HAProxy would be far more efficient and would let you do other things as well. They would also easily proxy websockets as well as properly cache static resources for efficiency.
  4. YOU DO NOT APPEAR TO HAVE USED TLS to encrypt the WAN side of things. As such, you are in real danger of being compromised and having your machine used by an attacker.

If you haven't correctly set up HTTPS and put a login on the editor, you will likely need to reset the machine.

2 Likes

In my setup i use Traefik for this.
Easy to setup, lightweight and with good and extensive documentation.
It does acme (certificates) with let’s encrypt on the fly.

1 Like

Seems a bit heavier to run as it prefers a containerised install (e.g. Docker)?

The advantage of the others is that they easily install on bare metal and are configured via simple config files.

I can see that Traefik might be better though if you had a server just running API microservices.

Incidentally:

  • NGINX and Caddy are web servers as well as reverse proxy servers. So if you are serving up static web resources in addition to microservices like Node-RED, these may be better suited. NGINX and I think Caddy both have dynamic capabilities as well as static servers. Web servers will cache all requests which can make a massive difference to client and server performance. Personally, I haven't recommended Apache web server for a long time, it is dated and resource hungry.
  • HAProxy is a pure network and reverse proxy server, it does not serve static web sites. But if you don't care about having a web server, it can be great. It is lightweight on the server and performs well.
  • Traefik is a dedicated API and microservices proxy, possibly best for more complex requirements, again where you don't need it to act as a web server as well.

Personally, I use NGINX simply because it is very widely used and supported so lots of information exists to draw on. There are good integrations with authentication services, I like that you can use Lua scripting for more advanced tasks. And I guess I am more used to working with web servers than anything else - having used Apache very extensively in the dim-and-distant past. :slight_smile:

My instance of Traefik runs in Docker without any problems, as it has for several years now.

Not everyone wants or needs the overheads of Docker. :wink:

Thanks for your answers.

I use nodejs for most of my use cases, so I want to continue using it, except if there is an advices against it, or if it isn't possible using nodejs.

  1. I have failed to find resources on how to tell Node-red to trust my proxy.
    I didn't even know that Node-red would be the wiser.
  2. I have tried setting ws: true in the createProxyMiddleware option.
  3. This is a closed net. No access to internet.
    I will probably use TLS down the road, but not yet. There is so far no rush.

My original reason for using reverse proxy is that I have an iframe pointing to grafana panels in a node-red dashboard 2.0. But when accessing the page from outside LAN it doesn't work.
But being able to access different services based on a path in the url I see several use cases for.

I might be a bit confused about this, so if if any can answer me that would be much appreciated.

app.use('/somePath', createProxyMiddleware({
    target: 'http://localhost:1882',
    changeOrigin: true,
    ws: true
}))

The server listens on :9000

If I am on the LAN side and enters the linuxmachine_ip:9000/somepath, will the proxy application forward my request to localhost:1882, or localhost:1882/somepath ?
I'm guessing the latter, since there is a rewrite option, like this:

pathRewrite: {
    '^/somepath': '/dashboard'
}

In the settings.js file for Node-red, it states that I have to add proxies in the OS as environment variables. Does this pertain to my use case?

In my mind this should work:

app.use('/somepath', createProxyMiddleware({
    target: 'http://localhost:1882',
    changeOrigin: true,
    ws: true,
    pathRewrite: {
        '^/somepath': '/dashboard'
    }
}))

I imagine that the middleware will forward me to localhost:1882/dashboard
no matter if I'm local or on wan.

I'm using your alternativ install of node-red. Probably doesn't matter though.

Best regards
Steffen

In settings.js

    /** If you need to set an http proxy to reach out, please set an environment variable
     * called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system.
     * For example - http_proxy=http://myproxy.com:8080
     * (Setting it here will have no effect)
     * You may also specify no_proxy (or NO_PROXY) to supply a comma separated
     * list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk
     */

Alternatively:

    /** The following property can be used to pass custom options to the Express.js
     * server used by Node-RED. For a full list of available options, refer
     * to http://expressjs.com/en/api.html#app.settings.table
     */
    httpServerOptions: {
        // http://expressjs.com/en/api.html#trust.proxy.options.table
        'trust proxy': '127.0.0.1/8, ::1/128', // true,  // true/false; or subnet(s) to trust; or custom function returning true/false. default=false
        'x-powered-by': false,
    },

Sorry, I have no idea on that. I won't use node.js/ExpressJS to do proxying. I can share NGINX settings.

Yes, this is a good use-case for proxies. But using a dedicate service is so much more efficient. I've never learned anything else.

Yes, indeed.

Some update:

Ended up using the http-proxy npm package.
Which is the package that http-proxy-middleware wraps around.

As mentioned by TotallyInformation you need to tell Node-red to trust the proxy.
In the settings.js file of the Node-red:

httpServerOptions: {
   trustProxy: true 
},

And in my case:

httpAdminRoot: '/editor',

if I want to hit the editor at IP:PORT/editor.

For Grafana you need to set a few things in the grafana.ini file, under [server].
I want to hit grafana on IP:PORT/grafana

[server]
...
root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/
serve_from_sub_path = true

And for Websockets to work I had to add, in grafana.ini under [live]:

[live]
...
allowed_origins = http://127.0.0.1:3000

And in the nodejs proxy.js file change Origin to http://127.0.0.1:3000

I want to hit Node-red dashboard with /dashboard and /spvv
Proxy file:

const http = require('http');
const httpProxy = require('http-proxy');
const url = require('url');
const PORT = 9000;

// Create proxy server
const proxy = httpProxy.createProxyServer({
    xfwd: true,
    changeOrigin: true,
    ws: true // I believe this is set by default, but I added it
});

// Force Origin-header, needed to make WebSocket work.
proxy.on('proxyReqWs', (proxyReq, req, socket, options) => {
    if (req.url.startsWith('/grafana')) {
        proxyReq.setHeader('Origin', 'http://127.0.0.1:3000');
    }
});

// HTTP-server
const server = http.createServer((req, res) => {
    const parsedUrl = url.parse(req.url);
    const pathname = parsedUrl.pathname;
    if (pathname.startsWith('/editor')) {
        proxy.web(req, res, { target: 'http://127.0.0.1:1882' });
    } else if (pathname === '/spvv' || pathname === '/spvv/') {
        // Redirect til dashboard
        res.writeHead(302, { Location: '/dashboard' });
        res.end();
    } else if (pathname.startsWith('/dashboard')) {
        proxy.web(req, res, { target: 'http://127.0.0.1:1882' });
    } else if (pathname.startsWith('/grafana')) {
        proxy.web(req, res, {
            target: 'http://127.0.0.1:3000',
            changeOrigin: true
        });
    } else {
        res.writeHead(404);
        res.end('Not found');
    }
});

// Start server
server.listen(PORT, () => {
    console.log(`Reverse proxy listening on http://localhost:${PORT}`);
});

// And now, to make WebSockets work
server.on('upgrade', (req, socket, head) => {
    const pathname = url.parse(req.url).pathname;

    if (pathname.startsWith('/grafana')) {
        proxy.ws(req, socket, head, {
            target: 'http://127.0.0.1:3000',
            changeOrigin: true
        });
    } else if (pathname.startsWith('/dashboard') || pathname.startsWith('/editor')) {
        proxy.ws(req, socket, head, {
            target: 'http://127.0.0.1:1882',
            changeOrigin: true
        });
    } else {
        socket.destroy();
    }
});

With this setup I'm able to hit Node-red dashboard on IP:PORT/dashboard and /spvv.
And hit Grafana on IP:PORT/grafana
And in my iframe in a dashboard 2.0 template the src:
src="/grafana/d-solo/adrc3fg6h105cc/something?orgId=2&from=simeTime&to=someOtherTime&theme=light&panelId=1"

Key points:

  1. Don't use localhost in "target".
    It will assume an ipv6 format address if not told otherwise. Use 127.0.0.1
  2. Tell Node-red to trust proxy.
  3. Set allowed_origins in grafana.ini and force an Origin in proxy file with proxyReqWs, not proxyReq.

And of course, depending on how you want to do it make sure that your proxy.js file fires at boot.
I typically use systemd.

Best regards
Steffen

I assume that you have adjusted Node-RED's systemd script to wait for your proxy? And set your proxy to wait on the network? systemd is good for ensuring the correct startup sequence.

I haven’t had the chance to make the systemd service yet. I tell all network-dependent applications to wait for network. I hadn’t considered your first point, but I’ll look into it. Thanks for bringing it up!

Best regards
Steffen