NGINX Reverse Proxy Multiple NodeJS Apps On Same Domain

I've recently setup an Ubuntu Server to host several NodeJS applications internally for our company. The applications all reside at the same domain (alpha.domain.com), but on different ports. The applications are served with ExpressJS (as they also act as an API).

I'm trying to setup NGINX to reverse proxy these ExpressJS/NodeJS applications but am struggling hard. I've followed every tutorial I can find but they don't seem solve my problem, or I am clearly not understanding what I am doing. I'm a front-end developer filling in for our dev-ops guy who recently left the company.

My server is at: alpha.domain.com (internal DNS forwards to static IP server)

My NGINX config is currently:

server {
        listen 80 default_server;
        server_name alpha.domain.com;
        return 301 https://$host$request_uri;
}

server {
        listen 443 ssl default_server;

        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers HIGH:!aNULL:!MD5;

        server_name alpha.domain.com www.alpha.domain.com;

        location /pnl {
                proxy_pass https://localhost:5000/;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection 'upgrade';
                proxy_set_header Host $host;
                proxy_cache_bypass $http_upgrade;
        }
}

Each application is a ReactJS application that will be served with ExpressJS/PM2. I want NGINX to only reverse proxy these urls in such a way that:

  • alpha.domain.com/pnl loads to https://localhost:5000
  • alpha.domain.com/2d loads to https://localhost:5001
  • etc...

If I change the location in the above server block to simply /, then the application at https://localhost:5000 works fine. Having it at /pnl causes all of my static assets (from Create-React-App build) to 404.

ExpressJS is (trimmed non-important bits):

let app = express()

const KEY = fs.readFileSync("path_to_key")
const CERT = fs.readFileSync("path_to_cert")
const PORT = 5000

app.use(express.static("build"))

// handle SPA application routing (routing done client side)
app.get("/*", function (_, res) {
  res.sendFile(path.join(__dirname, "build/index.html"), (err) => {
    if (err) res.status(500).send(err)
  })
})

const httpsServer = https.createServer({ key: KEY, cert: CERT }, app)
httpsServer.listen(PORT, () => {
  console.log(`App listening at https://alpha.domain.com:${PORT}`)
})

Here is my file structure:

├── build
│   ├── asset-manifest.json
│   ├── favicon.ico
│   ├── index.html
│   ├── manifest.json
│   ├── precache-manifest.12d9913d64add6e70d8bdc1e6f5fa0aa.js
│   ├── service-worker.js
│   └── static
│       ├── css
│       │   ├── 2.d26be9bd.chunk.css
│       │   └── main.86152b9d.chunk.css
│       ├── js
│       │   ├── 2.d769608a.chunk.js
│       │   ├── 2.d769608a.chunk.js.LICENSE.txt
│       │   ├── main.b32be59d.chunk.js
│       │   └── runtime~main.afeea630.js
│       └── media
│           └── logo.4d542fd5.svg
├── index.js
├── package-lock.json
└── package.json

Any guidance on how to solve this problem?

- EDIT -

Here is the contents of the index.html which is generated by ReactJS.

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8"/>
        <link rel="shortcut icon" href="/favicon.ico"/>
        <meta name="viewport" content="width=device-width,initial-scale=1"/>
        <meta name="theme-color" content="#000000"/>
        <link rel="manifest" href="/manifest.json"/>
        <title>Part Number Log</title>
        <link href="/static/css/2.d26be9bd.chunk.css" rel="stylesheet">
        <link href="/static/css/main.86152b9d.chunk.css" rel="stylesheet">
    </head>
    <body>
        <noscript>You need to enable JavaScript to run this app.</noscript>
        <div id="root"></div>
        <script src="/static/js/runtime~main.afeea630.js"></script>
        <script src="/static/js/2.d769608a.chunk.js"></script>
        <script src="/static/js/main.b32be59d.chunk.js"></script>
    </body>
</html>

With your config

        location /pnl {
            proxy_pass https://localhost:5000/;

/pnl is removed from the URL and replaced by /. Try

        location /pnl/static/ {
            alias /home/user/pnl-server/build/static
            try_files $uri $uri/ =404;
        }

        location /pnl {
            proxy_pass https://localhost:5000;

nginx proxy_pass directive