diff --git a/README.md b/README.md index c44bf80..5997a46 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,12 @@ If you need to support multiple virtual hosts for a container, you can separate 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). +### 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, make 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 `/`. + +The full request URI will be forwarded to the serving container in the `X-Forwarded-Path` header. + ### 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`. @@ -127,11 +133,11 @@ backend container. Your backend container should then listen on a port rather than a socket and expose that port. ### FastCGI Backends - + If you would like to connect to FastCGI backend, set `VIRTUAL_PROTO=fastcgi` on the backend container. Your backend container should then listen on a port rather than a socket and expose that port. - + ### FastCGI Filr Root Directory If you use fastcgi,you can set `VIRTUAL_ROOT=xxx` for your root directory @@ -180,7 +186,7 @@ Finally, start your containers with `VIRTUAL_HOST` environment variables. $ docker run -e VIRTUAL_HOST=foo.bar.com ... ### SSL Support using letsencrypt -[letsencrypt-nginx-proxy-companion](https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion) is a lightweight companion container for the nginx-proxy. It allow the creation/renewal of Let's Encrypt certificates automatically. +[letsencrypt-nginx-proxy-companion](https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion) is a lightweight companion container for the nginx-proxy. It allow the creation/renewal of Let's Encrypt certificates automatically. ### SSL Support @@ -213,7 +219,7 @@ at startup. Since it can take minutes to generate a new `dhparam.pem`, it is do background. Once generation is complete, the `dhparam.pem` is saved on a persistent volume and nginx is reloaded. This generation process only occurs the first time you start `nginx-proxy`. -> COMPATIBILITY WARNING: The default generated `dhparam.pem` key is 2048 bits for A+ security. Some +> COMPATIBILITY WARNING: The default generated `dhparam.pem` key is 2048 bits for A+ security. Some > older clients (like Java 6 and 7) do not support DH keys with over 1024 bits. In order to support these > clients, you must either provide your own `dhparam.pem`, or tell `nginx-proxy` to generate a 1024-bit > key on startup by passing `-e DHPARAM_BITS=1024`. @@ -272,19 +278,19 @@ a 500. To serve traffic in both SSL and non-SSL modes without redirecting to SSL, you can include the environment variable `HTTPS_METHOD=noredirect` (the default is `HTTPS_METHOD=redirect`). You can also -disable the non-SSL site entirely with `HTTPS_METHOD=nohttp`, or disable the HTTPS site with -`HTTPS_METHOD=nohttps`. `HTTPS_METHOD` must be specified on each container for which you want to -override the default behavior. If `HTTPS_METHOD=noredirect` is used, Strict Transport Security (HSTS) -is disabled to prevent HTTPS users from being redirected by the client. If you cannot get to the HTTP -site after changing this setting, your browser has probably cached the HSTS policy and is automatically -redirecting you back to HTTPS. You will need to clear your browser's HSTS cache or use an incognito +disable the non-SSL site entirely with `HTTPS_METHOD=nohttp`, or disable the HTTPS site with +`HTTPS_METHOD=nohttps`. `HTTPS_METHOD` must be specified on each container for which you want to +override the default behavior. If `HTTPS_METHOD=noredirect` is used, Strict Transport Security (HSTS) +is disabled to prevent HTTPS users from being redirected by the client. If you cannot get to the HTTP +site after changing this setting, your browser has probably cached the HSTS policy and is automatically +redirecting you back to HTTPS. You will need to clear your browser's HSTS cache or use an incognito window / different browser. -By default, [HTTP Strict Transport Security (HSTS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) -is enabled with `max-age=31536000` for HTTPS sites. You can disable HSTS with the environment variable -`HSTS=off` or use a custom HSTS configuration like `HSTS=max-age=31536000; includeSubDomains; preload`. -*WARNING*: HSTS will force your users to visit the HTTPS version of your site for the `max-age` time - -even if they type in `http://` manually. The only way to get to an HTTP site after receiving an HSTS +By default, [HTTP Strict Transport Security (HSTS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) +is enabled with `max-age=31536000` for HTTPS sites. You can disable HSTS with the environment variable +`HSTS=off` or use a custom HSTS configuration like `HSTS=max-age=31536000; includeSubDomains; preload`. +*WARNING*: HSTS will force your users to visit the HTTPS version of your site for the `max-age` time - +even if they type in `http://` manually. The only way to get to an HTTP site after receiving an HSTS response is to clear your browser's HSTS cache. ### Basic Authentication Support @@ -323,6 +329,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-Ssl $proxy_x_forwarded_ssl; 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) proxy_set_header Proxy ""; @@ -400,7 +407,7 @@ Before submitting pull requests or issues, please check github to make sure an e To run tests, you need to prepare the docker image to test which must be tagged `jwilder/nginx-proxy:test`: docker build -t jwilder/nginx-proxy:test . # build the Debian variant image - + and call the [test/pytest.sh](test/pytest.sh) script. Then build the Alpine variant of the image: @@ -413,6 +420,6 @@ and call the [test/pytest.sh](test/pytest.sh) script again. If your system has the `make` command, you can automate those tasks by calling: make test - + You can learn more about how the test suite works and how to write new tests in the [test/README.md](test/README.md) file. diff --git a/nginx.tmpl b/nginx.tmpl index 30c08b9..713df5a 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -17,6 +17,59 @@ {{ end }} {{ end }} +{{ define "location" }} +location {{ .Path }} { + {{ if eq .Proto "uwsgi" }} + include uwsgi_params; + uwsgi_pass {{ trim .Proto }}://{{ trim .Upstream }}; + {{ else if eq (trim .Proto) "fastcgi" }} + root {{ trim .VHostRoot }}; + include fastcgi.conf; + fastcgi_pass {{ trim .Upstream }}; + {{ else }} + proxy_pass {{ trim .Proto }}://{{ trim .Upstream }}/; + {{ 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_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-definition" }} + upstream {{ .Upstream }} { + {{ range $container := .Containers }} + {{ $addrLen := len $container.Addresses }} + {{ $networks := .Networks }} + {{ 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 connect 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) }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} + } +{{ 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 { @@ -59,6 +112,7 @@ log_format vhost '$host $remote_addr - $remote_user [$time_local] ' '"$http_referer" "$http_user_agent"'; access_log off; +error_log /dev/stderr; {{ if $.Env.RESOLVERS }} resolver {{ $.Env.RESOLVERS }}; @@ -78,6 +132,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-Ssl $proxy_x_forwarded_ssl; 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) proxy_set_header Proxy ""; @@ -112,36 +167,20 @@ server { {{ range $host, $containers := groupByMulti $ "Env.VIRTUAL_HOST" "," }} -{{ $host := trim $host }} -{{ $is_regexp := hasPrefix "~" $host }} -{{ $upstream_name := when $is_regexp (sha1 $host) $host }} +{{ $paths := groupBy $containers "Env.VIRTUAL_PATH" }} +{{ $nPaths := len $paths }} -# {{ $host }} -upstream {{ $upstream_name }} { - -{{ range $container := $containers }} - {{ $addrLen := len $container.Addresses }} - - {{ 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 connect 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) }} - {{ end }} - {{ end }} - {{ end }} +{{ if eq $nPaths 0 }} + # {{ $host }} + {{ template "upstream-definition" (dict "Upstream" $host "Containers" $containers "Networks" (json $CurrentContainer.Networks)) }} +{{ else }} + {{ range $path, $containers := $paths }} + {{ $sum := sha1 $path }} + {{ $upstream := printf "%s-%s" $host $sum }} + # {{ $host }}{{ $path }} + {{ template "upstream-definition" (dict "Upstream" $upstream "Containers" $containers "Networks" $CurrentContainer.Networks) }} {{ end }} {{ end }} -} {{ $default_host := or ($.Env.DEFAULT_HOST) "" }} {{ $default_server := index (dict $host "" $default_host "default_server") $host }} @@ -267,28 +306,15 @@ server { include /etc/nginx/vhost.d/default; {{ end }} - location / { - {{ if eq $proto "uwsgi" }} - include uwsgi_params; - uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }}; - {{ else if eq $proto "fastcgi" }} - root {{ trim $vhost_root }}; - include fastcgi.conf; - fastcgi_pass {{ trim $upstream_name }}; - {{ else }} - proxy_pass {{ trim $proto }}://{{ trim $upstream_name }}; + {{ if eq $nPaths 0 }} + {{ template "location" (dict "Path" "/" "Proto" $proto "Upstream" $host "Host" $host "VHostRoot" $vhost_root) }} + {{ else }} + {{ range $path, $containers := $paths }} + {{ $sum := sha1 $path }} + {{ $upstream := printf "%s-%s" $host $sum }} + {{ template "location" (dict "Path" $path "Proto" $proto "Upstream" $upstream "Host" $host "VHostRoot" $vhost_root) }} {{ 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_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 }} } {{ end }} @@ -314,27 +340,15 @@ server { include /etc/nginx/vhost.d/default; {{ end }} - location / { - {{ if eq $proto "uwsgi" }} - include uwsgi_params; - uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }}; - {{ else if eq $proto "fastcgi" }} - root {{ trim $vhost_root }}; - include fastcgi.conf; - fastcgi_pass {{ trim $upstream_name }}; - {{ else }} - proxy_pass {{ trim $proto }}://{{ trim $upstream_name }}; + {{ if eq $nPaths 0 }} + {{ template "location" (dict "Path" "/" "Proto" $proto "Upstream" $host "Host" $host "VHostRoot" $vhost_root) }} + {{ else }} + {{ range $path, $containers := $paths }} + {{ $sum := sha1 $path }} + {{ $upstream := printf "%s-%s" $host $sum }} + {{ template "location" (dict "Path" $path "Proto" $proto "Upstream" $upstream "Host" $host "VHostRoot" $vhost_root) }} {{ 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_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 }} } {{ if (and (not $is_https) (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }}