From 97a5dec57a6e33a3d4dd93ab22a52c0fffcc0872 Mon Sep 17 00:00:00 2001 From: Gilles Filippini Date: Fri, 28 May 2021 00:04:43 +0200 Subject: [PATCH 1/3] Honor VIRTUAL_PORT + DEBUG flag + fallback entry The VIRTUAL_PORT environment variable should always be honored. Even when the related port is not exposed. Fix for nging-proxy/nginx-proxy#1132. This commit also add the DEBUG environment variable which enables more verbose comments in the nginx comfiguration file to help troubleshooting unreachable containers. Finaly it fixes nging-proxy/nginx-proxy#1105 as well by defining only one fallback entry per upstream block. --- README.md | 45 ++++++++++++++++++++++++++++++++++++++------- nginx.tmpl | 46 +++++++++++++++++++++++++--------------------- 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 9b13463..a03b5e2 100644 --- a/README.md +++ b/README.md @@ -82,17 +82,21 @@ NginX does not support scoped IPv6 resolvers. In [docker-entrypoint.sh](./docker By default, docker uses IPv6-to-IPv4 NAT. This means all client connections from IPv6 addresses will show docker's internal IPv4 host address. To see true IPv6 client IP addresses, you must [enable IPv6](https://docs.docker.com/config/daemon/ipv6/) and use [ipv6nat](https://github.com/robbertkl/docker-ipv6nat). You must also disable the userland proxy by adding `"userland-proxy": false` to `/etc/docker/daemon.json` and restarting the daemon. -### Multiple Ports - -If your container exposes multiple ports, nginx-proxy will default to the service running on port 80. If you need to specify a different port, you can set a VIRTUAL_PORT env var to select a different one. If your container only exposes one port and it has a VIRTUAL_HOST env var set, that port will be selected. - - [1]: https://github.com/jwilder/docker-gen - [2]: http://jasonwilder.com/blog/2014/03/25/automated-nginx-reverse-proxy-for-docker/ - ### Multiple Hosts 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. +### Virtual Ports + +When your container exposes only one port, nginx-proxy will default to this port, else to port 80. + +If you need to specify a different port, you can set a `VIRTUAL_PORT` env var to select a different one. This variable cannot be set to more than one port. + +For each host defined into `VIRTUAL_HOST`, the associated virtual port is retrieved by order of precedence: +1. From the `VIRTUAL_PORT` environment variable +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 + ### 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 [xip.io](http://xip.io), using `~^foo\.bar\..*\.xip\.io` will match `foo.bar.127.0.0.1.xip.io`, `foo.bar.10.0.2.2.xip.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). @@ -424,6 +428,33 @@ will be used on any virtual host which does not have a `/etc/nginx/vhost.d/{VIRT #### Per-VIRTUAL_HOST `server_tokens` configuration Per virtual-host `servers_tokens` directive can be configured by passing appropriate value to the `SERVER_TOKENS` environment variable. Please see the [nginx http_core module configuration](https://nginx.org/en/docs/http/ngx_http_core_module.html#server_tokens) for more details. +### Troubleshooting + +In case you can't access your VIRTUAL_HOST, set `DEBUG=true` in the client container's environment and have a look at the generated nginx configuration file `/etc/nginx/conf.d/default`: + +``` +$ docker exec cat /etc/nginx/conf.d/default +``` +Especially at `upstream` definition blocks which should look like: + +``` +# foo.example.com +upstream foo.example.com { + ## Can be connected with "my_network" network + # Exposed ports: [{ tcp } { tcp } ...] + # Default virtual port: + # VIRTUAL_PORT: + # foo + server 172.18.0.9:; + # Fallback entry + server 127.0.0.1 down; +} +``` + +The effective `Port` is retrieved by order of precedence: +1. From the `VIRTUAL_PORT` environment variable +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 ### Contributing diff --git a/nginx.tmpl b/nginx.tmpl index 9d90241..66f346c 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -2,24 +2,25 @@ {{ $external_http_port := coalesce $.Env.HTTP_PORT "80" }} {{ $external_https_port := coalesce $.Env.HTTPS_PORT "443" }} +{{ $debug_all := $.Env.DEBUG }} {{ define "upstream" }} {{ if .Address }} {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} {{ if and .Container.Node.ID .Address.HostPort }} - # {{ .Container.Node.Name }}/{{ .Container.Name }} + # {{ .Container.Node.Name }}/{{ .Container.Name }} server {{ .Container.Node.Address.IP }}:{{ .Address.HostPort }}; {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} {{ else if .Network }} - # {{ .Container.Name }} - server {{ .Network.IP }}:{{ .Address.Port }}; + # {{ .Container.Name }} + server {{ .Network.IP }}:{{ .Address.Port }}; {{ end }} {{ else if .Network }} - # {{ .Container.Name }} + # {{ .Container.Name }} {{ if .Network.IP }} - server {{ .Network.IP }} down; + server {{ .Network.IP }}:{{ .VirtualPort }}; {{ else }} - server 127.0.0.1 down; + # /!\ No IP for this network! {{ end }} {{ end }} @@ -180,29 +181,32 @@ server { upstream {{ $upstream_name }} { {{ range $container := $containers }} - {{ $addrLen := len $container.Addresses }} - + {{ $debug := (eq (coalesce $container.Env.DEBUG $debug_all "false") "true") }} + {{/* If only 1 port exposed, use that as a default, else 80 */}} + {{ $defaultPort := (when (eq (len $container.Addresses) 1) (first $container.Addresses) (dict "Port" "80")).Port }} {{ range $knownNetwork := $CurrentContainer.Networks }} {{ range $containerNetwork := $container.Networks }} {{ if (and (ne $containerNetwork.Name "ingress") (or (eq $knownNetwork.Name $containerNetwork.Name) (eq $knownNetwork.Name "host"))) }} - ## Can be connected with "{{ $containerNetwork.Name }}" network - - {{/* If only 1 port exposed, use that */}} - {{ if eq $addrLen 1 }} - {{ $address := index $container.Addresses 0 }} - {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} - {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} - {{ else }} - {{ $port := coalesce $container.Env.VIRTUAL_PORT "80" }} - {{ $address := where $container.Addresses "Port" $port | first }} - {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} + ## Can be connected with "{{ $containerNetwork.Name }}" network + {{ $port := (coalesce $container.Env.VIRTUAL_PORT $defaultPort) }} + {{ $address := where $container.Addresses "Port" $port | first }} + {{ if $debug }} + # Exposed ports: {{ $container.Addresses }} + # Default virtual port: {{ $defaultPort }} + # VIRTUAL_PORT: {{ $container.Env.VIRTUAL_PORT }} + {{ if not $address }} + # /!\ Virtual port not exposed + {{ end }} {{ end }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork "VirtualPort" $port) }} {{ else }} - # Cannot connect to network of this container - server 127.0.0.1 down; + # Cannot connect to network '{{ $containerNetwork.Name }}' of this container {{ end }} {{ end }} {{ end }} + {{/* nginx-proxy/nginx-proxy#1105 */}} + # Fallback entry + server 127.0.0.1 down; {{ end }} } From 3785649eb60f2f6fc6f793ae66edc15737b2a539 Mon Sep 17 00:00:00 2001 From: Gilles Filippini Date: Fri, 28 May 2021 00:03:43 +0200 Subject: [PATCH 2/3] test: VIRTUAL_PORT != single exposed port --- ...TUAL_PORT-single-different-from-single-port.py | 8 ++++++++ ...UAL_PORT-single-different-from-single-port.yml | 15 +++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.py create mode 100644 test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.yml diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.py b/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.py new file mode 100644 index 0000000..4008166 --- /dev/null +++ b/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.py @@ -0,0 +1,8 @@ +import pytest +import re + + +def test_answer_is_served_from_virtual_port_which_is_ureachable(docker_compose, nginxproxy): + r = nginxproxy.get("http://web.nginx-proxy.tld/port") + assert r.status_code == 502 + assert re.search(r"\n\s+server \d+\.\d+\.\d+\.\d+:90;\n", nginxproxy.get_conf().decode('ASCII')) diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.yml b/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.yml new file mode 100644 index 0000000..e28a481 --- /dev/null +++ b/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.yml @@ -0,0 +1,15 @@ +web: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: "web.nginx-proxy.tld" + VIRTUAL_PORT: "90" + + +sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro From 2bb53bd303765159abfa24a57fe96f41459c9e65 Mon Sep 17 00:00:00 2001 From: Gilles Filippini Date: Fri, 28 May 2021 01:52:01 +0200 Subject: [PATCH 3/3] test: add tests for the DEBUG flag --- test/test_debug/test_proxy-debug-flag.py | 12 ++++++++++ test/test_debug/test_proxy-debug-flag.yml | 27 ++++++++++++++++++++++ test/test_debug/test_server-debug-flag.py | 8 +++++++ test/test_debug/test_server-debug-flag.yml | 26 +++++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 test/test_debug/test_proxy-debug-flag.py create mode 100644 test/test_debug/test_proxy-debug-flag.yml create mode 100644 test/test_debug/test_server-debug-flag.py create mode 100644 test/test_debug/test_server-debug-flag.yml diff --git a/test/test_debug/test_proxy-debug-flag.py b/test/test_debug/test_proxy-debug-flag.py new file mode 100644 index 0000000..b99ccef --- /dev/null +++ b/test/test_debug/test_proxy-debug-flag.py @@ -0,0 +1,12 @@ +import pytest +import re + +def test_debug_info_is_present_in_nginx_generated_conf(docker_compose, nginxproxy): + conf = nginxproxy.get_conf().decode('ASCII') + assert re.search(r"# Exposed ports: \[\{\d+\.\d+\.\d+\.\d+\s+80\s+tcp \} \{\d+\.\d+\.\d+\.\d+\s+81\s+tcp \}\]", conf) or \ + re.search(r"# Exposed ports: \[\{\d+\.\d+\.\d+\.\d+\s+81\s+tcp \} \{\d+\.\d+\.\d+\.\d+\s+80\s+tcp \}\]", conf) + assert re.search(r"# Exposed ports: \[\{\d+\.\d+\.\d+\.\d+\s+82\s+tcp \} \{\d+\.\d+\.\d+\.\d+\s+83\s+tcp \}\]", conf) or \ + re.search(r"# Exposed ports: \[\{\d+\.\d+\.\d+\.\d+\s+83\s+tcp \} \{\d+\.\d+\.\d+\.\d+\s+82\s+tcp \}\]", conf) + assert "# Default virtual port: 80" in conf + assert "# VIRTUAL_PORT: 82" in conf + assert conf.count("# /!\ Virtual port not exposed") == 1 diff --git a/test/test_debug/test_proxy-debug-flag.yml b/test/test_debug/test_proxy-debug-flag.yml new file mode 100644 index 0000000..e7af54c --- /dev/null +++ b/test/test_debug/test_proxy-debug-flag.yml @@ -0,0 +1,27 @@ +web1: + image: web + expose: + - "80" + - "81" + environment: + WEB_PORTS: "80 81" + VIRTUAL_HOST: "web1.nginx-proxy.tld" + VIRTUAL_PORT: "82" + +web2: + image: web + expose: + - "82" + - "83" + environment: + WEB_PORTS: "82 83" + VIRTUAL_HOST: "web2.nginx-proxy.tld" + VIRTUAL_PORT: "82" + +sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro + environment: + DEBUG: "true" diff --git a/test/test_debug/test_server-debug-flag.py b/test/test_debug/test_server-debug-flag.py new file mode 100644 index 0000000..50ae737 --- /dev/null +++ b/test/test_debug/test_server-debug-flag.py @@ -0,0 +1,8 @@ +import pytest +import re + +def test_debug_info_is_present_in_nginx_generated_conf(docker_compose, nginxproxy): + conf = nginxproxy.get_conf().decode('ASCII') + assert re.search(r"# Exposed ports: \[\{\d+\.\d+\.\d+\.\d+\s+80\s+tcp \} \{\d+\.\d+\.\d+\.\d+\s+81\s+tcp \}\]", conf) or \ + re.search(r"# Exposed ports: \[\{\d+\.\d+\.\d+\.\d+\s+81\s+tcp \} \{\d+\.\d+\.\d+\.\d+\s+80\s+tcp \}\]", conf) + assert conf.count("# Exposed ports: [{") == 1 diff --git a/test/test_debug/test_server-debug-flag.yml b/test/test_debug/test_server-debug-flag.yml new file mode 100644 index 0000000..0256cf8 --- /dev/null +++ b/test/test_debug/test_server-debug-flag.yml @@ -0,0 +1,26 @@ +web1: + image: web + expose: + - "80" + - "81" + environment: + WEB_PORTS: "80 81" + VIRTUAL_HOST: "web1.nginx-proxy.tld" + VIRTUAL_PORT: "82" + DEBUG: "true" + +web2: + image: web + expose: + - "82" + - "83" + environment: + WEB_PORTS: "82 83" + VIRTUAL_HOST: "web2.nginx-proxy.tld" + VIRTUAL_PORT: "82" + +sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro