diff --git a/README.md b/README.md index 2757c33..9834bc7 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,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. @@ -113,6 +115,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 5733351..2f2e86c 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -110,7 +110,8 @@ # exposed ports:{{ range sortObjectsByKeysAsc $.container.Addresses "Port" }} {{ .Port }}/{{ .Proto }}{{ else }} (none){{ end }} {{- $default_port := when (eq (len $.container.Addresses) 1) (first $.container.Addresses).Port "80" }} # default port: {{ $default_port }} - {{- $port := or $.container.Env.VIRTUAL_PORT $default_port }} + {{- $current_virtual_port := when (ne $.virtual_port "") $.virtual_port (coalesce $.container.Env.VIRTUAL_PORT "") }} + {{- $port := when (ne $current_virtual_port "") $current_virtual_port $default_port }} # using port: {{ $port }} {{- $addr_obj := where $.container.Addresses "Port" $port | first }} {{- if and $addr_obj $addr_obj.HostPort }} @@ -179,7 +180,7 @@ include {{ $override }}; {{- else }} {{- $keepalive := first (keys (groupByLabel .Containers "com.github.nginx-proxy.nginx-proxy.keepalive")) }} - location {{ .Path }} { + location {{ when (ne .Path "") .Path "/" }} { {{- if eq .NetworkTag "internal" }} # Only allow traffic from internal clients include /etc/nginx/network_internal.conf; @@ -219,6 +220,7 @@ {{- end }} {{- define "upstream" }} + {{- $virtual_port := .VirtualPort }} upstream {{ .Upstream }} { {{- $server_found := false }} {{- $loadbalance := first (keys (groupByLabel .Containers "com.github.nginx-proxy.nginx-proxy.loadbalance")) }} @@ -231,7 +233,7 @@ upstream {{ .Upstream }} { {{- $args := dict "globals" $.globals "container" $container }} {{- template "container_ip" $args }} {{- $ip := $args.ip }} - {{- $args := dict "container" $container }} + {{- $args := dict "container" $container "virtual_port" $virtual_port }} {{- template "container_port" $args }} {{- $port := $args.port }} {{- if $ip }} @@ -353,7 +355,10 @@ proxy_set_header Proxy ""; * and whether there are any missing certs. */}} {{- range $vhost, $containers := groupByMulti $globals.containers "Env.VIRTUAL_HOST" "," }} - {{- $vhost := trim $vhost }} + {{- if (ne (trim $vhost) $vhost) }} +# WARNING: virtual host '{{ $vhost }}' is not trimmed + {{- $vhost = trim $vhost }} + {{- end }} {{- if not $vhost }} {{- /* Ignore containers with VIRTUAL_HOST set to the empty string. */}} {{- continue }} @@ -447,12 +452,40 @@ server { {{- $is_regexp := hasPrefix "~" $host }} {{- $upstream_name := when (or $is_regexp $globals.sha1_upstream_name) (sha1 $host) $host }} - {{- $paths := groupBy $containers "Env.VIRTUAL_PATH" }} + {{- /* + * Split containers between legacy VIRTUAL_PORT syntax and multiport one + */}} + {{- $multiport_syntax_containers := list }} + {{- range $virtual_port, $vp_containers := groupBy $containers "Env.VIRTUAL_PORT" }} + {{- if (or (gt (len (split $virtual_port ":")) 1) (gt (len (split $virtual_port ",")) 1)) }} + {{- $multiport_syntax_containers = concat $multiport_syntax_containers $vp_containers }} + {{- range $container := $vp_containers }} + {{- $containers = without $containers $container }} + {{- end }} + {{- end }} + {{- end }} + {{- $virtualPorts := groupByMulti $multiport_syntax_containers "Env.VIRTUAL_PORT" "," }} + {{- $nVirtualPorts := len $virtualPorts }} + + {{- /* + * Ignore VIRTUAL_PATH variables when using ultiport syntax + */}} + {{- $paths := dict }} + {{- if (eq $nVirtualPorts 0) }} + {{- $paths = groupBy $containers "Env.VIRTUAL_PATH" }} + {{- end }} {{- $nPaths := len $paths }} - {{- if eq $nPaths 0 }} + + {{- /* + * No VIRTUAL_PORT, no VIRTUAL_PATH + */}} + {{- if (and (eq $nPaths 0) (gt (len $containers) 0)) }} {{- $paths = dict "/" $containers }} {{- end }} + {{- /* + * Loops over VIRTUAL_PATHs + */}} {{- range $path, $containers := $paths }} {{- $upstream := $upstream_name }} {{- if gt $nPaths 0 }} @@ -460,7 +493,28 @@ server { {{- $upstream = printf "%s-%s" $upstream $sum }} {{- end }} # {{ $host }}{{ $path }} -{{ template "upstream" (dict "globals" $globals "Upstream" $upstream "Containers" $containers) }} + {{- template "upstream" (dict "globals" $globals "Upstream" $upstream "Containers" $containers "VirtualPort" "") }} + {{- end }} + + {{- /* + * Loops over multiport syntax containers + */}} + {{- range $vp_spec, $containers := $virtualPorts }} + {{- $vp_data := split $vp_spec ":" }} + {{- $vp_port := index $vp_data 0 }} + {{- $vp_path := "" }} + {{- $upstream := "" }} + {{- if (ge (len $vp_data) 2) }} + {{- $vp_path = index $vp_data 1 }} + {{- end }} + {{- if (ne $vp_path "") }} + {{- $sum := sha1 $vp_path }} + {{- $upstream = printf "%s-%s-%s" $upstream_name $sum $vp_port }} + {{- else }} + {{- $upstream = printf "%s-%s" $upstream_name $vp_port }} + {{- end }} +# {{ $host }}:{{ $vp_port }}{{ $vp_path }} + {{- template "upstream" (dict "globals" $globals "Upstream" $upstream "Containers" $containers "VirtualPort" $vp_port)}} {{- end }} {{- /* @@ -597,6 +651,7 @@ server { include /etc/nginx/vhost.d/default; {{- end }} + {{- $needs_default_root_response := ne $globals.default_root_response "none" }} {{- range $path, $containers := $paths }} {{- /* * Get the VIRTUAL_PROTO defined by containers w/ the same @@ -617,8 +672,47 @@ server { {{- $dest = (or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "") }} {{- end }} {{- template "location" (dict "Path" $path "Proto" $proto "Upstream" $upstream "Host" $host "VhostRoot" $vhost_root "Dest" $dest "NetworkTag" $network_tag "Containers" $containers) }} + {{- if (eq $path "/") }} + {{- $needs_default_root_response = false }} + {{- end }} {{- end }} - {{- if and (not (contains $paths "/")) (ne $globals.default_root_response "none")}} + + {{- range $vp_spec, $containers := $virtualPorts }} + {{- $vp_data := split $vp_spec ":" }} + {{- $vp_port := index $vp_data 0 }} + {{- $vp_path := "" }} + {{- $vp_dest := "" }} + {{- $upstream := "" }} + {{- if (ge (len $vp_data) 2) }} + {{- $vp_path = index $vp_data 1 }} + {{- if (ge (len $vp_data) 3) }} + {{- $vp_dest = index $vp_data 2 }} + {{- end }} + {{- end }} + {{- if (ne $vp_path "") }} + {{- $sum := sha1 $vp_path }} + {{- $upstream = printf "%s-%s-%s" $upstream_name $sum $vp_port }} + {{- else }} + {{- $upstream = printf "%s-%s" $upstream_name $vp_port }} + {{- end }} + {{- if (or (eq $vp_path "") (eq $vp_path "/")) }} + {{- $needs_default_root_response = false }} + {{- end }} + {{- /* + * Get the VIRTUAL_PROTO defined by containers w/ the same + * vhost-vpath, falling back to "http". + */}} + {{- $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }} + + {{- /* + * Get the NETWORK_ACCESS defined by containers w/ the same vhost, + * falling back to "external". + */}} + {{- $network_tag := or (first (groupByKeys $containers "Env.NETWORK_ACCESS")) "external" }} + {{- template "location" (dict "Path" $vp_path "Proto" $proto "Upstream" $upstream "Host" $host "VhostRoot" $vhost_root "Dest" $vp_dest "NetworkTag" $network_tag "Containers" $containers) }} + {{- end }} + + {{- if $needs_default_root_response }} location / { return {{ $globals.default_root_response }}; } 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..5556dae --- /dev/null +++ b/test/test_multiport_syntax/test_ignore_virtual_path_when_multiport.yml @@ -0,0 +1,28 @@ +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..fd2440a --- /dev/null +++ b/test/test_multiport_syntax/test_multiport_syntax.yml @@ -0,0 +1,17 @@ +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;