nginxnginx-reverse-proxy

Trying to map folder-names to ports in nginx using a map $wanted_port $application


Essentially, trying to map localhost/10000 to localhost:10000 with nginx, while I was surprised the following did not bring down my nginx server;

# wildports
# https://localhost/10000/ -->https://localhost:10000
# https://localhost/9090/ --> https://localhost:9090
# https://localhost/8384/ --> https://localhost:8384
# wild applications
# https://webmin.soul -->     https://localhost:10000
# https://cockpit.soul -->    https://localhost:9090
# https://bluecherry.soul --> https://localhost:7001

map $wanted_port $application
{
    webmin 10000;
    cockpit 9090;
    bluecherry 7001;
    apache 8080;
    apaches 8443;
    dev 8443;
    local 8443;
    syncthing 8384;
    node 3000;
}
map $application $wanted_port
{
    10000 webmin;
    9090 cockpit;
    7001 bluecherry ;
    8080 apache ;
    8443 apaches ;
    8384 sytncthing ;
    3000 node ;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name  ~^(?<application>\.soul)$;
    set $wanted_port $application;
    include snippets/self-signed.conf;
    include snippets/ssl-params.conf;

    location /* {
        proxy_set_header Host $application;
        proxy_pass https://127.0.0.1:$wanted_port;
    }

}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name  _;
    include snippets/self-signed.conf;
    include snippets/ssl-params.conf;

    location /$application/* { # can I do this mapping now?!
        set $wanted_port $application; # set port from app
        proxy_set_header Host $application;
        proxy_pass https://127.0.0.1:$wanted_port;
    }
    location /$wanted_port/* {
        set $application $wanted_port; # is this crazy now?
        proxy_set_header Host $application;
        proxy_pass https://127.0.0.1:$wanted_port;
    }
}
server {
    listen 80;
    listen [::]:80;
    server_name  ~^(?<application>\.soul)$;
    set $wanted_port $application;
    
    location /* {
        proxy_set_header Host $application.soul;
        proxy_pass https://127.0.0.1:$wanted_port;
    }
}
server { # by numbered directory for ANY hostname eg localhost/10000
    listen 80;
    listen [::]:80;
    server_name  _;

    location /$application/* { # can I do this mapping now?!
        set $wanted_port $application; # set port from app
        proxy_set_header Host $application;
        proxy_pass https://127.0.0.1:$wanted_port;
    }
    location /$wanted_port/* {
        set $application $wanted_port; # is this crazy now?
        proxy_set_header Host $application;
        proxy_pass https://127.0.0.1:$wanted_port;
    }
    location / {
        return 301 https://www.funk.co.nz/;
    }
}


I still have this issue in that requests to some dev servers not working as planned:

http://localhost/cockpit <-- 404 Not Found nginx/1.26.2
http://localhost/9090 <-- i'd like this to go to localhost:9090/
http://cockpit/   <-- 200 OK Welcome to nginx!
http://cockpit.putin.soul/ <-- 200 OK Welcome to nginx!
https://cockpit.putin.soul/ ERR_ADDRESS_UNREACHABLE

Origins seem OK https://127.0.0.1:9090/ <-- 200 OK Cockpit login page https://127.0.0.1:8384/ <-- Syncthing login

I think my regular expression is all wrong and I also need to call up further nginx labels like... path or URL with this regex. For example: location ~/<$wanted_port>?([0-9]{1-5})/* or similar? to capture the number into a $wanted_port number?


Solution

  • What you are trying to do is called "hosting a web app under an URI prefix" or "hosting a web app under a base path other than root". Although this task may seem quite simple, in practice it is far from the point (and sometimes even impossible without altering the backend web app source code). Moreover, you aim to serve the backend web apps using different URI prefixes (/ and /<port_number>/) simultaneously, which makes your goal nearly unreachable.

    This type of question - how can I host a web app under a URI prefix - is one of the most common nginx (or any other reverse proxy) related questions on StackOverflow (and not only here). Someone writes an nginx configuration like this:

    location /prefix/ {
        proxy_set_header Host $host;
        proxy_pass http://backend/;
    }
    

    ...and wonders why it doesn't work properly. The answer is quite simple. Despite the root request (i.e. /prefix/) will be handled properly (while even here can be exceptions, see below), the following requests most likely won't. Consider this line within the main HTML file as an example:

    <link rel="stylesheet" href="/assets/css/styles.css" />
    

    It should be obvious that the upcoming /assets/css/styles.css request won't be handled by the location shown above.

    That is. If an application, for which the nginx configuration is intended to use a URI prefix, requests its assets (scripts, styles, images, etc.) using absolute paths rather than relative ones - i.e., starting from the root of the web server (which is likely if the application is not configured to use the particular URI prefix) - such requests will no longer be handled by the location /prefix/ { ... } block. Some web apps use relative URLs to request their assets by default (e.g., Etherpad), can automatically determine the URI prefix used to host them (e.g., phpMyAdmin), or can receive this prefix through an HTTP request header (e.g., RoundCube via the X-Request-Path header). Such applications can be served under different URI prefixes simultaneously. However, this is not a common case. While most applications do provide some way to configure them for being hosted under an arbitrary URI prefix, some don't even offer this option at all. Moreover, applications built using modern JavaScript frameworks (React, Angular, etc.), where the router is based on the HTML5 History API (e.g., React BrowserRouter), will interpret the route exactly as it appears in the browser's address bar. Unless they provide a corresponding configuration option, such applications cannot be hosted under a custom URI prefix without being rebuilt.

    With that said, your best (and probably only) option is to use subdomains for your backend services, and do not try to use URI prefixes at all. Now back to your nginx configuration. Besides the regex location should have the ~ modifier, your regex patterns - or what are intended to be regex patterns - make me cry. Nginx internally relies on the libpcre library (up to version 1.21.5) or the libpcre2 library (starting with version 1.21.5) for working with regular expressions (see "Reference - What does this regex mean?" and "Guide on how to use regex in Nginx location block section?" for more details). Your map block values are mixed up, and you don't need to use the set directive the way you have. The destination variable's value will be calculated automatically upon the first reference to the variable.

    Here is the configuration that (probably) should work:

    map $application $destination_port {
        webmin      10000;
        cockpit     9090;
        bluecherry  7001;
        apache      8080;
        apaches     8443;
        sytncthing  8384;
        node        3000;
    }
    
    # In case the different backends should use different schemes
    map $application $destination_scheme {
        webmin      https;
        ...
        node        http;
    }
    
    # WebSocket proxying, see http://nginx.org/en/docs/http/websocket.html
    map $http_upgrade $connection_upgrade {
        ''          close;
        default     upgrade;
    }
    
    server {
        listen 80;
        listen [::]:80;
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name ~^(?<application>.*)\.soul$
        include snippets/self-signed.conf;
        include snippets/ssl-params.conf;
    
        # if a non-existed application is specified via the `Host` header
        if ($destination_port = "") {
            return 404;
        }
    
        location / {
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
            # do not use `localhost` here or nginx will require a `resolver`
            # being defined for this configuration to work; use `127.0.0.1` instead
            proxy_pass $destination_scheme://127.0.0.1:$destination_port;
            proxy_redirect $destination_scheme://$host:destination_port/ /;
        }
    }
    

    Still, there is no guarantee that everything will work as expected. The good old curl is your best friend whether something goes wrong. The browser development tools are also can be useful. If you encounter issues with cookies, the directives proxy_cookie_domain and proxy_cookie_path might be helpful.