Merge branch 'nginx-proxy:main' into master

This commit is contained in:
Hannes Happle 2022-11-29 12:15:12 +01:00 committed by GitHub
commit dbf596efdf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 964 additions and 223 deletions

View file

@ -1,6 +1,9 @@
.git .git
.github
test
.dockerignore .dockerignore
circle.yml .gitignore
*.yml
Dockerfile*
Makefile Makefile
README.md README.md
test

View file

@ -1,16 +1,35 @@
# !!!PLEASE READ!!! # ⚠️ PLEASE READ ⚠️
## Questions ## Questions or Features
If you have a question, DO NOT SUBMIT a new issue. If you have a question or want to request a feature, please **DO NOT SUBMIT** a new issue.
Please ask the question on the Discussions section: https://github.com/nginx-proxy/nginx-proxy/discussions Instead please use the relevant Discussions section's category:
- 🙏 [Ask a question](https://github.com/nginx-proxy/nginx-proxy/discussions/categories/q-a)
- 💡 [Request a feature](https://github.com/nginx-proxy/nginx-proxy/discussions/categories/ideas)
## Bugs or Features ## Bugs
If you are logging a bug or feature request, please search the current open issues to see if there is already a bug or feature opened. If you are logging a bug, please search the current open issues first to see if there is already a bug opened.
For bugs, the easier you make it to reproduce the issue you see, the easier and faster it can get fixed. If you can provide a script or docker-compose file that reproduces the problems, that is very helpful. For bugs, the easier you make it to reproduce the issue you see and the more initial information you provide, the easier and faster the bug can be identified and can get fixed.
Please at least provide:
- the exact nginx-proxy version you're using (if using `latest` please make sure it is up to date and provide the version number printed at container startup).
- complete configuration (compose file, command line, etc) of both your nginx-proxy container(s) and proxied containers. You should redact sensitive info if needed but please provide **full** configurations.
- generated nginx configuration obtained with `docker exec nameofyournginxproxycontainer nginx -T`
If you can provide a script or docker-compose file that reproduces the problems, that is very helpful.
## General advice about `latest`
Do not use the `latest` tag for production setups.
`latest` is nothing more than a convenient default used by Docker if no specific tag is provided, there isn't any strict convention on what goes into this tag over different projects, and it does not carry any promise of stability.
Using `latest` will most certainly put you at risk of experiencing uncontrolled updates to non backward compatible versions (or versions with breaking changes) and makes it harder for maintainers to track which exact version of the container you are experiencing an issue with.
This recommendation stands for pretty much every Docker image in existence, not just nginx-proxy's ones.
Thanks, Thanks,
Jason Nicolas

View file

@ -12,7 +12,6 @@ on:
paths-ignore: paths-ignore:
- 'test/*' - 'test/*'
- '.gitignore' - '.gitignore'
- '.travis.yml'
- 'docker-compose-separate-containers.yml' - 'docker-compose-separate-containers.yml'
- 'docker-compose.yml' - 'docker-compose.yml'
- 'LICENSE' - 'LICENSE'
@ -29,17 +28,24 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Retrieve version
run: echo "GIT_DESCRIBE=$(git describe --tags)" >> $GITHUB_ENV
- name: Get Docker tags for Debian based image - name: Get Docker tags for Debian based image
id: docker_meta_debian id: docker_meta_debian
uses: crazy-max/ghaction-docker-meta@v2 uses: docker/metadata-action@v3
with: with:
images: | images: |
ghcr.io/nginx-proxy/nginx-proxy
nginxproxy/nginx-proxy nginxproxy/nginx-proxy
jwilder/nginx-proxy jwilder/nginx-proxy
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' }}
labels: |
org.opencontainers.image.authors=Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag), Jason Wilder
org.opencontainers.image.version=${{ env.GIT_DESCRIBE }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
@ -53,12 +59,20 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push the Debian based image - name: Build and push the Debian based image
id: docker_build_debian id: docker_build_debian
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: . context: .
file: Dockerfile file: Dockerfile
build-args: NGINX_PROXY_VERSION=${{ env.GIT_DESCRIBE }}
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true push: true
tags: ${{ steps.docker_meta_debian.outputs.tags }} tags: ${{ steps.docker_meta_debian.outputs.tags }}
@ -76,17 +90,24 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Retrieve version
run: echo "GIT_DESCRIBE=$(git describe --tags)" >> $GITHUB_ENV
- name: Get Docker tags for Alpine based image - name: Get Docker tags for Alpine based image
id: docker_meta_alpine id: docker_meta_alpine
uses: crazy-max/ghaction-docker-meta@v2 uses: docker/metadata-action@v3
with: with:
images: | images: |
ghcr.io/nginx-proxy/nginx-proxy
nginxproxy/nginx-proxy nginxproxy/nginx-proxy
jwilder/nginx-proxy jwilder/nginx-proxy
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' }}
labels: |
org.opencontainers.image.authors=Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag), Jason Wilder
org.opencontainers.image.version=${{ env.GIT_DESCRIBE }}
flavor: latest=false flavor: latest=false
- name: Set up QEMU - name: Set up QEMU
@ -101,12 +122,20 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push the Alpine based image - name: Build and push the Alpine based image
id: docker_build_alpine id: docker_build_alpine
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: . context: .
file: Dockerfile.alpine file: Dockerfile.alpine
build-args: NGINX_PROXY_VERSION=${{ env.GIT_DESCRIBE }}
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true push: true
tags: ${{ steps.docker_meta_alpine.outputs.tags }} tags: ${{ steps.docker_meta_alpine.outputs.tags }}

View file

@ -1,16 +1,16 @@
# 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=0.9.0
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
FROM golang:1.16.7 as gobuilder FROM golang:1.18.1 as gobuilder
# Build docker-gen from scratch # Build docker-gen from scratch
FROM gobuilder as dockergen FROM gobuilder as dockergen
ARG DOCKER_GEN_VERSION ARG DOCKER_GEN_VERSION
RUN git clone https://github.com/jwilder/docker-gen \ RUN git clone https://github.com/nginx-proxy/docker-gen \
&& cd /go/docker-gen \ && cd /go/docker-gen \
&& git -c advice.detachedHead=false checkout $DOCKER_GEN_VERSION \ && git -c advice.detachedHead=false checkout $DOCKER_GEN_VERSION \
&& go mod download \ && go mod download \
@ -36,8 +36,15 @@ RUN git clone https://github.com/nginx-proxy/forego/ \
&& rm -rf /go/forego && rm -rf /go/forego
# Build the final image # Build the final image
FROM nginx:1.21.4 FROM nginx:1.21.6
LABEL maintainer="Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag)"
ARG NGINX_PROXY_VERSION
# Add DOCKER_GEN_VERSION environment variable
# Because some external projects rely on it
ARG DOCKER_GEN_VERSION
ENV NGINX_PROXY_VERSION=${NGINX_PROXY_VERSION} \
DOCKER_GEN_VERSION=${DOCKER_GEN_VERSION} \
DOCKER_HOST=unix:///tmp/docker.sock
# Install wget and install/updates certificates # Install wget and install/updates certificates
RUN apt-get update \ RUN apt-get update \
@ -48,7 +55,7 @@ RUN apt-get update \
&& rm -r /var/lib/apt/lists/* && rm -r /var/lib/apt/lists/*
# Configure Nginx and apply fix for very long server names # Configure Nginx
RUN echo "daemon off;" >> /etc/nginx/nginx.conf \ RUN echo "daemon off;" >> /etc/nginx/nginx.conf \
&& sed -i 's/worker_processes 1/worker_processes auto/' /etc/nginx/nginx.conf \ && sed -i 's/worker_processes 1/worker_processes auto/' /etc/nginx/nginx.conf \
&& sed -i 's/worker_connections 1024/worker_connections 10240/' /etc/nginx/nginx.conf \ && sed -i 's/worker_connections 1024/worker_connections 10240/' /etc/nginx/nginx.conf \
@ -58,17 +65,10 @@ RUN echo "daemon off;" >> /etc/nginx/nginx.conf \
COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego
COPY --from=dockergen /usr/local/bin/docker-gen /usr/local/bin/docker-gen COPY --from=dockergen /usr/local/bin/docker-gen /usr/local/bin/docker-gen
# Add DOCKER_GEN_VERSION environment variable
# Because some external projects rely on it
ARG DOCKER_GEN_VERSION
ENV DOCKER_GEN_VERSION=${DOCKER_GEN_VERSION}
COPY network_internal.conf /etc/nginx/ COPY network_internal.conf /etc/nginx/
COPY . /app/ COPY app nginx.tmpl LICENSE /app/
WORKDIR /app/ WORKDIR /app/
ENV DOCKER_HOST unix:///tmp/docker.sock
ENTRYPOINT ["/app/docker-entrypoint.sh"] ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["forego", "start", "-r"] CMD ["forego", "start", "-r"]

View file

@ -1,9 +1,9 @@
# 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=0.9.0
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
FROM golang:1.16.7-alpine as gobuilder FROM golang:1.18.1-alpine as gobuilder
RUN apk add --no-cache git musl-dev RUN apk add --no-cache git musl-dev
# Build docker-gen from scratch # Build docker-gen from scratch
@ -11,7 +11,7 @@ FROM gobuilder as dockergen
ARG DOCKER_GEN_VERSION ARG DOCKER_GEN_VERSION
RUN git clone https://github.com/jwilder/docker-gen \ RUN git clone https://github.com/nginx-proxy/docker-gen \
&& cd /go/docker-gen \ && cd /go/docker-gen \
&& git -c advice.detachedHead=false checkout $DOCKER_GEN_VERSION \ && git -c advice.detachedHead=false checkout $DOCKER_GEN_VERSION \
&& go mod download \ && go mod download \
@ -37,15 +37,22 @@ RUN git clone https://github.com/nginx-proxy/forego/ \
&& rm -rf /go/forego && rm -rf /go/forego
# Build the final image # Build the final image
FROM nginx:1.21.4-alpine FROM nginx:1.21.6-alpine
LABEL maintainer="Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag)"
ARG NGINX_PROXY_VERSION
# Add DOCKER_GEN_VERSION environment variable
# Because some external projects rely on it
ARG DOCKER_GEN_VERSION
ENV NGINX_PROXY_VERSION=${NGINX_PROXY_VERSION} \
DOCKER_GEN_VERSION=${DOCKER_GEN_VERSION} \
DOCKER_HOST=unix:///tmp/docker.sock
# Install wget and install/updates certificates # Install wget and install/updates certificates
RUN apk add --no-cache --virtual .run-deps \ RUN apk add --no-cache --virtual .run-deps \
ca-certificates bash wget openssl \ ca-certificates bash wget openssl \
&& update-ca-certificates && update-ca-certificates
# Configure Nginx and apply fix for very long server names # Configure Nginx
RUN echo "daemon off;" >> /etc/nginx/nginx.conf \ RUN echo "daemon off;" >> /etc/nginx/nginx.conf \
&& sed -i 's/worker_processes 1/worker_processes auto/' /etc/nginx/nginx.conf \ && sed -i 's/worker_processes 1/worker_processes auto/' /etc/nginx/nginx.conf \
&& sed -i 's/worker_connections 1024/worker_connections 10240/' /etc/nginx/nginx.conf \ && sed -i 's/worker_connections 1024/worker_connections 10240/' /etc/nginx/nginx.conf \
@ -55,17 +62,10 @@ RUN echo "daemon off;" >> /etc/nginx/nginx.conf \
COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego
COPY --from=dockergen /usr/local/bin/docker-gen /usr/local/bin/docker-gen COPY --from=dockergen /usr/local/bin/docker-gen /usr/local/bin/docker-gen
# Add DOCKER_GEN_VERSION environment variable
# Because some external projects rely on it
ARG DOCKER_GEN_VERSION
ENV DOCKER_GEN_VERSION=${DOCKER_GEN_VERSION}
COPY network_internal.conf /etc/nginx/ COPY network_internal.conf /etc/nginx/
COPY . /app/ COPY app nginx.tmpl LICENSE /app/
WORKDIR /app/ WORKDIR /app/
ENV DOCKER_HOST unix:///tmp/docker.sock
ENTRYPOINT ["/app/docker-entrypoint.sh"] ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["forego", "start", "-r"] CMD ["forego", "start", "-r"]

View file

@ -1,6 +1,7 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2014 Jason Wilder Copyright (c) 2014-2020 Jason Wilder
Copyright (c) 2021-2022 Nicolas Duchon
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -6,10 +6,10 @@ build-webserver:
docker build -t web test/requirements/web docker build -t web test/requirements/web
build-nginx-proxy-test-debian: build-nginx-proxy-test-debian:
docker build -t nginxproxy/nginx-proxy:test . docker build --build-arg NGINX_PROXY_VERSION="test" -t nginxproxy/nginx-proxy:test .
build-nginx-proxy-test-alpine: build-nginx-proxy-test-alpine:
docker build -f Dockerfile.alpine -t nginxproxy/nginx-proxy:test . docker build --build-arg NGINX_PROXY_VERSION="test" -f Dockerfile.alpine -t nginxproxy/nginx-proxy:test .
test-debian: build-webserver build-nginx-proxy-test-debian test-debian: build-webserver build-nginx-proxy-test-debian
test/pytest.sh test/pytest.sh

View file

@ -1,6 +1,6 @@
[![Test](https://github.com/nginx-proxy/nginx-proxy/actions/workflows/test.yml/badge.svg)](https://github.com/nginx-proxy/nginx-proxy/actions/workflows/test.yml) [![Test](https://github.com/nginx-proxy/nginx-proxy/actions/workflows/test.yml/badge.svg)](https://github.com/nginx-proxy/nginx-proxy/actions/workflows/test.yml)
[![GitHub release](https://img.shields.io/github/v/release/nginx-proxy/nginx-proxy)](https://github.com/nginx-proxy/nginx-proxy/releases) [![GitHub release](https://img.shields.io/github/v/release/nginx-proxy/nginx-proxy)](https://github.com/nginx-proxy/nginx-proxy/releases)
![nginx 1.21.4](https://img.shields.io/badge/nginx-1.21.4-brightgreen.svg) ![nginx 1.21.6](https://img.shields.io/badge/nginx-1.21.6-brightgreen.svg)
[![Docker Image Size](https://img.shields.io/docker/image-size/nginxproxy/nginx-proxy?sort=semver)](https://hub.docker.com/r/nginxproxy/nginx-proxy "Click to view the image on Docker Hub") [![Docker Image Size](https://img.shields.io/docker/image-size/nginxproxy/nginx-proxy?sort=semver)](https://hub.docker.com/r/nginxproxy/nginx-proxy "Click to view the image on Docker Hub")
[![Docker stars](https://img.shields.io/docker/stars/nginxproxy/nginx-proxy.svg)](https://hub.docker.com/r/nginxproxy/nginx-proxy 'DockerHub') [![Docker stars](https://img.shields.io/docker/stars/nginxproxy/nginx-proxy.svg)](https://hub.docker.com/r/nginxproxy/nginx-proxy 'DockerHub')
[![Docker pulls](https://img.shields.io/docker/pulls/nginxproxy/nginx-proxy.svg)](https://hub.docker.com/r/nginxproxy/nginx-proxy 'DockerHub') [![Docker pulls](https://img.shields.io/docker/pulls/nginxproxy/nginx-proxy.svg)](https://hub.docker.com/r/nginxproxy/nginx-proxy 'DockerHub')
@ -115,7 +115,51 @@ For each host defined into `VIRTUAL_HOST`, the associated virtual port is retrie
### Wildcard Hosts ### 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). 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`
### Virtual Host Aliases ### Virtual Host Aliases
@ -218,7 +262,7 @@ docker run -d -e VIRTUAL_HOST=foo.bar.com nginx
### Separate Containers ### Separate Containers
nginx-proxy can also be run as two separate containers using the [jwilder/docker-gen](https://hub.docker.com/r/jwilder/docker-gen) image and the official [nginx](https://registry.hub.docker.com/_/nginx/) image. nginx-proxy can also be run as two separate containers using the [nginxproxy/docker-gen](https://hub.docker.com/r/nginxproxy/docker-gen) image and the official [nginx](https://registry.hub.docker.com/_/nginx/) image.
You may want to do this to prevent having the docker socket bound to a publicly exposed container service. You may want to do this to prevent having the docker socket bound to a publicly exposed container service.
@ -249,7 +293,7 @@ Then start the docker-gen container with the shared volume and template:
docker run --volumes-from nginx \ docker run --volumes-from nginx \
-v /var/run/docker.sock:/tmp/docker.sock:ro \ -v /var/run/docker.sock:/tmp/docker.sock:ro \
-v $(pwd):/etc/docker-gen/templates \ -v $(pwd):/etc/docker-gen/templates \
-t jwilder/docker-gen -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf -t nginxproxy/docker-gen -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
``` ```
Finally, start your containers with `VIRTUAL_HOST` environment variables. Finally, start your containers with `VIRTUAL_HOST` environment variables.
@ -284,7 +328,7 @@ To use custom `dhparam.pem` files per-virtual-host, the files should be named af
> COMPATIBILITY WARNING: The default generated `dhparam.pem` key is 4096 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 provide your own `dhparam.pem`. > COMPATIBILITY WARNING: The default generated `dhparam.pem` key is 4096 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 provide your own `dhparam.pem`.
In the separate container setup, no pre-generated key will be available and neither the [jwilder/docker-gen](https://hub.docker.com/r/jwilder/docker-gen) image, nor the offical [nginx](https://registry.hub.docker.com/_/nginx/) image will provide one. If you still want A+ security in a separate container setup, you should mount an RFC7919 DH key file to the nginx container at `/etc/nginx/dhparam/dhparam.pem`. In the separate container setup, no pre-generated key will be available and neither the [nginxproxy/docker-gen](https://hub.docker.com/r/nginxproxy/docker-gen) image, nor the offical [nginx](https://registry.hub.docker.com/_/nginx/) image will provide one. If you still want A+ security in a separate container setup, you should mount an RFC7919 DH key file to the nginx container at `/etc/nginx/dhparam/dhparam.pem`.
Set `DHPARAM_SKIP` environment variable to `true` to disable using default Diffie-Hellman parameters. The default value is `false`. Set `DHPARAM_SKIP` environment variable to `true` to disable using default Diffie-Hellman parameters. The default value is `false`.
@ -362,6 +406,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 "";
@ -448,10 +493,10 @@ Please note that using regular expressions in `VIRTUAL_HOST` will always result
### Troubleshooting ### 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`: 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.conf`:
```console ```console
docker exec <nginx-proxy-instance> cat /etc/nginx/conf.d/default docker exec <nginx-proxy-instance> cat /etc/nginx/conf.d/default.conf
``` ```
Especially at `upstream` definition blocks which should look like: Especially at `upstream` definition blocks which should look like:

View file

@ -29,6 +29,12 @@ function _parse_false() {
esac esac
} }
function _print_version {
if [[ -n "${NGINX_PROXY_VERSION:-}" ]]; then
echo "Info: running nginx-proxy version ${NGINX_PROXY_VERSION}"
fi
}
function _check_unix_socket() { function _check_unix_socket() {
# Warn if the DOCKER_HOST socket does not exist # Warn if the DOCKER_HOST socket does not exist
if [[ ${DOCKER_HOST} == unix://* ]]; then if [[ ${DOCKER_HOST} == unix://* ]]; then
@ -96,6 +102,8 @@ function _setup_dhparam() {
# Run the init logic if the default CMD was provided # Run the init logic if the default CMD was provided
if [[ $* == 'forego start -r' ]]; then if [[ $* == 'forego start -r' ]]; then
_print_version
_check_unix_socket _check_unix_socket
_resolvers _resolvers

View file

@ -9,8 +9,9 @@ services:
- /etc/nginx/conf.d - /etc/nginx/conf.d
dockergen: dockergen:
image: jwilder/docker-gen image: nginxproxy/docker-gen
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl
/etc/nginx/conf.d/default.conf
volumes_from: volumes_from:
- nginx - nginx
volumes: volumes:

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

@ -1,9 +1,11 @@
{{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }} {{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }}
{{ $nginx_proxy_version := coalesce $.Env.NGINX_PROXY_VERSION "" }}
{{ $external_http_port := coalesce $.Env.HTTP_PORT "80" }} {{ $external_http_port := coalesce $.Env.HTTP_PORT "80" }}
{{ $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" }}
@ -48,6 +50,103 @@
{{ 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 "" }}
# nginx-proxy version : {{ $nginx_proxy_version }}
{{ end }}
# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the # If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the
# scheme used to connect to this server # scheme used to connect to this server
map $http_x_forwarded_proto $proxy_x_forwarded_proto { map $http_x_forwarded_proto $proxy_x_forwarded_proto {
@ -95,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 }};
@ -114,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 "";
@ -157,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") }}
@ -298,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;
@ -332,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) }}
{{ end }} {{ else }}
{{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }} {{ range $path, $container := $paths }}
include {{ printf "/etc/nginx/vhost.d/%s_location" $host}}; {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, falling back to "http" */}}
{{ else if (exists "/etc/nginx/vhost.d/default_location") }} {{ $proto := trim (or (first (groupByKeys $container "Env.VIRTUAL_PROTO")) "http") }}
include /etc/nginx/vhost.d/default_location;
{{/* 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 (not (contains $paths "/")) }}
location / {
return {{ $default_root_response }};
} }
{{ end }}
{{ end }}
} }
{{ end }} {{ end }}
@ -369,44 +420,41 @@ server {
{{ end }} {{ end }}
listen {{ $external_http_port }} {{ $default_server }}; listen {{ $external_http_port }} {{ $default_server }};
{{ if $enable_ipv6 }} {{ if $enable_ipv6 }}
listen [::]:80 {{ $default_server }}; listen [::]:{{ $external_http_port }} {{ $default_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 if eq $proto "grpc" }}
grpc_pass {{ trim $proto }}://{{ trim $upstream_name }};
{{ else }} {{ else }}
proxy_pass {{ trim $proto }}://{{ trim $upstream_name }}; {{ range $path, $container := $paths }}
{{ end }} {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, falling back to "http" */}}
{{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} {{ $proto := trim (or (first (groupByKeys $container "Env.VIRTUAL_PROTO")) "http") }}
auth_basic "Restricted {{ $host }}";
auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; {{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}}
{{ end }} {{ $network_tag := or (first (groupByKeys $container "Env.NETWORK_ACCESS")) "external" }}
{{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }} {{ $sum := sha1 $path }}
include {{ printf "/etc/nginx/vhost.d/%s_location" $host}}; {{ $upstream := printf "%s-%s" $upstream_name $sum }}
{{ else if (exists "/etc/nginx/vhost.d/default_location") }} {{ $dest := (or (first (groupByKeys $container "Env.VIRTUAL_DEST")) "") }}
include /etc/nginx/vhost.d/default_location; {{ template "location" (dict "Path" $path "Proto" $proto "Upstream" $upstream "Host" $host "VhostRoot" $vhost_root "Dest" $dest "NetworkTag" $network_tag) }}
{{ end }} {{ end }}
{{ if (not (contains $paths "/")) }}
location / {
return {{ $default_root_response }};
} }
{{ end }}
{{ 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

@ -23,12 +23,15 @@ logging.getLogger('DNS').setLevel(logging.DEBUG)
logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARN) logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARN)
CA_ROOT_CERTIFICATE = os.path.join(os.path.dirname(__file__), 'certs/ca-root.crt') CA_ROOT_CERTIFICATE = os.path.join(os.path.dirname(__file__), 'certs/ca-root.crt')
I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER = os.path.isfile("/.dockerenv") PYTEST_RUNNING_IN_CONTAINER = os.environ.get('PYTEST_RUNNING_IN_CONTAINER') == "1"
FORCE_CONTAINER_IPV6 = False # ugly global state to consider containers' IPv6 address instead of IPv4 FORCE_CONTAINER_IPV6 = False # ugly global state to consider containers' IPv6 address instead of IPv4
docker_client = docker.from_env() docker_client = docker.from_env()
# Name of pytest container to reference if it's being used for running tests
test_container = 'nginx-proxy-pytest'
############################################################################### ###############################################################################
# #
@ -189,6 +192,10 @@ def nginx_proxy_dns_resolver(domain_name):
nginxproxy_containers = docker_client.containers.list(filters={"status": "running", "ancestor": "nginxproxy/nginx-proxy:test"}) nginxproxy_containers = docker_client.containers.list(filters={"status": "running", "ancestor": "nginxproxy/nginx-proxy:test"})
if len(nginxproxy_containers) == 0: if len(nginxproxy_containers) == 0:
log.warn(f"no container found from image nginxproxy/nginx-proxy:test while resolving {domain_name!r}") log.warn(f"no container found from image nginxproxy/nginx-proxy:test while resolving {domain_name!r}")
exited_nginxproxy_containers = docker_client.containers.list(filters={"status": "exited", "ancestor": "nginxproxy/nginx-proxy:test"})
if len(exited_nginxproxy_containers) > 0:
exited_nginxproxy_container_logs = exited_nginxproxy_containers[0].logs()
log.warn(f"nginxproxy/nginx-proxy:test container might have exited unexpectedly. Container logs: " + "\n" + exited_nginxproxy_container_logs.decode())
return return
nginxproxy_container = nginxproxy_containers[0] nginxproxy_container = nginxproxy_containers[0]
ip = container_ip(nginxproxy_container) ip = container_ip(nginxproxy_container)
@ -236,6 +243,11 @@ def monkey_patch_urllib_dns_resolver():
logging.getLogger('DNS').debug(f"resolving domain name {repr(args)}") logging.getLogger('DNS').debug(f"resolving domain name {repr(args)}")
_args = list(args) _args = list(args)
# Fail early when querying IP directly and it is forced ipv6 when not supported,
# Otherwise a pytest container not using the host network fails to pass `test_raw-ip-vhost`.
if FORCE_CONTAINER_IPV6 and not HAS_IPV6:
pytest.skip("This system does not support IPv6")
# custom DNS resolvers # custom DNS resolvers
ip = nginx_proxy_dns_resolver(args[0]) ip = nginx_proxy_dns_resolver(args[0])
if ip is None: if ip is None:
@ -259,7 +271,7 @@ def restore_urllib_dns_resolver(getaddrinfo_func):
def remove_all_containers(): def remove_all_containers():
for container in docker_client.containers.list(all=True): for container in docker_client.containers.list(all=True):
if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER and container.id.startswith(socket.gethostname()): if PYTEST_RUNNING_IN_CONTAINER and container.name == test_container:
continue # pytest is running within a Docker container, so we do not want to remove that particular container continue # pytest is running within a Docker container, so we do not want to remove that particular container
logging.info(f"removing container {container.name}") logging.info(f"removing container {container.name}")
container.remove(v=True, force=True) container.remove(v=True, force=True)
@ -349,18 +361,23 @@ def connect_to_network(network):
:return: the name of the network we were connected to, or None :return: the name of the network we were connected to, or None
""" """
if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER: if PYTEST_RUNNING_IN_CONTAINER:
try: try:
my_container = docker_client.containers.get(socket.gethostname()) my_container = docker_client.containers.get(test_container)
except docker.errors.NotFound: except docker.errors.NotFound:
logging.warn(f"container {socket.gethostname()!r} not found") logging.warn(f"container {test_container} not found")
return return
# figure out our container networks # figure out our container networks
my_networks = list(my_container.attrs["NetworkSettings"]["Networks"].keys()) my_networks = list(my_container.attrs["NetworkSettings"]["Networks"].keys())
# make sure our container is connected to the nginx-proxy's network # If the pytest container is using host networking, it cannot connect to container networks (not required with host network)
if network not in my_networks: if 'host' in my_networks:
return None
# Make sure our container is connected to the nginx-proxy's network,
# but avoid connecting to `none` network (not valid) with `test_server-down` tests
if network.name not in my_networks and network.name != 'none':
logging.info(f"Connecting to docker network: {network.name}") logging.info(f"Connecting to docker network: {network.name}")
network.connect(my_container) network.connect(my_container)
return network return network
@ -372,11 +389,11 @@ def disconnect_from_network(network=None):
:param network: name of a docker network to disconnect from :param network: name of a docker network to disconnect from
""" """
if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER and network is not None: if PYTEST_RUNNING_IN_CONTAINER and network is not None:
try: try:
my_container = docker_client.containers.get(socket.gethostname()) my_container = docker_client.containers.get(test_container)
except docker.errors.NotFound: except docker.errors.NotFound:
logging.warn(f"container {socket.gethostname()!r} not found") logging.warn(f"container {test_container} not found")
return return
# figure out our container networks # figure out our container networks
@ -394,11 +411,11 @@ def connect_to_all_networks():
:return: a list of networks we connected to :return: a list of networks we connected to
""" """
if not I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER: if not PYTEST_RUNNING_IN_CONTAINER:
return [] return []
else: else:
# find the list of docker networks # find the list of docker networks
networks = [network for network in docker_client.networks.list() if len(network.containers) > 0 and network.name != 'bridge'] networks = [network for network in docker_client.networks.list(greedy=True) if len(network.containers) > 0 and network.name != 'bridge']
return [connect_to_network(network) for network in networks] return [connect_to_network(network) for network in networks]

View file

@ -18,8 +18,8 @@ docker build -t nginx-proxy-tester -f "${DIR}/requirements/Dockerfile-nginx-prox
# run the nginx-proxy-tester container setting the correct value for the working dir in order for # run the nginx-proxy-tester container setting the correct value for the working dir in order for
# docker-compose to work properly when run from within that container. # docker-compose to work properly when run from within that container.
exec docker run --rm -it \ exec docker run --rm -it --name "nginx-proxy-pytest" \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume "/var/run/docker.sock:/var/run/docker.sock" \
--volume "${DIR}:${DIR}" \ --volume "${DIR}:${DIR}" \
--workdir "${DIR}" \ --workdir "${DIR}" \
nginx-proxy-tester "${ARGS[@]}" nginx-proxy-tester "${ARGS[@]}"

View file

@ -1,5 +1,7 @@
FROM python:3.9 FROM python:3.9
ENV PYTEST_RUNNING_IN_CONTAINER=1
COPY python-requirements.txt /requirements.txt COPY python-requirements.txt /requirements.txt
RUN pip install -r /requirements.txt RUN pip install -r /requirements.txt

View file

@ -1,5 +1,5 @@
backoff==1.11.1 backoff==1.11.1
docker-compose==1.29.2 docker-compose==1.29.2
docker==5.0.3 docker==5.0.3
pytest==6.2.5 pytest==7.1.2
requests==2.26.0 requests==2.27.1

View file

@ -8,7 +8,7 @@ services:
- /etc/nginx/conf.d - /etc/nginx/conf.d
dockergen: dockergen:
image: jwilder/docker-gen image: nginxproxy/docker-gen
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
volumes_from: volumes_from:
- nginx - nginx

View file

@ -7,7 +7,7 @@ services:
- nginx_conf:/etc/nginx/conf.d - nginx_conf:/etc/nginx/conf.d
dockergen: dockergen:
image: jwilder/docker-gen image: nginxproxy/docker-gen
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
volumes: volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro - /var/run/docker.sock:/tmp/docker.sock:ro

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

@ -22,3 +22,8 @@ def test_forwards_to_web2(docker_compose, nginxproxy):
def test_ipv6_is_disabled_by_default(docker_compose, nginxproxy): def test_ipv6_is_disabled_by_default(docker_compose, nginxproxy):
with pytest.raises(ConnectionError): with pytest.raises(ConnectionError):
nginxproxy.get("http://nginx-proxy/port", ipv6=True) nginxproxy.get("http://nginx-proxy/port", ipv6=True)
def test_container_version_is_displayed(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
assert "# nginx-proxy version : test" in conf

View file

@ -0,0 +1,8 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
-----END DH PARAMETERS-----

View file

@ -1,5 +1,6 @@
import re import re
import subprocess import subprocess
import os
import backoff import backoff
import docker import docker
@ -80,12 +81,17 @@ def negotiate_cipher(sut_container, additional_params='', grep='Cipher is'):
raise Exception("Failed to process CLI request:\n" + e.stderr) from None raise Exception("Failed to process CLI request:\n" + e.stderr) from None
def can_negotiate_dhe_ciphersuite(sut_container): # The default `dh_bits` can vary due to configuration.
r = negotiate_cipher(sut_container, "-cipher 'EDH'") # `additional_params` allows for adjusting the request to a specific `VIRTUAL_HOST`,
# where DH size can differ from the configured global default DH size.
def can_negotiate_dhe_ciphersuite(sut_container, dh_bits=4096, additional_params=''):
openssl_params = f"-cipher 'EDH' {additional_params}"
r = negotiate_cipher(sut_container, openssl_params)
assert "New, TLSv1.2, Cipher is DHE-RSA-AES256-GCM-SHA384\n" == r assert "New, TLSv1.2, Cipher is DHE-RSA-AES256-GCM-SHA384\n" == r
r2 = negotiate_cipher(sut_container, "-cipher 'EDH'", "Server Temp Key") r2 = negotiate_cipher(sut_container, openssl_params, "Server Temp Key")
assert "DH" in r2 assert f"Server Temp Key: DH, {dh_bits} bits" in r2
def cannot_negotiate_dhe_ciphersuite(sut_container): def cannot_negotiate_dhe_ciphersuite(sut_container):
@ -101,6 +107,29 @@ def cannot_negotiate_dhe_ciphersuite(sut_container):
assert "X25519" in r3 assert "X25519" in r3
# To verify self-signed certificates, the file path to their CA cert must be provided.
# Use the `fqdn` arg to specify the `VIRTUAL_HOST` to request for verification for that cert.
#
# Resolves the following stderr warnings regarding self-signed cert verification and missing SNI:
# `Can't use SSL_get_servername`
# `verify error:num=20:unable to get local issuer certificate`
# `verify error:num=21:unable to verify the first certificate`
#
# The stderr output is hidden due to running the openssl command with `stderr=subprocess.PIPE`.
def can_verify_chain_of_trust(sut_container, ca_cert, fqdn):
openssl_params = f"-CAfile '{ca_cert}' -servername '{fqdn}'"
r = negotiate_cipher(sut_container, openssl_params, "Verify return code")
assert "Verify return code: 0 (ok)" in r
def should_be_equivalent_content(sut_container, expected, actual):
expected_checksum = sut_container.exec_run(f"md5sum {expected}").output.split()[0]
actual_checksum = sut_container.exec_run(f"md5sum {actual}").output.split()[0]
assert expected_checksum == actual_checksum
# Parse array of container ENV, splitting at the `=` and returning the value, otherwise `None` # Parse array of container ENV, splitting at the `=` and returning the value, otherwise `None`
def get_env(sut_container, var): def get_env(sut_container, var):
env = sut_container.attrs['Config']['Env'] env = sut_container.attrs['Config']['Env']
@ -125,14 +154,17 @@ def test_default_dhparam_is_ffdhe4096(docker_compose):
assert_log_contains("Setting up DH Parameters..", container_name) assert_log_contains("Setting up DH Parameters..", container_name)
# Make sure the dhparam file used is the default ffdhe4096.pem: # `dhparam.pem` contents should match the default (ffdhe4096.pem):
default_checksum = sut_container.exec_run("md5sum /app/dhparam/ffdhe4096.pem").output.split() should_be_equivalent_content(
current_checksum = sut_container.exec_run("md5sum /etc/nginx/dhparam/dhparam.pem").output.split() sut_container,
assert default_checksum[0] == current_checksum[0] "/app/dhparam/ffdhe4096.pem",
"/etc/nginx/dhparam/dhparam.pem"
)
can_negotiate_dhe_ciphersuite(sut_container) can_negotiate_dhe_ciphersuite(sut_container, 4096)
# Overrides default DH group via ENV `DHPARAM_BITS=3072`:
def test_can_change_dhparam_group(docker_compose): def test_can_change_dhparam_group(docker_compose):
container_name="dh-env" container_name="dh-env"
sut_container = docker_client.containers.get(container_name) sut_container = docker_client.containers.get(container_name)
@ -140,12 +172,14 @@ def test_can_change_dhparam_group(docker_compose):
assert_log_contains("Setting up DH Parameters..", container_name) assert_log_contains("Setting up DH Parameters..", container_name)
# Make sure the dhparam file used is ffdhe2048.pem, not the default (ffdhe4096.pem): # `dhparam.pem` contents should not match the default (ffdhe4096.pem):
default_checksum = sut_container.exec_run("md5sum /app/dhparam/ffdhe2048.pem").output.split() should_be_equivalent_content(
current_checksum = sut_container.exec_run("md5sum /etc/nginx/dhparam/dhparam.pem").output.split() sut_container,
assert default_checksum[0] == current_checksum[0] "/app/dhparam/ffdhe3072.pem",
"/etc/nginx/dhparam/dhparam.pem"
)
can_negotiate_dhe_ciphersuite(sut_container) can_negotiate_dhe_ciphersuite(sut_container, 3072)
def test_fail_if_dhparam_group_not_supported(docker_compose): def test_fail_if_dhparam_group_not_supported(docker_compose):
@ -162,6 +196,7 @@ def test_fail_if_dhparam_group_not_supported(docker_compose):
) )
# Overrides default DH group by providing a custom `/etc/nginx/dhparam/dhparam.pem`:
def test_custom_dhparam_is_supported(docker_compose): def test_custom_dhparam_is_supported(docker_compose):
container_name="dh-file" container_name="dh-file"
sut_container = docker_client.containers.get(container_name) sut_container = docker_client.containers.get(container_name)
@ -172,14 +207,49 @@ def test_custom_dhparam_is_supported(docker_compose):
container_name container_name
) )
# Make sure the dhparam file used is not the default (ffdhe4096.pem): # `dhparam.pem` contents should not match the default (ffdhe4096.pem):
default_checksum = sut_container.exec_run("md5sum /app/dhparam/ffdhe4096.pem").output.split() should_be_equivalent_content(
current_checksum = sut_container.exec_run("md5sum /etc/nginx/dhparam/dhparam.pem").output.split() sut_container,
assert default_checksum[0] != current_checksum[0] "/app/dhparam/ffdhe3072.pem",
"/etc/nginx/dhparam/dhparam.pem"
)
can_negotiate_dhe_ciphersuite(sut_container) can_negotiate_dhe_ciphersuite(sut_container, 3072)
# Only `web2` has a site-specific DH param file (which overrides all other DH config)
# Other tests here use `web5` explicitly, or implicitly (via ENV `DEFAULT_HOST`, otherwise first HTTPS server)
def test_custom_dhparam_is_supported_per_site(docker_compose):
container_name="dh-file"
sut_container = docker_client.containers.get(container_name)
assert sut_container.status == "running"
# A site specific `dhparam.pem` with DH group size of 2048-bit.
# DH group size should not match the:
# - 4096-bit default.
# - 3072-bit default, overriden by file.
should_be_equivalent_content(
sut_container,
"/app/dhparam/ffdhe2048.pem",
"/etc/nginx/certs/web2.nginx-proxy.tld.dhparam.pem"
)
# `-servername` required for nginx-proxy to respond with site-specific DH params used:
can_negotiate_dhe_ciphersuite(sut_container, 2048, '-servername web2.nginx-proxy.tld')
# --Unrelated to DH support--
# - `web5` is missing a certificate, but falls back to available `/etc/nginx/certs/nginx-proxy.tld.crt` via `nginx.tmpl` "closest" result.
# - `web2` has it's own cert provisioned at `/etc/nginx/certs/web2.nginx-proxy.tld.crt`.
can_verify_chain_of_trust(
sut_container,
ca_cert = f"{os.getcwd()}/certs/ca-root.crt",
fqdn = 'web2.nginx-proxy.tld'
)
# NOTE: These two tests will fail without the ENV `DEFAULT_HOST` to prevent
# accidentally falling back to `web2` as the default server, which has explicit DH params configured.
# Only copying DH params is skipped, not explicit usage via user providing custom files.
def test_can_skip_dhparam(docker_compose): def test_can_skip_dhparam(docker_compose):
container_name="dh-skip" container_name="dh-skip"
sut_container = docker_client.containers.get(container_name) sut_container = docker_client.containers.get(container_name)
@ -189,6 +259,7 @@ def test_can_skip_dhparam(docker_compose):
cannot_negotiate_dhe_ciphersuite(sut_container) cannot_negotiate_dhe_ciphersuite(sut_container)
def test_can_skip_dhparam_backward_compatibility(docker_compose): def test_can_skip_dhparam_backward_compatibility(docker_compose):
container_name="dh-skip-backward" container_name="dh-skip-backward"
sut_container = docker_client.containers.get(container_name) sut_container = docker_client.containers.get(container_name)

View file

@ -6,12 +6,27 @@ web5:
WEB_PORTS: "85" WEB_PORTS: "85"
VIRTUAL_HOST: "web5.nginx-proxy.tld" VIRTUAL_HOST: "web5.nginx-proxy.tld"
# Intended for testing with `dh-file` container.
# VIRTUAL_HOST is paired with site-specific DH param file.
# DEFAULT_HOST is required to avoid defaulting to web2,
# if not specifying FQDN (`-servername`) in openssl queries.
web2:
image: web
expose:
- "85"
environment:
WEB_PORTS: "85"
VIRTUAL_HOST: "web2.nginx-proxy.tld"
# sut - System Under Test # sut - System Under Test
# `docker.sock` required for functionality # `docker.sock` required for functionality
# `certs` required to enable HTTPS via template # `certs` required to enable HTTPS via template
with_default_group: with_default_group:
container_name: dh-default container_name: dh-default
image: &img-nginxproxy nginxproxy/nginx-proxy:test image: &img-nginxproxy nginxproxy/nginx-proxy:test
environment: &env-common
- &default-host DEFAULT_HOST=web5.nginx-proxy.tld
volumes: &vols-common volumes: &vols-common
- &docker-sock /var/run/docker.sock:/tmp/docker.sock:ro - &docker-sock /var/run/docker.sock:/tmp/docker.sock:ro
- &nginx-certs ./certs:/etc/nginx/certs:ro - &nginx-certs ./certs:/etc/nginx/certs:ro
@ -19,7 +34,8 @@ with_default_group:
with_alternative_group: with_alternative_group:
container_name: dh-env container_name: dh-env
environment: environment:
- DHPARAM_BITS=2048 - DHPARAM_BITS=3072
- *default-host
image: *img-nginxproxy image: *img-nginxproxy
volumes: *vols-common volumes: *vols-common
@ -27,21 +43,24 @@ with_invalid_group:
container_name: invalid-group-1024 container_name: invalid-group-1024
environment: environment:
- DHPARAM_BITS=1024 - DHPARAM_BITS=1024
- *default-host
image: *img-nginxproxy image: *img-nginxproxy
volumes: *vols-common volumes: *vols-common
with_custom_file: with_custom_file:
container_name: dh-file container_name: dh-file
image: *img-nginxproxy image: *img-nginxproxy
environment: *env-common
volumes: volumes:
- *docker-sock - *docker-sock
- *nginx-certs - *nginx-certs
- ../../dhparam/ffdhe3072.pem:/etc/nginx/dhparam/dhparam.pem:ro - ../../app/dhparam/ffdhe3072.pem:/etc/nginx/dhparam/dhparam.pem:ro
with_skip: with_skip:
container_name: dh-skip container_name: dh-skip
environment: environment:
- DHPARAM_SKIP=true - DHPARAM_SKIP=true
- *default-host
image: *img-nginxproxy image: *img-nginxproxy
volumes: *vols-common volumes: *vols-common
@ -49,5 +68,6 @@ with_skip_backward:
container_name: dh-skip-backward container_name: dh-skip-backward
environment: environment:
- DHPARAM_GENERATION=false - DHPARAM_GENERATION=false
- *default-host
image: *img-nginxproxy image: *img-nginxproxy
volumes: *vols-common volumes: *vols-common

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