146

I have a running web-application at http://example.com/, and want to "mount" another application, on a separate server on http://example.com/en. Upstream servers and proxy_pass seem to work, but for one issue:

upstream luscious {
 server lixxxx.members.linode.com:9001;
}

server {
  root /var/www/example.com/current/public/;
  server_name example.com;

  location /en {
    proxy_pass http://luscious;
  }
}

When opening example.com/en, my upstream application returns 404 not found /en. This makes sense, as the upstream does not have the path /en.

Is proxy_path the right solution? Should I rewrite "upstream" so it listens to /en instead, as it root path? Or is there a directive that allows me to rewrite the path passed along to upstream?

berkes
  • 1,975
  • 3
  • 15
  • 18

5 Answers5

278

This is likely the most efficient way to do what you want, without the use of any regular expressions:

location = /en {
    return 302 /en/;
}
location /en/ {
    proxy_pass http://luscious/;  # note the trailing slash here, it matters!
}
ralien
  • 113
  • 4
cnst
  • 12,948
  • 7
  • 51
  • 75
  • 1
    As far as I know, the last part would still pass "/en" as path along to the proxy, not? – berkes Dec 21 '13 at 13:17
  • 19
    @berkes, no, it won't -- the trailing slash in `proxy_pass` is what makes a difference. Also, this answer is more correct than the one you came up with, because it also ensures that [`proxy_redirect`](http://nginx.org/r/proxy_redirect) stays at `default`, so, you could still use `302` et al within your backend, and have it work correctly everywhere. – cnst Dec 21 '13 at 18:27
  • 1
    Doesn't looks like it works. Here's what luscious would get: "GET /en/ HTTP/1.0" 404 I.e. it's proxied along with a path – Vanuan May 02 '16 at 22:21
  • @Vanuan, no idea what you mean, or where `/docs/` come from! Everything in the answer should still work 100%. – cnst May 02 '16 at 22:25
  • 7
    Ah, I was missing the trailing slash :( – Vanuan May 02 '16 at 23:54
  • 11
    AAARGH! TRAILING SLASH ! – barrymac May 26 '17 at 16:27
  • 9
    3 hours of searching, and yeah... It was the trailing slash. Thanks mate! – Lucas P. Oct 15 '18 at 16:09
  • 1
    Soo... what does the trailing slash do? – The Guy with The Hat Apr 29 '19 at 19:53
  • 4
    @TheGuywithTheHat it just specifies that there's a path specified for the mapping; otherwise, no mapping is assumed, and paths are passed as-is. – cnst Apr 29 '19 at 20:07
  • That goddamned trailing slash wasted my time – William Wino May 15 '19 at 08:53
  • Works for me well, but beware, doesn't work in combination with directive try_files!! – Michel Samia Nov 06 '19 at 14:29
  • 1
    Doesn't seem to work with variable upstream either. Hmm. `location "/in/" { set $upstream 'service:port'; proxy_pass http://$upstream/; }` Still calls service with `GET /in/` Hmm. Purpose of variable upstream it to allow nginx to be up before service is up. Just gets bad gateway until service IS up, but doesn't prevent nginx itself. – Jesse Chisholm May 27 '20 at 19:08
  • 1
    @JesseChisholm variables are a special case, see http://nginx.org/r/proxy_pass; tl;dr: use something like `proxy_pass http://$upstream$uri;` – cnst May 31 '20 at 17:02
  • The documentation says that URI `http://in/some/path` should in my example be proxied as `http://$upstream/some/path` (because I have a trailing slash after $upstream) with the matched `/in` removed. I'm seeing the lead part remain. I don't want the entire `$uri` I want the portion AFTER the match in location. – Jesse Chisholm Jun 01 '20 at 00:47
  • 1
    Finally found what works for me: `location ~* ^/in/(.*)$ { set $upstream server:port; proxy_pass http://$upstream/$1; }` so the regex matches the part I want to remove, and the `$1` puts it back where I want it. And I still get to use my variable upstream. :) – Jesse Chisholm Jun 16 '20 at 20:40
  • @JesseChisholm if you're using variables in `proxy_pass`, you don't have to switch to regex in `location`, you can merely use `$uri` in place of the `$1` with a corresponding non-regex location. – cnst Jun 17 '20 at 21:34
  • @cnst I wouldn't have bothered with the regex form if the `$uri` form worked for me. But using `location = /in/ { set $upstream ...; proxy_pass http://$upstream/$uri; }` the request `https://target/in/health` gets translated as `http://upstream/in/health` which is not what I want, `http://upstream/health`. – Jesse Chisholm Jun 19 '20 at 14:59
  • @JesseChisholm you're right; I think I forgot to mention also using `rewrite`; I'd probably use a static location, rewrite, and then `$uri` in proxy_pass, but I guess it might be all the same and/or classified as a premature optimisation. – cnst Jun 19 '20 at 22:04
  • In production, I use the actual hostname to distinguish which internal service to proxy to. But this is in development where I don't have DNS mapping many hosts to one IP for me, so I needed a backdoor to test with. So, yes, static locations per actual hostname is _much_ better. – Jesse Chisholm Jun 21 '20 at 01:14
  • I have configured as per above verified answer but it did not work for me. and my configuration are as below. looking for help. – Govinda Chaulagain Aug 09 '22 at 01:31
  • server { server_name abc.com.np www.abc.com.np; root /home/nsec/nsec-fe; index index index.html index.htm index.html; access_log /var/log/nginx/abc.com.np/abc.com.np.access.log; error_log /var/log/nginx/abc.com.np/abc.com.np.errors.log; location / { root /home/nsec/nsec-fe; index index index.html index.htm index.html; } location /images/ { root /home/backend/cdn; } location /api/ { proxy_pass http://api; } } – Govinda Chaulagain Aug 09 '22 at 01:33
  • Could you add a note for *both* trailing slashes? The fact you only referenced one meant I missed the second... – Tom Sep 11 '22 at 15:22
20

I'd like to address a newer regex-based answer that's been rising in popularity.

location ~ ^/en(/?)(.*)$ {  # OOPS!
  proxy_pass http://luscious/$2$is_args$args;  # OOPS!
}

The solution may seem more cute at first glance, but it's wrong for multiple reasons.

  • The above regex would match a request uri of /enjoy, redirecting it to /joy upstream. Is this really intended?

  • A request for /en will not result in any redirects, directly serving a / from the upstream (almost as if a request for /en/ was made instead, but not quite). If you use relative URIs within your root page upstream (otherwise, why wouldn't you have the /en/ prefix right there within the upstream URIs?), e.g. src="style.css" (which might reference a language-specific url("menu.png"), for example), then the browser will request that as /style.css instead of /en/style.css. (Or even if you use absolute URIs everywhere, what if someone references an obscure semi-optional resource relatively?) Oops, suddenly the site may not work, but only sometimes or in edge cases.

  • As per my earlier advice at another question already mentioned by the OP's own answer, using regular expressions prevents the proxy_redirect directive from having the default value of default, turning it down to off instead. This means that if the upstream replies with Location: http://127.0.0.1:8080/en/dir/ when a request for /en/dir is made, then that's what the client will see, which obviously won't work correctly. (Which would have been especially ironic for a /en request which prompts regex use in the first place, yet this specific implementation instead suffers from another problem as already mentioned above.) Plus, if you're already using the upstream directive, then it might get extra ugly if you just try to go with a custom one, especially if you may have more than one upstream server — how do you have a separate proxy_redirect for each one of those? You could use regular expressions within proxy_redirect, too, maybe even to match any host, but then what if you decide to give a cross-domain redirect in the future?

To try to address some of the above points with a single regex-based location, we could do the following (note that in proxy_pass we also had to drop the reference to a server from an upstream-based directive, to make proxy_redirect more straightforward):

location ~ ^/en/?((?<=/).*)?$ {
  location = /en { return 302 /en/; }
  proxy_pass http://127.0.0.1:8080/$1$is_args$args;
  proxy_redirect http://127.0.0.1:8080/ /en/;
}

So, if you ask me, the original solution with the two sibling top-level locations would still be a better idea than digging yourself into a rabbit hole by going the regex route instead.

cnst
  • 12,948
  • 7
  • 51
  • 75
  • 1
    This causes an exception: `nginx: [emerg] location "/en" is outside location "^/en/?((?<=/).*` – Athlan Jul 05 '16 at 11:41
  • 1
    @Athlan, that's because you shouldn't really be using that in the first place! If you still want to, you can put that location outside of the regexp. – cnst Jul 05 '16 at 11:59
  • puting localation = /en outside works really fine! thanks – Sebastian Webber Jan 12 '18 at 13:16
  • Also wrong when you have spaces in your URLs, `$2` will not re-encode a space as `+` or `%20` and the upstream will get a real space in the URL which is invalid. I filed this as a bug but the NGINX maintainer disagrees. – Marc Feb 22 '22 at 11:58
8

So, I found the answer on stackoverflow:

upstream luscious {
 server lixxxx.members.linode.com:9001;
}

server {
  root /var/www/example.com/current/public/;
  server_name example.com;

  location ~ ^/en(/?)(.*) {
    proxy_pass http://luscious/$2;
  }
}

Basically: passing a regex into location and passing the backref along to the proxy_pass url.

berkes
  • 1,975
  • 3
  • 15
  • 18
6

Accounting to Nginx documents

To pass a request to an HTTP proxied server, the proxy_pass directive is specified inside a location. For example:

location /some/path/ {
    proxy_pass http://www.example.com/link/;
}

This example configuration results in passing all requests processed in this location to the proxied server at the specified address. This address can be specified as a domain name or an IP address. The address may also include a port:

location ~ \.php {
    proxy_pass http://127.0.0.1:8000;
}

Note that in the first example above, the address of the proxied server is followed by a URI, /link/. If the URI is specified along with the address, it replaces the part of the request URI that matches the location parameter. For example, here the request with the /some/path/page.html URI will be proxied to http://www.example.com/link/page.html. If the address is specified without a URI, or it is not possible to determine the part of URI to be replaced, the full request URI is passed (possibly, modified).

Jeff
  • 161
  • 1
  • 2
  • 1
    Why isn't this the correct answer? It provides the correct approach using the built-in tools of nginx instead of clumsy regex patterns – Teodoro Aug 14 '21 at 03:14
0

I had a play with the accepted solution above but found it was causing dodgy redirects for all the CSS and JS assets. In the end, I found inspiration from the way that the LinuxServer SWAG Nginx configurations are done. You can find them here. Take a look at pihole.subfolder.conf.sample.

As such, my solution looks like this:

location /en {
    return 302 $scheme://$host/en/;
}

location ^~ /en/ {
    set $upstream_app lixxxx.members.linode.com;
    set $upstream_port 9001;
    set $upstream_proto http;
    proxy_pass $upstream_proto://$upstream_app:$upstream_port;

    rewrite /en(.*) $1 break;
}
kabadisha
  • 161
  • 6