diff --git a/docs/README.md b/docs/README.md index 09deae6..d476bdb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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. +Do **not** put any space before of after each comma. + ### Virtual Ports 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 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 = [ | ]; +multiport = port, { ",", port }; +port = [ ":", [ ":", ]]; +``` + +``, ``, and `` 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 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). diff --git a/nginx.tmpl b/nginx.tmpl index fefb07f..0a72a9e 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -347,6 +347,7 @@ upstream {{ $vpath.upstream }} { * - "Containers": List of container's RuntimeContainer struct. * - "Upstream_name" * - "Has_virtual_paths": boolean + * - "Multiport_syntax": boolean * - "Path" * * 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 }} {{- $dest = or (first (groupByKeys $.Containers "Env.VIRTUAL_DEST")) "" }} {{- end }} + {{- if $.Multiport_syntax }} + {{- if (not (eq $.Path "/")) }} + {{- $sum := sha1 $.Path }} + {{- $upstream = printf "%s-%s" $upstream $sum }} + {{- end }} + {{- end }} {{- $_ := set $ "proto" $proto }} {{- $_ := set $ "network_tag" $network_tag }} {{- $_ := set $ "upstream" $upstream }} @@ -381,6 +388,30 @@ upstream {{ $vpath.upstream }} { {{- $_ := set $ "keepalive" $keepalive }} {{- 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 # scheme used to connect to this server 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 */}} {{- $vhost_root := or (first (groupByKeys $containers "Env.VIRTUAL_ROOT")) "/var/www/public" }} - - {{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }} - {{- $has_virtual_paths := gt (len $tmp_paths) 0}} - {{- if not $has_virtual_paths }} - {{- $tmp_paths = dict "/" $containers }} + {{- $vhost_multiport_syntax := false }} + {{- range $vp_spec, $_ := groupBy $containers "Env.VIRTUAL_PORT" }} + {{- if (or (gt (len (split $vp_spec ":")) 1) (gt (len (split $vp_spec ",")) 1)) }} + {{- $vhost_multiport_syntax = true }} + {{- break }} + {{- end }} {{- end }} {{- $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 }} - {{- $args := dict "Containers" $containers "Path" $path "Upstream_name" $upstream_name "Has_virtual_paths" $has_virtual_paths }} - {{- template "get_path_info" $args }} - {{- $_ := set $paths $path (dict - "ports" (dict "legacy" $containers) - "dest" $args.dest - "proto" $args.proto - "network_tag" $args.network_tag - "upstream" $args.upstream - "loadbalance" $args.loadbalance - "keepalive" $args.keepalive - ) }} + {{- $path_data := when (hasKey $paths $vp_path) (get $paths $vp_path) (dict) }} + {{- $path_ports := when (hasKey $path_data "ports") (get $path_data "ports") (dict) }} + {{- $path_port_containers := when (hasKey $path_ports $vp_port) (get $path_ports $vp_port) (list) }} + {{- $path_port_containers = concat $path_port_containers $containers }} + + {{- $_ := set $path_ports $vp_port $path_port_containers }} + {{- $_ := set $path_data "ports" $path_ports }} + {{- if (not (hasKey $path_data "dest")) }} + {{- $_ := set $path_data "dest" $vp_dest }} + {{- end }} + {{- $_ := 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 }} {{- $_ := set $globals.vhosts $hostname (dict diff --git a/test/test_multiport_syntax/test_ignore_virtual_path_when_multiport.py b/test/test_multiport_syntax/test_ignore_virtual_path_when_multiport.py new file mode 100644 index 0000000..36b9489 --- /dev/null +++ b/test/test_multiport_syntax/test_ignore_virtual_path_when_multiport.py @@ -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 diff --git a/test/test_multiport_syntax/test_ignore_virtual_path_when_multiport.yml b/test/test_multiport_syntax/test_ignore_virtual_path_when_multiport.yml new file mode 100644 index 0000000..8192761 --- /dev/null +++ b/test/test_multiport_syntax/test_ignore_virtual_path_when_multiport.yml @@ -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" diff --git a/test/test_multiport_syntax/test_multiport_syntax.py b/test/test_multiport_syntax/test_multiport_syntax.py new file mode 100644 index 0000000..18962de --- /dev/null +++ b/test/test_multiport_syntax/test_multiport_syntax.py @@ -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 diff --git a/test/test_multiport_syntax/test_multiport_syntax.yml b/test/test_multiport_syntax/test_multiport_syntax.yml new file mode 100644 index 0000000..b1dbcc0 --- /dev/null +++ b/test/test_multiport_syntax/test_multiport_syntax.yml @@ -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 diff --git a/test/test_multiport_syntax/web.nginx-proxy.tld_022792f37cc2c58102bdf79316aebed215e0de21_location b/test/test_multiport_syntax/web.nginx-proxy.tld_022792f37cc2c58102bdf79316aebed215e0de21_location new file mode 100644 index 0000000..b08e818 --- /dev/null +++ b/test/test_multiport_syntax/web.nginx-proxy.tld_022792f37cc2c58102bdf79316aebed215e0de21_location @@ -0,0 +1 @@ +rewrite ^/83/(.*)$ /$1 break;