Add multiport syntax for variable VIRTUAL_PORT

Syntax:
  VIRTUAL_PORT = [ <virtual_port> | <multiport> ];
  multiport    = port, { ",",  port };
  port         = <virtual_port> [ ":", <virtual_path> [ ":", <virtual_dest> ]];

Example with multiport syntax:
  VIRTUAL_HOST: "multiport.example.com"
  VIRTUAL_PORT: "9220:~ ^/(admin|fonts?|images|webmin)/,10901,20901:/ws2p,30901:/gva/playground"

Produces:
  # multiport.example.com:10901
  upstream multiport.example.com-10901 {
      # Exposed ports: [{   10901  tcp } {   20901  tcp } {   30901  tcp } {   9220  tcp }]
      # Default virtual port: 80
      # VIRTUAL_PORT: 10901
      ## Can be connected with "docker-gen-bridge" network
      # blah
      server 172.29.0.5:10901;
  }
  # multiport.example.com:20901/ws2p
  upstream multiport.example.com-5c7ebef820fe004e45e3af1d0c47971594d028b2-20901 {
      # Exposed ports: [{   10901  tcp } {   20901  tcp } {   30901  tcp } {   9220  tcp }]
      # Default virtual port: 80
      # VIRTUAL_PORT: 20901
      ## Can be connected with "docker-gen-bridge" network
      # blah
      server 172.29.0.5:20901;
  }
  # multiport.example.com:30901/gva/playground
  upstream multiport.example.com-1f02ce2421b17d828edaabfc3014360891bb0be3-30901 {
      # Exposed ports: [{   10901  tcp } {   20901  tcp } {   30901  tcp } {   9220  tcp }]
      # Default virtual port: 80
      # VIRTUAL_PORT: 30901
      ## Can be connected with "docker-gen-bridge" network
      # blah
      server 172.29.0.5:30901;
  }
  # multiport.example.com:9220~ ^/(admin|fonts?|images|webmin)/
  upstream multiport.example.com-cae8bfc2ea1fe6bb6fda08727ab065e8fed98aa2-9220 {
      # Exposed ports: [{   10901  tcp } {   20901  tcp } {   30901  tcp } {   9220  tcp }]
      # Default virtual port: 80
      # VIRTUAL_PORT: 9220
      ## Can be connected with "docker-gen-bridge" network
      # blah
      server 172.29.0.5:9220;
  }
  server {
      server_name multiport.example.com;
      access_log /var/log/nginx/access.log vhost;
      listen 80 ;
      location / {
          proxy_pass http://multiport.example.com-10901;
      }
      location /ws2p {
          proxy_pass http://multiport.example.com-5c7ebef820fe004e45e3af1d0c47971594d028b2-20901/;
      }
      location /gva/playground {
          proxy_pass http://multiport.example.com-1f02ce2421b17d828edaabfc3014360891bb0be3-30901;
      }
      location ~ ^/(admin|fonts?|images|webmin)/ {
          proxy_pass http://multiport.example.com-cae8bfc2ea1fe6bb6fda08727ab065e8fed98aa2-9220;
      }
  }

This feature is discussed in that upstream issue:
https://github.com/nginx-proxy/nginx-proxy/issues/1504
This commit is contained in:
Gilles Filippini 2023-01-09 19:32:52 +01:00
parent 94fb8459cd
commit bd1aeda540
7 changed files with 268 additions and 17 deletions

View file

@ -50,6 +50,8 @@ By default, docker uses IPv6-to-IPv4 NAT. This means all client connections from
If you need to support multiple virtual hosts for a container, you can separate each entry with commas. For example, `foo.bar.com,baz.bar.com,bar.com` and each host will be setup the same. If you need to support multiple virtual hosts for a container, you can separate each entry with commas. For example, `foo.bar.com,baz.bar.com,bar.com` and each host will be setup the same.
Do **not** put any space before of after each comma.
### Virtual Ports ### Virtual Ports
When your container exposes only one port, nginx-proxy will default to this port, else to port 80. When your container exposes only one port, nginx-proxy will default to this port, else to port 80.
@ -61,6 +63,71 @@ For each host defined into `VIRTUAL_HOST`, the associated virtual port is retrie
1. From the container's exposed port if there is only one 1. From the container's exposed port if there is only one
1. From the default port 80 when none of the above methods apply 1. From the default port 80 when none of the above methods apply
### Multiport Syntax
There are services which expose more than one port. In this case you can set the `VIRTUAL_PORT` variable using multiport syntax:
```
VIRTUAL_PORT = [ <virtual_port> | <multiport> ];
multiport = port, { ",", port };
port = <virtual_port> [ ":", <virtual_path> [ ":", <virtual_dest> ]];
```
`<virtual_port>`, `<virtual_path>`, and `<virtual_dest>` accept the same values that `VIRTUAL_PORT`, `VIRTUAL_PATH`, and `VIRTUAL_DEST` do.
Example:
```
VIRTUAL_HOST: "multiport.example.com"
VIRTUAL_PORT: "9220:~ ^/(admin|fonts?|images|webmin)/,10901,20901:/ws2p,30901:/gva/playground"
```
would produce one nginx `upstream` definition per port, and as many `location` blocs:
```
# multiport.example.com:10901
upstream multiport.example.com-10901 {
## Can be connected with "docker-gen-bridge" network
# blah
server 172.29.0.5:10901;
}
# multiport.example.com:20901/ws2p
upstream multiport.example.com-5c7ebef820fe004e45e3af1d0c47971594d028b2-20901 {
## Can be connected with "docker-gen-bridge" network
# blah
server 172.29.0.5:20901;
}
# multiport.example.com:30901/gva/playground
upstream multiport.example.com-1f02ce2421b17d828edaabfc3014360891bb0be3-30901 {
## Can be connected with "docker-gen-bridge" network
# blah
server 172.29.0.5:30901;
}
# multiport.example.com:9220~ ^/(admin|fonts?|images|webmin)/
upstream multiport.example.com-cae8bfc2ea1fe6bb6fda08727ab065e8fed98aa2-9220 {
## Can be connected with "docker-gen-bridge" network
# blah
server 172.29.0.5:9220;
}
server {
server_name multiport.example.com;
listen 80 ;
access_log /var/log/nginx/access.log vhost;
location / {
proxy_pass http://multiport.example.com-10901;
}
location /ws2p {
proxy_pass http://multiport.example.com-5c7ebef820fe004e45e3af1d0c47971594d028b2-20901;
}
location /gva/playground {
proxy_pass http://multiport.example.com-1f02ce2421b17d828edaabfc3014360891bb0be3-30901;
}
location ~ ^/(admin|fonts?|images|webmin)/ {
proxy_pass http://multiport.example.com-cae8bfc2ea1fe6bb6fda08727ab065e8fed98aa2-9220;
}
}
```
As with the `VIRTUAL_PATH` it is possible to define per path location configuration files.
**Important note:** All `VIRTUAL_PATH` variables will be ignored for any virtual host appearing in a at least one container where `VIRTUAL_PORT` uses the multiport syntax, .
### Wildcard Hosts ### Wildcard Hosts
You can also use wildcards at the beginning and the end of host name, like `*.bar.com` or `foo.bar.*`. Or even a regular expression, which can be very useful in conjunction with a wildcard DNS service like [nip.io](https://nip.io) or [sslip.io](https://sslip.io), using `~^foo\.bar\..*\.nip\.io` will match `foo.bar.127.0.0.1.nip.io`, `foo.bar.10.0.2.2.nip.io` and all other given IPs. More information about this topic can be found in the nginx documentation about [`server_names`](http://nginx.org/en/docs/http/server_names.html). You can also use wildcards at the beginning and the end of host name, like `*.bar.com` or `foo.bar.*`. Or even a regular expression, which can be very useful in conjunction with a wildcard DNS service like [nip.io](https://nip.io) or [sslip.io](https://sslip.io), using `~^foo\.bar\..*\.nip\.io` will match `foo.bar.127.0.0.1.nip.io`, `foo.bar.10.0.2.2.nip.io` and all other given IPs. More information about this topic can be found in the nginx documentation about [`server_names`](http://nginx.org/en/docs/http/server_names.html).

View file

@ -347,6 +347,7 @@ upstream {{ $vpath.upstream }} {
* - "Containers": List of container's RuntimeContainer struct. * - "Containers": List of container's RuntimeContainer struct.
* - "Upstream_name" * - "Upstream_name"
* - "Has_virtual_paths": boolean * - "Has_virtual_paths": boolean
* - "Multiport_syntax": boolean
* - "Path" * - "Path"
* *
* The return values will be added to the dot dict with keys: * The return values will be added to the dot dict with keys:
@ -373,6 +374,12 @@ upstream {{ $vpath.upstream }} {
{{- $upstream = printf "%s-%s" $upstream $sum }} {{- $upstream = printf "%s-%s" $upstream $sum }}
{{- $dest = or (first (groupByKeys $.Containers "Env.VIRTUAL_DEST")) "" }} {{- $dest = or (first (groupByKeys $.Containers "Env.VIRTUAL_DEST")) "" }}
{{- end }} {{- end }}
{{- if $.Multiport_syntax }}
{{- if (not (eq $.Path "/")) }}
{{- $sum := sha1 $.Path }}
{{- $upstream = printf "%s-%s" $upstream $sum }}
{{- end }}
{{- end }}
{{- $_ := set $ "proto" $proto }} {{- $_ := set $ "proto" $proto }}
{{- $_ := set $ "network_tag" $network_tag }} {{- $_ := set $ "network_tag" $network_tag }}
{{- $_ := set $ "upstream" $upstream }} {{- $_ := set $ "upstream" $upstream }}
@ -381,6 +388,30 @@ upstream {{ $vpath.upstream }} {
{{- $_ := set $ "keepalive" $keepalive }} {{- $_ := set $ "keepalive" $keepalive }}
{{- end }} {{- end }}
{{- /*
* Template used as a function to parse a port specification from
* multiport syntax.
* The results (path, port, dest) are returned via the provided do dict.
*/}}
{{- define "parse_multiport_syntax_port_spec" }}
{{- $vp_data := split $.spec ":" }}
{{- $vp_port := index $vp_data 0 }}
{{- $vp_path := "/" }}
{{- $vp_dest := "" }}
{{- if (gt (len $vp_data) 1) }}
{{- $vp_path = index $vp_data 1 }}
{{- if (empty $vp_path) }}
{{- $vp_path = "/" }}
{{- end }}
{{- end }}
{{- if (gt (len $vp_data) 2) }}
{{- $vp_dest = index $vp_data 2 }}
{{- end }}
{{- $_ := set $ "path" $vp_path }}
{{- $_ := set $ "port" $vp_port }}
{{- $_ := set $ "dest" $vp_dest }}
{{- end }}
# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the # If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the
# scheme used to connect to this server # scheme used to connect to this server
map $http_x_forwarded_proto $proxy_x_forwarded_proto { map $http_x_forwarded_proto $proxy_x_forwarded_proto {
@ -535,27 +566,73 @@ proxy_set_header Proxy "";
{{- /* Get the VIRTUAL_ROOT By containers w/ use fastcgi root */}} {{- /* Get the VIRTUAL_ROOT By containers w/ use fastcgi root */}}
{{- $vhost_root := or (first (groupByKeys $containers "Env.VIRTUAL_ROOT")) "/var/www/public" }} {{- $vhost_root := or (first (groupByKeys $containers "Env.VIRTUAL_ROOT")) "/var/www/public" }}
{{- $vhost_multiport_syntax := false }}
{{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }} {{- range $vp_spec, $_ := groupBy $containers "Env.VIRTUAL_PORT" }}
{{- $has_virtual_paths := gt (len $tmp_paths) 0}} {{- if (or (gt (len (split $vp_spec ":")) 1) (gt (len (split $vp_spec ",")) 1)) }}
{{- if not $has_virtual_paths }} {{- $vhost_multiport_syntax = true }}
{{- $tmp_paths = dict "/" $containers }} {{- break }}
{{- end }}
{{- end }} {{- end }}
{{- $paths := dict }} {{- $paths := dict }}
{{- if $vhost_multiport_syntax }}
{{- range $vp_spec, $containers := groupByMulti $containers "Env.VIRTUAL_PORT" "," }}
{{- $vp_spec = trim $vp_spec }}
{{- if not $vp_spec }}
{{- continue }}
{{- end }}
{{- $args := dict "spec" $vp_spec }}
{{- template "parse_multiport_syntax_port_spec" $args }}
{{- $vp_path := $args.path }}
{{- $vp_port := $args.port }}
{{- $vp_dest := $args.dest }}
{{- range $path, $containers := $tmp_paths }} {{- $path_data := when (hasKey $paths $vp_path) (get $paths $vp_path) (dict) }}
{{- $args := dict "Containers" $containers "Path" $path "Upstream_name" $upstream_name "Has_virtual_paths" $has_virtual_paths }} {{- $path_ports := when (hasKey $path_data "ports") (get $path_data "ports") (dict) }}
{{- template "get_path_info" $args }} {{- $path_port_containers := when (hasKey $path_ports $vp_port) (get $path_ports $vp_port) (list) }}
{{- $_ := set $paths $path (dict {{- $path_port_containers = concat $path_port_containers $containers }}
"ports" (dict "legacy" $containers)
"dest" $args.dest {{- $_ := set $path_ports $vp_port $path_port_containers }}
"proto" $args.proto {{- $_ := set $path_data "ports" $path_ports }}
"network_tag" $args.network_tag {{- if (not (hasKey $path_data "dest")) }}
"upstream" $args.upstream {{- $_ := set $path_data "dest" $vp_dest }}
"loadbalance" $args.loadbalance {{- end }}
"keepalive" $args.keepalive {{- $_ := set $paths $vp_path $path_data }}
) }} {{- end }}
{{- range $path, $path_data := $paths }}
{{- $path_containers := list }}
{{- range $port, $port_containers := $path_data.ports }}
{{ $path_containers = concat $path_containers $port_containers }}
{{- end }}
{{- $args := dict "Containers" $path_containers "Path" $path "Upstream_name" $upstream_name "Has_virtual_paths" false "Multiport_syntax" true }}
{{- template "get_path_info" $args }}
{{- $_ := set $path_data "proto" $args.proto }}
{{- $_ := set $path_data "network_tag" $args.network_tag }}
{{- $_ := set $path_data "upstream" $args.upstream }}
{{- $_ := set $path_data "loadbalance" $args.loadbalance }}
{{- $_ := set $path_data "keepalive" $args.keepalive }}
{{- $_ := set $paths $path $path_data }}
{{- end }}
{{- else }}
{{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }}
{{- $has_virtual_paths := gt (len $tmp_paths) 0}}
{{- if not $has_virtual_paths }}
{{- $tmp_paths = dict "/" $containers }}
{{- end }}
{{- range $path, $containers := $tmp_paths }}
{{- $args := dict "Containers" $containers "Path" $path "Upstream_name" $upstream_name "Has_virtual_paths" $has_virtual_paths "Multiport_syntax" false }}
{{- template "get_path_info" $args }}
{{- $_ := set $paths $path (dict
"ports" (dict "legacy" $containers)
"dest" (or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "")
"proto" $args.proto
"network_tag" $args.network_tag
"upstream" $args.upstream
"loadbalance" $args.loadbalance
"keepalive" $args.keepalive
) }}
{{- end }}
{{- end }} {{- end }}
{{- $_ := set $globals.vhosts $hostname (dict {{- $_ := set $globals.vhosts $hostname (dict

View file

@ -0,0 +1,29 @@
import pytest
def test_web_no_slash_location(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/")
assert r.status_code == 405
def test_web_rout_to_slash_port(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/which-port")
assert r.status_code == 200
assert "answer from port 83\n" in r.text
def test_web1_answers_on_slash_location(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/")
assert r.status_code == 200
def test_web1_no_virtual_path(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/which-port")
assert r.status_code == 404
def test_web1_port_80_is_served_by_location_slash_80(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/80/port")
assert r.status_code == 200
assert "answer from port 80\n" in r.text
def test_web1_port_81_is_served_by_location_slash_81(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/81/port")
assert r.status_code == 200
assert "answer from port 81\n" in r.text

View file

@ -0,0 +1,31 @@
version: "2"
services:
web:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: "web.nginx-proxy.tld,web1.nginx-proxy.tld"
VIRTUAL_PORT: "83"
VIRTUAL_PATH: "/which-port"
VIRTUAL_DEST: "/port"
web1:
image: web
expose:
- "80"
- "81"
environment:
WEB_PORTS: "80 81"
VIRTUAL_HOST: "web1.nginx-proxy.tld"
VIRTUAL_PORT: "80:/80:/,81:/81:/"
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./web.nginx-proxy.tld_022792f37cc2c58102bdf79316aebed215e0de21_location:/etc/nginx/vhost.d/web.nginx-proxy.tld_022792f37cc2c58102bdf79316aebed215e0de21_location
environment:
DEFAULT_ROOT: "405"

View file

@ -0,0 +1,26 @@
import pytest
def test_port_80_is_server_by_location_root(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/port")
assert r.status_code == 200
assert "answer from port 80\n" in r.text
def test_port_81_is_server_by_location_slash81(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/81/port")
assert r.status_code == 200
assert "answer from port 81\n" in r.text
def test_port_82_is_server_by_location_slash82_with_dest_slashport(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/82")
assert r.status_code == 200
assert "answer from port 82\n" in r.text
def test_port_83_is_server_by_regex_location_slash83_with_rewrite_in_custom_location_file(docker_compose, nginxproxy):
# The custom location file with rewrite is requested because when
# location is specified using a regex then proxy_pass should be
# specified without a URI
# see http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
r = nginxproxy.get("http://web.nginx-proxy.tld/83/port")
assert r.status_code == 200
assert "answer from port 83\n" in r.text

View file

@ -0,0 +1,20 @@
version: "2"
services:
web:
image: web
expose:
- "80"
- "81"
- "82"
- "83"
environment:
WEB_PORTS: "80 81 82 83"
VIRTUAL_HOST: "web.nginx-proxy.tld"
VIRTUAL_PORT: "80,81:/81:/,82:/82:/port,83:~ ^/[8][3]"
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./web.nginx-proxy.tld_022792f37cc2c58102bdf79316aebed215e0de21_location:/etc/nginx/vhost.d/web.nginx-proxy.tld_022792f37cc2c58102bdf79316aebed215e0de21_location

View file

@ -0,0 +1 @@
rewrite ^/83/(.*)$ /$1 break;