From 3d368343713b3e7f5e744b27ba43e3631da67b84 Mon Sep 17 00:00:00 2001 From: Pepper Linden <3782201+rohvani@users.noreply.github.com> Date: Wed, 5 Nov 2025 23:51:02 +0000 Subject: [PATCH] Enable optional runtime DNS resolver for rotating upstream hosts --- Dockerfile | 2 + README.md | 17 ++++++++ src/etc/nginx/conf.d/default.conf.template | 6 ++- src/etc/nginx/nginx.conf.template | 7 ++++ test/test.sh | 45 ++++++++++++++++++++++ 5 files changed, 76 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0909fc1..0488afe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,8 @@ RUN apk add --no-cache \ COPY src / ENV KEEPALIVE_TIMEOUT=65 ENV PROXY_UWSGI=0 +ENV NGINX_RESOLVER= +ENV UPSTREAM_RESOLVE=0 ENV LISTEN_PORT=80 ENV STATUS_LISTEN_PORT=8091 ENV HEALTHCHECK_PATH="/lb-status/" diff --git a/README.md b/README.md index b4e8362..a2b0a88 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Pair nginx-proxy with your favorite upstream server (wsgi, uwsgi, asgi, et al.) | `STATUS_LISTEN_PORT` | nginx status port | No | 8091 | | | `UPSTREAM_SERVER` | Upstream server | Yes | | myapp:8080 fail_timeout=0, unix://mnt/server.sock | | `PROXY_REVERSE_URL` | Upstream server URL (Deprecated, please use UPSTREAM_SERVER) | No | | http://myapp:8080 | +| `NGINX_RESOLVER` | Value for nginx `resolver` directive | No | auto* | `169.254.169.253 valid=60s` | +| `UPSTREAM_RESOLVE` | Enable dynamic DNS resolution for the upstream (`resolve` parameter) | No | 0 | 1 | | `SERVER_NAME` | Allowed server names (hostnames) | Yes | | | | `SILENT` | Silence entrypoint output | No | | | | `STATIC_LOCATIONS` | Static asset mappings | No | | | @@ -30,6 +32,20 @@ Pair nginx-proxy with your favorite upstream server (wsgi, uwsgi, asgi, et al.) | `LOG_ONLY_5XX` | only log 5XX HTTP status access events | No | 0 | 1 | | `WORKER_CONNECTIONS` | Set the number of allowed worker connections | No | 1024 | 2048 | +* Defaults to 169.254.169.253 valid=60s when `UPSTREAM_RESOLVE=1` and no custom resolver is provided. + +### Handling DNS Changes + +If your upstream service rotates IP addresses (for example when fronted by a load +balancer), configure nginx to re-resolve the upstream host name by setting +`NGINX_RESOLVER` to the DNS servers you want nginx to use and enabling +`UPSTREAM_RESOLVE=1`. The value of `NGINX_RESOLVER` is passed directly to the +[`resolver` directive][nginx-resolver], so you can include modifiers such as +`valid=60s` or `ipv6=off`. When using a unix socket (`UPSTREAM_SERVER=unix://…`) +the resolve option is ignored. +If you leave `NGINX_RESOLVER` unset, nginx-proxy defaults to AWS's metadata +resolver (`169.254.169.253 valid=60s`) whenever `UPSTREAM_RESOLVE=1`. + ### Hosting Static Assets Static files can be hosted from your proxied application by sharing a volume @@ -93,3 +109,4 @@ Notable differences from the official [nginx container][] [gomplate]: https://docs.gomplate.ca/ [uwsgi]: https://uwsgi-docs.readthedocs.io/en/latest/ [nginx status]: https://nginx.org/en/docs/http/ngx_http_stub_status_module.html +[nginx-resolver]: https://nginx.org/en/docs/http/ngx_http_core_module.html#resolver diff --git a/src/etc/nginx/conf.d/default.conf.template b/src/etc/nginx/conf.d/default.conf.template index 1bc6888..91c4b99 100644 --- a/src/etc/nginx/conf.d/default.conf.template +++ b/src/etc/nginx/conf.d/default.conf.template @@ -9,7 +9,11 @@ server { {{ end }} upstream app { - server {{ .Env.UPSTREAM_SERVER }}; + {{- $upstream := .Env.UPSTREAM_SERVER -}} + {{- $isUnixSocket := strings.HasPrefix $upstream "unix://" -}} + {{- $resolve := and (eq .Env.UPSTREAM_RESOLVE "1") (not $isUnixSocket) -}} + # Enable nginx runtime DNS re-resolution when targeting hostnames; unix sockets do not support `resolve`. + server {{ $upstream }}{{ if $resolve }} resolve{{ end }}; } server { diff --git a/src/etc/nginx/nginx.conf.template b/src/etc/nginx/nginx.conf.template index a7889b6..b4c1f33 100644 --- a/src/etc/nginx/nginx.conf.template +++ b/src/etc/nginx/nginx.conf.template @@ -53,6 +53,13 @@ http { default 0; } + {{- $explicitResolver := ne .Env.NGINX_RESOLVER "" -}} + {{- $shouldResolve := eq .Env.UPSTREAM_RESOLVE "1" -}} + {{ if or $explicitResolver $shouldResolve }} + # Configure DNS servers for runtime upstream re-resolution. + resolver {{ if $explicitResolver }}{{ .Env.NGINX_RESOLVER }}{{ else }}169.254.169.253 valid=60s{{ end }}; + {{ end }} + {{ if (eq .Env.LOG_ONLY_5XX "1") }} access_log /dev/stdout json_analytics if=$is5xx; {{ else }} diff --git a/test/test.sh b/test/test.sh index 01b16bf..b739e54 100755 --- a/test/test.sh +++ b/test/test.sh @@ -7,6 +7,51 @@ function fail { exit 1 } +RESOLVER=$(awk '/^nameserver/ {print $2; exit}' /etc/resolv.conf) +if [ -n "$RESOLVER" ]; then + rendered_default=$(LISTEN_PORT=8080 \ + SERVER_NAME="example.com" \ + HEALTHCHECK_PATH="/health" \ + NO_ACCESS_LOGS=0 \ + PROXY_UWSGI=0 \ + STATIC_LOCATIONS= \ + UPSTREAM_SERVER="service.internal:8080" \ + UPSTREAM_RESOLVE=1 \ + NGINX_RESOLVER="$RESOLVER valid=60s" \ + gomplate < /etc/nginx/conf.d/default.conf.template) + + echo "$rendered_default" | grep -F "server service.internal:8080 resolve;" \ + || fail "expected resolve parameter when UPSTREAM_RESOLVE=1" + + rendered_nginx=$(LISTEN_PORT=8080 \ + SERVER_NAME="example.com" \ + HEALTHCHECK_PATH="/health" \ + NO_ACCESS_LOGS=0 \ + PROXY_UWSGI=0 \ + STATIC_LOCATIONS= \ + UPSTREAM_SERVER="service.internal:8080" \ + UPSTREAM_RESOLVE=1 \ + NGINX_RESOLVER="$RESOLVER valid=60s" \ + gomplate < /etc/nginx/nginx.conf.template) + + echo "$rendered_nginx" | grep -F "resolver $RESOLVER valid=60s;" \ + || fail "expected resolver directive in nginx.conf" + + fallback_nginx=$(LISTEN_PORT=8080 \ + SERVER_NAME="example.com" \ + HEALTHCHECK_PATH="/health" \ + NO_ACCESS_LOGS=0 \ + PROXY_UWSGI=0 \ + STATIC_LOCATIONS= \ + UPSTREAM_SERVER="service.internal:8080" \ + UPSTREAM_RESOLVE=1 \ + NGINX_RESOLVER="" \ + gomplate < /etc/nginx/nginx.conf.template) + + echo "$fallback_nginx" | grep -F "resolver 169.254.169.253 valid=60s;" \ + || fail "expected default resolver when UPSTREAM_RESOLVE=1 and NGINX_RESOLVER is empty" +fi + LISTEN_PORT="8080" \ KEEPALIVE_TIMEOUT="65" \ PROXY_REVERSE_URL="http://localhost:8081" \