1
0
mirror of https://github.com/thib8956/nginx-proxy synced 2025-02-24 01:38:15 +00:00

Merge pull request #1901 from nginx-proxy/dev

Path based routing (VIRTUAL_PATH)
This commit is contained in:
Nicolas Duchon 2022-02-24 16:15:50 +01:00 committed by GitHub
commit 55651bba8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 705 additions and 114 deletions

View File

@ -7,6 +7,7 @@ on:
push: push:
branches: branches:
- main - main
- dev
tags: tags:
- '*.*.*' - '*.*.*'
paths-ignore: paths-ignore:
@ -42,7 +43,8 @@ jobs:
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }}
labels: | labels: |
org.opencontainers.image.authors=Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag), Jason Wilder org.opencontainers.image.authors=Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag), Jason Wilder
org.opencontainers.image.version=${{ env.GIT_DESCRIBE }} org.opencontainers.image.version=${{ env.GIT_DESCRIBE }}
@ -60,6 +62,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push the Debian based image - name: Build and push the Debian based image
if: github.ref == 'refs/heads/main'
id: docker_build_debian id: docker_build_debian
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
@ -72,8 +75,25 @@ jobs:
labels: ${{ steps.docker_meta_debian.outputs.labels }} labels: ${{ steps.docker_meta_debian.outputs.labels }}
- name: Images digests - name: Images digests
if: github.ref == 'refs/heads/main'
run: echo ${{ steps.docker_build_debian.outputs.digest }} run: echo ${{ steps.docker_build_debian.outputs.digest }}
- name: Build and push the Debian based dev image
if: github.ref == 'refs/heads/dev'
id: docker_build_debian_dev
uses: docker/build-push-action@v2
with:
file: Dockerfile
build-args: DOCKER_GEN_VERSION=main
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.docker_meta_debian.outputs.tags }}
labels: ${{ steps.docker_meta_debian.outputs.labels }}
- name: Images digests
if: github.ref == 'refs/heads/dev'
run: echo ${{ steps.docker_build_debian_dev.outputs.digest }}
multiarch-build-alpine: multiarch-build-alpine:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -96,7 +116,8 @@ jobs:
tags: | tags: |
type=semver,suffix=-alpine,pattern={{version}} type=semver,suffix=-alpine,pattern={{version}}
type=semver,suffix=-alpine,pattern={{major}}.{{minor}} type=semver,suffix=-alpine,pattern={{major}}.{{minor}}
type=raw,value=alpine,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} type=raw,value=alpine,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=dev-alpine,enable=${{ github.ref == 'refs/heads/dev' }}
labels: | labels: |
org.opencontainers.image.authors=Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag), Jason Wilder org.opencontainers.image.authors=Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag), Jason Wilder
org.opencontainers.image.version=${{ env.GIT_DESCRIBE }} org.opencontainers.image.version=${{ env.GIT_DESCRIBE }}
@ -115,6 +136,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push the Alpine based image - name: Build and push the Alpine based image
if: github.ref == 'refs/heads/main'
id: docker_build_alpine id: docker_build_alpine
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
@ -127,4 +149,21 @@ jobs:
labels: ${{ steps.docker_meta_alpine.outputs.labels }} labels: ${{ steps.docker_meta_alpine.outputs.labels }}
- name: Images digests - name: Images digests
if: github.ref == 'refs/heads/main'
run: echo ${{ steps.docker_build_alpine.outputs.digest }} run: echo ${{ steps.docker_build_alpine.outputs.digest }}
- name: Build and push the Alpine based dev image
if: github.ref == 'refs/heads/dev'
id: docker_build_alpine_dev
uses: docker/build-push-action@v2
with:
file: Dockerfile.alpine
build-args: DOCKER_GEN_VERSION=main
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.docker_meta_alpine.outputs.tags }}
labels: ${{ steps.docker_meta_alpine.outputs.labels }}
- name: Images digests
if: github.ref == 'refs/heads/dev'
run: echo ${{ steps.docker_build_alpine_dev.outputs.digest }}

View File

@ -1,5 +1,5 @@
# setup build arguments for version of dependencies to use # setup build arguments for version of dependencies to use
ARG DOCKER_GEN_VERSION=0.7.7 ARG DOCKER_GEN_VERSION=main
ARG FOREGO_VERSION=v0.17.0 ARG FOREGO_VERSION=v0.17.0
# Use a specific version of golang to build both binaries # Use a specific version of golang to build both binaries

View File

@ -1,5 +1,5 @@
# setup build arguments for version of dependencies to use # setup build arguments for version of dependencies to use
ARG DOCKER_GEN_VERSION=0.7.7 ARG DOCKER_GEN_VERSION=main
ARG FOREGO_VERSION=v0.17.0 ARG FOREGO_VERSION=v0.17.0
# Use a specific version of golang to build both binaries # Use a specific version of golang to build both binaries

View File

@ -117,6 +117,50 @@ For each host defined into `VIRTUAL_HOST`, the associated virtual port is retrie
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).
### Path-based Routing
You can have multiple containers proxied by the same `VIRTUAL_HOST` by adding a `VIRTUAL_PATH` environment variable containing the absolute path to where the container should be mounted. For example with `VIRTUAL_HOST=foo.example.com` and `VIRTUAL_PATH=/api/v2/service`, then requests to http://foo.example.com/api/v2/service will be routed to the container. If you wish to have a container serve the root while other containers serve other paths, give the root container a `VIRTUAL_PATH` of `/`. Unmatched paths will be served by the container at `/` or will return the default nginx error page if no container has been assigned `/`.
It is also possible to specify multiple paths with regex locations like `VIRTUAL_PATH=~^/(app1|alternative1)/`. For further details see the nginx documentation on location blocks. This is not compatible with `VIRTUAL_DEST`.
The full request URI will be forwarded to the serving container in the `X-Forwarded-Path` header.
**NOTE**: Your application needs to be able to generate links starting with `VIRTUAL_PATH`. This can be achieved by it being natively on this path or having an option to prepend this path. The application does not need to expect this path in the request.
#### VIRTUAL_DEST
This environment variable can be used to rewrite the `VIRTUAL_PATH` part of the requested URL to proxied application. The default value is empty (off).
Make sure that your settings won't result in the slash missing or being doubled. Both these versions can cause troubles.
If the application runs natively on this sub-path or has a setting to do so, `VIRTUAL_DEST` should not be set or empty.
If the requests are expected to not contain a sub-path and the generated links contain the sub-path, `VIRTUAL_DEST=/` should be used.
```console
$ docker run -d -e VIRTUAL_HOST=example.tld -e VIRTUAL_PATH=/app1/ -e VIRTUAL_DEST=/ --name app1 app
```
In this example, the incoming request `http://example.tld/app1/foo` will be proxied as `http://app1/foo` instead of `http://app1/app1/foo`.
#### Per-VIRTUAL_PATH location configuration
The same options as from [Per-VIRTUAL_HOST location configuration](#Per-VIRTUAL_HOST-location-configuration) are available on a `VIRTUAL_PATH` basis.
The only difference is that the filename gets an additional block `HASH=$(echo -n $VIRTUAL_PATH | sha1sum | awk '{ print $1 }')`. This is the sha1-hash of the `VIRTUAL_PATH` (no newline). This is done filename sanitization purposes.
The used filename is `${VIRTUAL_HOST}_${HASH}_location`
The filename of the previous example would be `example.tld_8610f6c344b4096614eab6e09d58885349f42faf_location`.
#### DEFAULT_ROOT
This environment variable of the nginx proxy container can be used to customize the return error page if no matching path is found. Furthermore it is possible to use anything which is compatible with the `return` statement of nginx.
For example `DEFAUL_ROOT=418` will return a 418 error page instead of the normal 404 one.
Another example is `DEFAULT_ROOT="301 https://github.com/nginx-proxy/nginx-proxy/blob/main/README.md"` which would redirect an invalid request to this documentation.
Nginx variables such as $scheme, $host, and $request_uri can be used. However, care must be taken to make sure the $ signs are escaped properly.
If you want to use `301 $scheme://$host/myapp1$request_uri` you should use:
* Bash: `DEFAULT_ROOT='301 $scheme://$host/myapp1$request_uri'`
* Docker Compose yaml: `- DEFAULT_ROOT: 301 $$scheme://$$host/myapp1$$request_uri`
### Multiple Networks ### Multiple Networks
With the addition of [overlay networking](https://docs.docker.com/engine/userguide/networking/get-started-overlay/) in Docker 1.9, your `nginx-proxy` container may need to connect to backend containers on multiple networks. By default, if you don't pass the `--net` flag when your `nginx-proxy` container is created, it will only be attached to the default `bridge` network. This means that it will not be able to connect to containers on networks other than `bridge`. With the addition of [overlay networking](https://docs.docker.com/engine/userguide/networking/get-started-overlay/) in Docker 1.9, your `nginx-proxy` container may need to connect to backend containers on multiple networks. By default, if you don't pass the `--net` flag when your `nginx-proxy` container is created, it will only be attached to the default `bridge` network. This means that it will not be able to connect to containers on networks other than `bridge`.
@ -337,6 +381,7 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl; proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Forwarded-Path $request_uri;
# Mitigate httpoxy attack (see README for details) # Mitigate httpoxy attack (see README for details)
proxy_set_header Proxy ""; proxy_set_header Proxy "";

View File

@ -3,4 +3,5 @@ allow 127.0.0.0/8;
allow 10.0.0.0/8; allow 10.0.0.0/8;
allow 192.168.0.0/16; allow 192.168.0.0/16;
allow 172.16.0.0/12; allow 172.16.0.0/12;
allow fc00::/7; # IPv6 local address range
deny all; deny all;

View File

@ -5,6 +5,7 @@
{{ $external_https_port := coalesce $.Env.HTTPS_PORT "443" }} {{ $external_https_port := coalesce $.Env.HTTPS_PORT "443" }}
{{ $debug_all := $.Env.DEBUG }} {{ $debug_all := $.Env.DEBUG }}
{{ $sha1_upstream_name := parseBool (coalesce $.Env.SHA1_UPSTREAM_NAME "false") }} {{ $sha1_upstream_name := parseBool (coalesce $.Env.SHA1_UPSTREAM_NAME "false") }}
{{ $default_root_response := coalesce $.Env.DEFAULT_ROOT "404" }}
{{ define "ssl_policy" }} {{ define "ssl_policy" }}
{{ if eq .ssl_policy "Mozilla-Modern" }} {{ if eq .ssl_policy "Mozilla-Modern" }}
@ -49,6 +50,99 @@
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ define "location" }}
location {{ .Path }} {
{{ if eq .NetworkTag "internal" }}
# Only allow traffic from internal clients
include /etc/nginx/network_internal.conf;
{{ end }}
{{ if eq .Proto "uwsgi" }}
include uwsgi_params;
uwsgi_pass {{ trim .Proto }}://{{ trim .Upstream }};
{{ else if eq .Proto "fastcgi" }}
root {{ trim .VhostRoot }};
include fastcgi_params;
fastcgi_pass {{ trim .Upstream }};
{{ else if eq .Proto "grpc" }}
grpc_pass {{ trim .Proto }}://{{ trim .Upstream }};
{{ else }}
proxy_pass {{ trim .Proto }}://{{ trim .Upstream }}{{ trim .Dest }};
{{ end }}
{{ if (exists (printf "/etc/nginx/htpasswd/%s" .Host)) }}
auth_basic "Restricted {{ .Host }}";
auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" .Host) }};
{{ end }}
{{ if (exists (printf "/etc/nginx/vhost.d/%s_%s_location" .Host (sha1 .Path) )) }}
include {{ printf "/etc/nginx/vhost.d/%s_%s_location" .Host (sha1 .Path) }};
{{ else if (exists (printf "/etc/nginx/vhost.d/%s_location" .Host)) }}
include {{ printf "/etc/nginx/vhost.d/%s_location" .Host}};
{{ else if (exists "/etc/nginx/vhost.d/default_location") }}
include /etc/nginx/vhost.d/default_location;
{{ end }}
}
{{ end }}
{{ define "upstream" }}
{{ $networks := .Networks }}
{{ $debug_all := .Debug }}
upstream {{ .Upstream }} {
{{ $server_found := "false" }}
{{ range $container := .Containers }}
{{ $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 }}
{{ $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 }}
{{ range $knownNetwork := $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 $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 }}
{{ $server_found = "true" }}
# {{ $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 $containerNetwork }}
{{ $server_found = "true" }}
# {{ $container.Name }}
server {{ $containerNetwork.IP }}:{{ $address.Port }};
{{ end }}
{{ else if $containerNetwork }}
# {{ $container.Name }}
{{ if $containerNetwork.IP }}
{{ $server_found = "true" }}
server {{ $containerNetwork.IP }}:{{ $port }};
{{ else }}
# /!\ No IP for this network!
{{ end }}
{{ end }}
{{ else }}
# Cannot connect to network '{{ $containerNetwork.Name }}' of this container
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{/* nginx-proxy/nginx-proxy#1105 */}}
{{ if (eq $server_found "false") }}
# Fallback entry
server 127.0.0.1 down;
{{ end }}
}
{{ end }}
{{ if ne $nginx_proxy_version "" }} {{ if ne $nginx_proxy_version "" }}
# nginx-proxy version : {{ $nginx_proxy_version }} # nginx-proxy version : {{ $nginx_proxy_version }}
{{ end }} {{ end }}
@ -100,6 +194,7 @@ access_log off;
{{/* Get the SSL_POLICY defined by this container, falling back to "Mozilla-Intermediate" */}} {{/* Get the SSL_POLICY defined by this container, falling back to "Mozilla-Intermediate" */}}
{{ $ssl_policy := or ($.Env.SSL_POLICY) "Mozilla-Intermediate" }} {{ $ssl_policy := or ($.Env.SSL_POLICY) "Mozilla-Intermediate" }}
{{ template "ssl_policy" (dict "ssl_policy" $ssl_policy) }} {{ template "ssl_policy" (dict "ssl_policy" $ssl_policy) }}
error_log /dev/stderr;
{{ if $.Env.RESOLVERS }} {{ if $.Env.RESOLVERS }}
resolver {{ $.Env.RESOLVERS }}; resolver {{ $.Env.RESOLVERS }};
@ -119,6 +214,7 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl; proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Original-URI $request_uri;
# Mitigate httpoxy attack (see README for details) # Mitigate httpoxy attack (see README for details)
proxy_set_header Proxy ""; proxy_set_header Proxy "";
@ -162,73 +258,27 @@ server {
{{ $is_regexp := hasPrefix "~" $host }} {{ $is_regexp := hasPrefix "~" $host }}
{{ $upstream_name := when (or $is_regexp $sha1_upstream_name) (sha1 $host) $host }} {{ $upstream_name := when (or $is_regexp $sha1_upstream_name) (sha1 $host) $host }}
# {{ $host }} {{ $paths := groupBy $containers "Env.VIRTUAL_PATH" }}
upstream {{ $upstream_name }} { {{ $nPaths := len $paths }}
{{ $server_found := "false" }} {{ if eq $nPaths 0 }}
{{ range $container := $containers }} # {{ $host }}
{{ $debug := (eq (coalesce $container.Env.DEBUG $debug_all "false") "true") }} {{ template "upstream" (dict "Upstream" $upstream_name "Containers" $containers "Networks" $CurrentContainer.Networks "Debug" $debug_all) }}
{{/* If only 1 port exposed, use that as a default, else 80 */}} {{ else }}
{{ $defaultPort := (when (eq (len $container.Addresses) 1) (first $container.Addresses) (dict "Port" "80")).Port }} {{ range $path, $containers := $paths }}
{{ $port := (coalesce $container.Env.VIRTUAL_PORT $defaultPort) }} {{ $sum := sha1 $path }}
{{ $address := where $container.Addresses "Port" $port | first }} {{ $upstream := printf "%s-%s" $upstream_name $sum }}
{{ if $debug }} # {{ $host }}{{ $path }}
# Exposed ports: {{ $container.Addresses }} {{ template "upstream" (dict "Upstream" $upstream "Containers" $containers "Networks" $CurrentContainer.Networks "Debug" $debug_all) }}
# Default virtual port: {{ $defaultPort }}
# VIRTUAL_PORT: {{ $container.Env.VIRTUAL_PORT }}
{{ if not $address }}
# /!\ Virtual port not exposed
{{ end }}
{{ end }}
{{ 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 $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 }}
{{ $server_found = "true" }}
# {{ $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 $containerNetwork }}
{{ $server_found = "true" }}
# {{ $container.Name }}
server {{ $containerNetwork.IP }}:{{ $address.Port }};
{{ end }}
{{ else if $containerNetwork }}
# {{ $container.Name }}
{{ if $containerNetwork.IP }}
{{ $server_found = "true" }}
server {{ $containerNetwork.IP }}:{{ $port }};
{{ else }}
# /!\ No IP for this network!
{{ end }}
{{ end }}
{{ else }}
# Cannot connect to network '{{ $containerNetwork.Name }}' of this container
{{ end }}
{{ end }}
{{ end }} {{ end }}
{{ end }} {{ end }}
{{/* nginx-proxy/nginx-proxy#1105 */}}
{{ if (eq $server_found "false") }}
# Fallback entry
server 127.0.0.1 down;
{{ end }}
}
{{ $default_host := or ($.Env.DEFAULT_HOST) "" }} {{ $default_host := or ($.Env.DEFAULT_HOST) "" }}
{{ $default_server := index (dict $host "" $default_host "default_server") $host }} {{ $default_server := index (dict $host "" $default_host "default_server") $host }}
{{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}}
{{ $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }}
{{/* Get the SERVER_TOKENS defined by containers w/ the same vhost, falling back to "" */}} {{/* Get the SERVER_TOKENS defined by containers w/ the same vhost, falling back to "" */}}
{{ $server_tokens := trim (or (first (groupByKeys $containers "Env.SERVER_TOKENS")) "") }} {{ $server_tokens := trim (or (first (groupByKeys $containers "Env.SERVER_TOKENS")) "") }}
{{/* 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" }}
{{/* Get the HTTPS_METHOD defined by containers w/ the same vhost, falling back to "redirect" */}} {{/* Get the HTTPS_METHOD defined by containers w/ the same vhost, falling back to "redirect" */}}
{{ $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) (or $.Env.HTTPS_METHOD "redirect") }} {{ $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) (or $.Env.HTTPS_METHOD "redirect") }}
@ -303,11 +353,6 @@ server {
{{ end }} {{ end }}
{{ $access_log }} {{ $access_log }}
{{ if eq $network_tag "internal" }}
# Only allow traffic from internal clients
include /etc/nginx/network_internal.conf;
{{ end }}
{{ template "ssl_policy" (dict "ssl_policy" $ssl_policy) }} {{ template "ssl_policy" (dict "ssl_policy" $ssl_policy) }}
ssl_session_timeout 5m; ssl_session_timeout 5m;
@ -337,30 +382,31 @@ server {
include /etc/nginx/vhost.d/default; include /etc/nginx/vhost.d/default;
{{ end }} {{ end }}
location / { {{ if eq $nPaths 0 }}
{{ if eq $proto "uwsgi" }} {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}}
include uwsgi_params; {{ $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }}
uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }};
{{ else if eq $proto "fastcgi" }}
root {{ trim $vhost_root }};
include fastcgi_params;
fastcgi_pass {{ trim $upstream_name }};
{{ else if eq $proto "grpc" }}
grpc_pass {{ trim $proto }}://{{ trim $upstream_name }};
{{ else }}
proxy_pass {{ trim $proto }}://{{ trim $upstream_name }};
{{ end }}
{{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} {{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}}
auth_basic "Restricted {{ $host }}"; {{ $network_tag := or (first (groupByKeys $containers "Env.NETWORK_ACCESS")) "external" }}
auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; {{ template "location" (dict "Path" "/" "Proto" $proto "Upstream" $upstream_name "Host" $host "VhostRoot" $vhost_root "Dest" "" "NetworkTag" $network_tag) }}
{{ else }}
{{ range $path, $container := $paths }}
{{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, falling back to "http" */}}
{{ $proto := trim (or (first (groupByKeys $container "Env.VIRTUAL_PROTO")) "http") }}
{{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}}
{{ $network_tag := or (first (groupByKeys $container "Env.NETWORK_ACCESS")) "external" }}
{{ $sum := sha1 $path }}
{{ $upstream := printf "%s-%s" $upstream_name $sum }}
{{ $dest := (or (first (groupByKeys $container "Env.VIRTUAL_DEST")) "") }}
{{ template "location" (dict "Path" $path "Proto" $proto "Upstream" $upstream "Host" $host "VhostRoot" $vhost_root "Dest" $dest "NetworkTag" $network_tag) }}
{{ end }} {{ end }}
{{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }} {{ if (not (contains $paths "/")) }}
include {{ printf "/etc/nginx/vhost.d/%s_location" $host}}; location / {
{{ else if (exists "/etc/nginx/vhost.d/default_location") }} return {{ $default_root_response }};
include /etc/nginx/vhost.d/default_location; }
{{ end }} {{ end }}
} {{ end }}
} }
{{ end }} {{ end }}
@ -378,40 +424,37 @@ server {
{{ end }} {{ end }}
{{ $access_log }} {{ $access_log }}
{{ if eq $network_tag "internal" }}
# Only allow traffic from internal clients
include /etc/nginx/network_internal.conf;
{{ end }}
{{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }} {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }}
include {{ printf "/etc/nginx/vhost.d/%s" $host }}; include {{ printf "/etc/nginx/vhost.d/%s" $host }};
{{ else if (exists "/etc/nginx/vhost.d/default") }} {{ else if (exists "/etc/nginx/vhost.d/default") }}
include /etc/nginx/vhost.d/default; include /etc/nginx/vhost.d/default;
{{ end }} {{ end }}
location / { {{ if eq $nPaths 0 }}
{{ if eq $proto "uwsgi" }} {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}}
include uwsgi_params; {{ $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }}
uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }};
{{ else if eq $proto "fastcgi" }} {{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}}
root {{ trim $vhost_root }}; {{ $network_tag := or (first (groupByKeys $containers "Env.NETWORK_ACCESS")) "external" }}
include fastcgi_params; {{ template "location" (dict "Path" "/" "Proto" $proto "Upstream" $upstream_name "Host" $host "VhostRoot" $vhost_root "Dest" "" "NetworkTag" $network_tag) }}
fastcgi_pass {{ trim $upstream_name }}; {{ else }}
{{ else if eq $proto "grpc" }} {{ range $path, $container := $paths }}
grpc_pass {{ trim $proto }}://{{ trim $upstream_name }}; {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, falling back to "http" */}}
{{ else }} {{ $proto := trim (or (first (groupByKeys $container "Env.VIRTUAL_PROTO")) "http") }}
proxy_pass {{ trim $proto }}://{{ trim $upstream_name }};
{{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}}
{{ $network_tag := or (first (groupByKeys $container "Env.NETWORK_ACCESS")) "external" }}
{{ $sum := sha1 $path }}
{{ $upstream := printf "%s-%s" $upstream_name $sum }}
{{ $dest := (or (first (groupByKeys $container "Env.VIRTUAL_DEST")) "") }}
{{ template "location" (dict "Path" $path "Proto" $proto "Upstream" $upstream "Host" $host "VhostRoot" $vhost_root "Dest" $dest "NetworkTag" $network_tag) }}
{{ end }} {{ end }}
{{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} {{ if (not (contains $paths "/")) }}
auth_basic "Restricted {{ $host }}"; location / {
auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; return {{ $default_root_response }};
}
{{ end }} {{ end }}
{{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }} {{ end }}
include {{ printf "/etc/nginx/vhost.d/%s_location" $host}};
{{ else if (exists "/etc/nginx/vhost.d/default_location") }}
include /etc/nginx/vhost.d/default_location;
{{ end }}
}
} }
{{ if (and (not $is_https) (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} {{ if (and (not $is_https) (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }}

View File

@ -29,13 +29,36 @@ def web1(docker_compose):
except NotFound: except NotFound:
pass pass
@pytest.fixture()
def web2(docker_compose):
"""
pytest fixture creating a web container with `VIRTUAL_HOST=nginx-proxy`, `VIRTUAL_PATH=/web2/` and `VIRTUAL_DEST=/` listening on port 82.
"""
container = docker_compose.containers.run(
name="web2",
image="web",
detach=True,
environment={
"WEB_PORTS": "82",
"VIRTUAL_HOST": "nginx-proxy",
"VIRTUAL_PATH": "/web2/",
"VIRTUAL_DEST": "/",
},
ports={"82/tcp": None}
)
sleep(2) # give it some time to initialize and for docker-gen to detect it
yield container
try:
docker_compose.containers.get("web2").remove(force=True)
except NotFound:
pass
def test_nginx_proxy_behavior_when_alone(docker_compose, nginxproxy): def test_nginx_proxy_behavior_when_alone(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/") r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503 assert r.status_code == 503
def test_new_container_is_detected(web1, nginxproxy): def test_new_container_is_detected_vhost(web1, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy/port") r = nginxproxy.get("http://web1.nginx-proxy/port")
assert r.status_code == 200 assert r.status_code == 200
assert "answer from port 81\n" == r.text assert "answer from port 81\n" == r.text
@ -44,3 +67,16 @@ def test_new_container_is_detected(web1, nginxproxy):
sleep(2) sleep(2)
r = nginxproxy.get("http://web1.nginx-proxy/port") r = nginxproxy.get("http://web1.nginx-proxy/port")
assert r.status_code == 503 assert r.status_code == 503
def test_new_container_is_detected_vpath(web2, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/web2/port")
assert r.status_code == 200
assert "answer from port 82\n" == r.text
r = nginxproxy.get("http://nginx-proxy/port")
assert r.status_code in [404, 503]
web2.remove(force=True)
sleep(2)
r = nginxproxy.get("http://nginx-proxy/web2/port")
assert r.status_code == 503

View File

@ -0,0 +1,11 @@
# Only allow traffic from internal clients
allow 127.0.0.0/8;
allow 10.0.0.0/8;
allow 192.168.0.0/16;
allow 172.16.0.0/12;
allow fc00::/7; # IPv6 local address range
deny all;
# Dummy header for testing
add_header X-network internal;

View File

@ -0,0 +1,14 @@
import pytest
def test_network_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-network" in r.headers
assert "internal" == r.headers["X-network"]
def test_network_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-network" not in r.headers

View File

@ -0,0 +1,23 @@
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: web1.nginx-proxy.local
NETWORK_ACCESS: internal
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: 82
VIRTUAL_HOST: web2.nginx-proxy.local
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./network_internal.conf:/etc/nginx/network_internal.conf:ro

View File

@ -0,0 +1,14 @@
import pytest
def test_network_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy.local/web1/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-network" in r.headers
assert "internal" == r.headers["X-network"]
def test_network_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy.local/web2/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-network" not in r.headers

View File

@ -0,0 +1,27 @@
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: nginx-proxy.local
VIRTUAL_PATH: /web1/
VIRTUAL_DEST: /
NETWORK_ACCESS: internal
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: 82
VIRTUAL_HOST: nginx-proxy.local
VIRTUAL_PATH: /web2/
VIRTUAL_DEST: /
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./network_internal.conf:/etc/nginx/network_internal.conf:ro

View File

@ -0,0 +1,15 @@
import pytest
@pytest.mark.parametrize("path", ["web1", "web2"])
def test_web1_http_redirects_to_https(docker_compose, nginxproxy, path):
r = nginxproxy.get("http://www.nginx-proxy.tld/%s/port" % path, allow_redirects=False)
assert r.status_code == 301
assert "Location" in r.headers
assert "https://www.nginx-proxy.tld/%s/port" % path == r.headers['Location']
@pytest.mark.parametrize("path,port", [("web1", 81), ("web2", 82)])
def test_web1_https_is_forwarded(docker_compose, nginxproxy, path, port):
r = nginxproxy.get("https://www.nginx-proxy.tld/%s/port" % path, allow_redirects=False)
assert r.status_code == 200
assert "answer from port %d\n" % port in r.text

View File

@ -0,0 +1,26 @@
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: "www.nginx-proxy.tld"
VIRTUAL_PATH: "/web1/"
VIRTUAL_DEST: "/"
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: "www.nginx-proxy.tld"
VIRTUAL_PATH: "/web2/"
VIRTUAL_DEST: "/"
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./certs:/etc/nginx/certs:ro

View File

@ -0,0 +1 @@
rewrite ^/(web3|alt)/(.*) /$2 break;

View File

@ -0,0 +1 @@
add_header X-test bar;

View File

@ -0,0 +1 @@
add_header X-test-default true;

View File

@ -0,0 +1 @@
add_header X-test f00;

View File

@ -0,0 +1 @@
add_header X-test-host true;

View File

@ -0,0 +1 @@
add_header X-test-path true;

View File

@ -0,0 +1,38 @@
import pytest
def test_default_root_response(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy.test/")
assert r.status_code == 418
@pytest.mark.parametrize("stub,header", [
("nginx-proxy.test/web1", "bar"),
("foo.nginx-proxy.test", "f00"),
])
def test_custom_applies(docker_compose, nginxproxy, stub, header):
r = nginxproxy.get(f"http://{stub}/port")
assert r.status_code == 200
assert "X-test" in r.headers
assert header == r.headers["X-test"]
@pytest.mark.parametrize("stub,code", [
("nginx-proxy.test/foo", 418),
("nginx-proxy.test/web2", 200),
("nginx-proxy.test/web3", 200),
("bar.nginx-proxy.test", 503),
])
def test_custom_does_not_apply(docker_compose, nginxproxy, stub, code):
r = nginxproxy.get(f"http://{stub}/port")
assert r.status_code == code
assert "X-test" not in r.headers
@pytest.mark.parametrize("stub,port", [
("nginx-proxy.test/web1", 81),
("nginx-proxy.test/web2", 82),
("nginx-proxy.test/web3", 83),
("nginx-proxy.test/alt", 83),
])
def test_alternate(docker_compose, nginxproxy, stub, port):
r = nginxproxy.get(f"http://{stub}/port")
assert r.status_code == 200
assert r.text == f"answer from port {port}\n"

View File

@ -0,0 +1,48 @@
foo:
image: web
expose:
- "42"
environment:
WEB_PORTS: "42"
VIRTUAL_HOST: "foo.nginx-proxy.test"
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: "nginx-proxy.test"
VIRTUAL_PATH: "/web1/"
VIRTUAL_DEST: "/"
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: "nginx-proxy.test"
VIRTUAL_PATH: "/web2/"
VIRTUAL_DEST: "/"
web3:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: "nginx-proxy.test"
VIRTUAL_PATH: "~ ^/(web3|alt)/"
sut:
image: nginxproxy/nginx-proxy:test
environment:
DEFAULT_ROOT: 418
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./foo.conf:/etc/nginx/vhost.d/foo.nginx-proxy.test:ro
- ./bar.conf:/etc/nginx/vhost.d/nginx-proxy.test_918d687a929083edd0c7224ee2293e0e7c062ab4_location:ro
- ./alternate.conf:/etc/nginx/vhost.d/nginx-proxy.test_7fb22b74bbdf907425dbbad18e4462565cada230_location:ro

View File

@ -0,0 +1,18 @@
import pytest
def test_root_redirects_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://www.nginx-proxy.tld/port", allow_redirects=False)
assert r.status_code == 301
assert "Location" in r.headers
assert "http://www.nginx-proxy.tld/web1/port" == r.headers['Location']
def test_direct_access(docker_compose, nginxproxy):
r = nginxproxy.get("http://www.nginx-proxy.tld/web1/port", allow_redirects=False)
assert r.status_code == 200
assert "answer from port 81\n" in r.text
def test_root_is_forwarded(docker_compose, nginxproxy):
r = nginxproxy.get("http://www.nginx-proxy.tld/port", allow_redirects=True)
assert r.status_code == 200
assert "answer from port 81\n" in r.text

View File

@ -0,0 +1,17 @@
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: "www.nginx-proxy.tld"
VIRTUAL_PATH: "/web1/"
VIRTUAL_DEST: "/"
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./certs:/etc/nginx/certs:ro
environment:
- DEFAULT_ROOT=301 http://$$host/web1$$request_uri

View File

@ -0,0 +1,32 @@
import pytest
def test_location_precedence_case1(docker_compose, nginxproxy):
r = nginxproxy.get(f"http://foo.nginx-proxy.test/web1/port")
assert r.status_code == 200
assert "X-test-default" in r.headers
assert "X-test-host" not in r.headers
assert "X-test-path" not in r.headers
assert r.headers["X-test-default"] == "true"
def test_location_precedence_case2(docker_compose, nginxproxy):
r = nginxproxy.get(f"http://bar.nginx-proxy.test/web2/port")
assert r.status_code == 200
assert "X-test-default" not in r.headers
assert "X-test-host" in r.headers
assert "X-test-path" not in r.headers
assert r.headers["X-test-host"] == "true"
def test_location_precedence_case3(docker_compose, nginxproxy):
r = nginxproxy.get(f"http://bar.nginx-proxy.test/web3/port")
assert r.status_code == 200
assert "X-test-default" not in r.headers
assert "X-test-host" not in r.headers
assert "X-test-path" in r.headers
assert r.headers["X-test-path"] == "true"

View File

@ -0,0 +1,37 @@
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: "foo.nginx-proxy.test"
VIRTUAL_PATH: "/web1/"
VIRTUAL_DEST: "/"
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: "bar.nginx-proxy.test"
VIRTUAL_PATH: "/web2/"
VIRTUAL_DEST: "/"
web3:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: "bar.nginx-proxy.test"
VIRTUAL_PATH: "/web3/"
VIRTUAL_DEST: "/"
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./default.conf:/etc/nginx/vhost.d/default_location:ro
- ./host.conf:/etc/nginx/vhost.d/bar.nginx-proxy.test_location:ro
- ./path.conf:/etc/nginx/vhost.d/bar.nginx-proxy.test_99f2db0ed8aa95dbb5b87fca79c7eff2ff6bb8bd_location:ro

View File

@ -0,0 +1,59 @@
from time import sleep
import pytest
from docker.errors import NotFound
@pytest.mark.parametrize("stub,expected_port", [
("nginx-proxy.test/web1", 81),
("nginx-proxy.test/web2", 82),
("nginx-proxy.test", 83),
("foo.nginx-proxy.test", 42),
])
def test_valid_path(docker_compose, nginxproxy, stub, expected_port):
r = nginxproxy.get(f"http://{stub}/port")
assert r.status_code == 200
assert r.text == f"answer from port {expected_port}\n"
@pytest.mark.parametrize("stub", [
"nginx-proxy.test/foo",
"bar.nginx-proxy.test",
])
def test_invalid_path(docker_compose, nginxproxy, stub):
r = nginxproxy.get(f"http://{stub}/port")
assert r.status_code in [404, 503]
@pytest.fixture()
def web4(docker_compose):
"""
pytest fixture creating a web container with `VIRTUAL_HOST=nginx-proxy.test`, `VIRTUAL_PATH=/web4/` and `VIRTUAL_DEST=/` listening on port 84.
"""
container = docker_compose.containers.run(
name="web4",
image="web",
detach=True,
environment={
"WEB_PORTS": "84",
"VIRTUAL_HOST": "nginx-proxy.test",
"VIRTUAL_PATH": "/web4/",
"VIRTUAL_DEST": "/",
},
ports={"84/tcp": None}
)
sleep(2) # give it some time to initialize and for docker-gen to detect it
yield container
try:
docker_compose.containers.get("web4").remove(force=True)
except NotFound:
pass
"""
Test if we can add and remove a single virtual_path from multiple ones on the same subdomain.
"""
def test_container_hotplug(web4, nginxproxy):
r = nginxproxy.get(f"http://nginx-proxy.test/web4/port")
assert r.status_code == 200
assert r.text == f"answer from port 84\n"
web4.remove(force=True)
sleep(2)
r = nginxproxy.get(f"http://nginx-proxy.test/web4/port")
assert r.status_code == 404

View File

@ -0,0 +1,42 @@
foo:
image: web
expose:
- "42"
environment:
WEB_PORTS: "42"
VIRTUAL_HOST: "foo.nginx-proxy.test"
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: "nginx-proxy.test"
VIRTUAL_PATH: "/web1/"
VIRTUAL_DEST: "/"
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: "nginx-proxy.test"
VIRTUAL_PATH: "/web2/"
VIRTUAL_DEST: "/"
web3:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: "nginx-proxy.test"
VIRTUAL_PATH: "/"
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro