feat: add dns-resolver-timeout property defaulting to 5s

Adds a new `dns-resolver-timeout` global nginx property that controls how long nginx waits on the configured DNS resolver before giving up and returning 502. Without an explicit timeout nginx blocks on the resolver for its built-in 30s default, which means clients see hung requests instead of a fast 502 when coredns-docker is unreachable. The 5s default keeps failure-mode requests short while leaving headroom over a healthy local resolver round trip. The bundled `nginx.conf.sigil` emits `resolver_timeout {{ $.NGINX_DNS_RESOLVER_TIMEOUT }}` next to the existing `resolver` directive, and the outage-mode bats and docker-deploy tests now expect the curl call to complete inside that window.
This commit is contained in:
Jose Diaz-Gonzalez
2026-05-13 12:01:21 -04:00
parent 1d2854fa93
commit b5315de49b
13 changed files with 73 additions and 11 deletions

View File

@@ -325,6 +325,7 @@ fn-nginx-vhosts-render-template() {
local NGINX_BIND_ADDRESS_IP4="$(fn-nginx-computed-bind-address-ipv4 "$APP")"
local NGINX_BIND_ADDRESS_IP6="$(fn-nginx-computed-bind-address-ipv6 "$APP")"
local NGINX_DNS_RESOLVER="$(fn-nginx-computed-dns-resolver "$APP")"
local NGINX_DNS_RESOLVER_TIMEOUT="$(fn-nginx-computed-dns-resolver-timeout "$APP")"
local NGINX_DNS_ZONE="$(fn-nginx-computed-dns-zone "$APP")"
local NGINX_UNDERSCORE_IN_HEADERS="$(fn-nginx-computed-underscore-in-headers "$APP")"
local PROXY_X_FORWARDED_FOR="$(fn-nginx-computed-x-forwarded-for-value "$APP")"
@@ -357,6 +358,7 @@ fn-nginx-vhosts-render-template() {
NGINX_BIND_ADDRESS_IP4="$NGINX_BIND_ADDRESS_IP4"
NGINX_BIND_ADDRESS_IP6="$NGINX_BIND_ADDRESS_IP6"
NGINX_DNS_RESOLVER="$NGINX_DNS_RESOLVER"
NGINX_DNS_RESOLVER_TIMEOUT="$NGINX_DNS_RESOLVER_TIMEOUT"
NGINX_DNS_ZONE="$NGINX_DNS_ZONE"
HTTP2_DIRECTIVE_SUPPORTED="$HTTP2_DIRECTIVE_SUPPORTED"
HTTP2_PUSH_SUPPORTED="$HTTP2_PUSH_SUPPORTED"

View File

@@ -193,6 +193,27 @@ fn-nginx-global-dns-resolver() {
fn-get-property --app "$APP" --global "dns-resolver"
}
fn-nginx-dns-resolver-timeout() {
declare desc="get the configured dns-resolver-timeout value"
declare APP="$1"
fn-get-property --app "$APP" "dns-resolver-timeout"
}
fn-nginx-computed-dns-resolver-timeout() {
declare desc="get the computed dns-resolver-timeout value"
declare APP="$1"
fn-get-property --app "$APP" --computed "dns-resolver-timeout"
}
fn-nginx-global-dns-resolver-timeout() {
declare desc="get the global dns-resolver-timeout value"
declare APP="$1"
fn-get-property --app "$APP" --global "dns-resolver-timeout"
}
fn-nginx-dns-zone() {
declare desc="get the configured dns-zone value"
declare APP="$1"

View File

@@ -167,6 +167,23 @@ func GlobalDnsResolver() string {
return common.PropertyGetDefault("nginx", "--global", "dns-resolver", "127.0.0.1:1053")
}
func AppDnsResolverTimeout(appName string) string {
return common.PropertyGet("nginx", appName, "dns-resolver-timeout")
}
func ComputedDnsResolverTimeout(appName string) string {
appValue := AppDnsResolverTimeout(appName)
if appValue != "" {
return appValue
}
return GlobalDnsResolverTimeout()
}
func GlobalDnsResolverTimeout() string {
return common.PropertyGetDefault("nginx", "--global", "dns-resolver-timeout", "5s")
}
func AppDnsZone(appName string) string {
return common.PropertyGet("nginx", appName, "dns-zone")
}

View File

@@ -30,6 +30,7 @@ func nginxReportProps() []nginxReportProp {
{"client-max-body-size", AppClientMaxBodySize, ComputedClientMaxBodySize, wrapGlobal(GlobalClientMaxBodySize)},
{"disable-custom-config", AppDisableCustomConfig, ComputedDisableCustomConfig, wrapGlobal(GlobalDisableCustomConfig)},
{"dns-resolver", AppDnsResolver, ComputedDnsResolver, wrapGlobal(GlobalDnsResolver)},
{"dns-resolver-timeout", AppDnsResolverTimeout, ComputedDnsResolverTimeout, wrapGlobal(GlobalDnsResolverTimeout)},
{"dns-zone", AppDnsZone, ComputedDnsZone, wrapGlobal(GlobalDnsZone)},
{"error-log-path", AppErrorLogPath, ComputedErrorLogPath, wrapGlobal(GlobalErrorLogPath)},
{"hsts-include-subdomains", AppHSTSIncludeSubdomains, ComputedHSTSIncludeSubdomains, wrapGlobal(GlobalHSTSIncludeSubdomains)},

View File

@@ -53,6 +53,8 @@ func appValue(appName string, property string) string {
value = nginx_vhosts.AppDisableCustomConfig(appName)
case "dns-resolver":
value = nginx_vhosts.AppDnsResolver(appName)
case "dns-resolver-timeout":
value = nginx_vhosts.AppDnsResolverTimeout(appName)
case "dns-zone":
value = nginx_vhosts.AppDnsZone(appName)
case "error-log-path":
@@ -129,6 +131,8 @@ func computedValue(appName string, property string) string {
value = nginx_vhosts.ComputedDisableCustomConfig(appName)
case "dns-resolver":
value = nginx_vhosts.ComputedDnsResolver(appName)
case "dns-resolver-timeout":
value = nginx_vhosts.ComputedDnsResolverTimeout(appName)
case "dns-zone":
value = nginx_vhosts.ComputedDnsZone(appName)
case "error-log-path":
@@ -205,6 +209,8 @@ func globalValue(appName string, property string) string {
value = nginx_vhosts.GlobalDisableCustomConfig()
case "dns-resolver":
value = nginx_vhosts.GlobalDnsResolver()
case "dns-resolver-timeout":
value = nginx_vhosts.GlobalDnsResolverTimeout()
case "dns-zone":
value = nginx_vhosts.GlobalDnsZone()
case "error-log-path":

View File

@@ -9,13 +9,13 @@ cmd-nginx-set() {
declare cmd="nginx:set"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1" KEY="$2" VALUE="$3"
local VALID_KEYS=("access-log-format" "access-log-path" "bind-address-ipv4" "bind-address-ipv6" "client-body-timeout" "client-header-timeout" "client-max-body-size" "disable-custom-config" "dns-resolver" "dns-zone" "error-log-path" "hsts" "hsts-include-subdomains" "hsts-preload" "hsts-max-age" "keepalive-timeout" "lingering-timeout" "nginx-conf-sigil-path" "nginx-service-command" "proxy-buffer-size" "proxy-buffering" "proxy-buffers" "proxy-busy-buffers-size" "proxy-http-version" "proxy-connect-timeout" "proxy-read-timeout" "proxy-send-timeout" "proxy-keepalive" "send-timeout" "underscore-in-headers" "x-forwarded-for-value" "x-forwarded-port-value" "x-forwarded-proto-value" "x-forwarded-ssl")
local GLOBAL_KEYS=("access-log-format" "access-log-path" "bind-address-ipv4" "bind-address-ipv6" "client-body-timeout" "client-header-timeout" "client-max-body-size" "disable-custom-config" "dns-resolver" "dns-zone" "error-log-path" "hsts-include-subdomains" "hsts-max-age" "hsts-preload" "hsts" "keepalive-timeout" "lingering-timeout" "nginx-conf-sigil-path" "nginx-service-command" "proxy-buffer-size" "proxy-buffering" "proxy-buffers" "proxy-busy-buffers-size" "proxy-http-version" "proxy-connect-timeout" "proxy-read-timeout" "proxy-send-timeout" "proxy-keepalive" "send-timeout" "underscore-in-headers" "x-forwarded-for-value" "x-forwarded-port-value" "x-forwarded-proto-value" "x-forwarded-ssl")
local VALID_KEYS=("access-log-format" "access-log-path" "bind-address-ipv4" "bind-address-ipv6" "client-body-timeout" "client-header-timeout" "client-max-body-size" "disable-custom-config" "dns-resolver" "dns-resolver-timeout" "dns-zone" "error-log-path" "hsts" "hsts-include-subdomains" "hsts-preload" "hsts-max-age" "keepalive-timeout" "lingering-timeout" "nginx-conf-sigil-path" "nginx-service-command" "proxy-buffer-size" "proxy-buffering" "proxy-buffers" "proxy-busy-buffers-size" "proxy-http-version" "proxy-connect-timeout" "proxy-read-timeout" "proxy-send-timeout" "proxy-keepalive" "send-timeout" "underscore-in-headers" "x-forwarded-for-value" "x-forwarded-port-value" "x-forwarded-proto-value" "x-forwarded-ssl")
local GLOBAL_KEYS=("access-log-format" "access-log-path" "bind-address-ipv4" "bind-address-ipv6" "client-body-timeout" "client-header-timeout" "client-max-body-size" "disable-custom-config" "dns-resolver" "dns-resolver-timeout" "dns-zone" "error-log-path" "hsts-include-subdomains" "hsts-max-age" "hsts-preload" "hsts" "keepalive-timeout" "lingering-timeout" "nginx-conf-sigil-path" "nginx-service-command" "proxy-buffer-size" "proxy-buffering" "proxy-buffers" "proxy-busy-buffers-size" "proxy-http-version" "proxy-connect-timeout" "proxy-read-timeout" "proxy-send-timeout" "proxy-keepalive" "send-timeout" "underscore-in-headers" "x-forwarded-for-value" "x-forwarded-port-value" "x-forwarded-proto-value" "x-forwarded-ssl")
[[ -z "$KEY" ]] && dokku_log_fail "No key specified"
if ! fn-in-array "$KEY" "${VALID_KEYS[@]}"; then
dokku_log_fail "Invalid key specified, valid keys include: access-log-format, access-log-path, bind-address-ipv4, bind-address-ipv6, client-body-timeout, client-header-timeout, client-max-body-size, disable-custom-config, dns-resolver, dns-zone, error-log-path, hsts, hsts-include-subdomains, hsts-preload, hsts-max-age, keepalive-timeout, lingering-timeout, nginx-conf-sigil-path, nginx-service-command, proxy-buffer-size, proxy-buffering, proxy-buffers, proxy-busy-buffers-size, proxy-http-version, proxy-connect-timeout, proxy-read-timeout, proxy-send-timeout, proxy-keepalive, send-timeout, underscore-in-headers, x-forwarded-for-value, x-forwarded-port-value, x-forwarded-proto-value, x-forwarded-ssl"
dokku_log_fail "Invalid key specified, valid keys include: access-log-format, access-log-path, bind-address-ipv4, bind-address-ipv6, client-body-timeout, client-header-timeout, client-max-body-size, disable-custom-config, dns-resolver, dns-resolver-timeout, dns-zone, error-log-path, hsts, hsts-include-subdomains, hsts-preload, hsts-max-age, keepalive-timeout, lingering-timeout, nginx-conf-sigil-path, nginx-service-command, proxy-buffer-size, proxy-buffering, proxy-buffers, proxy-busy-buffers-size, proxy-http-version, proxy-connect-timeout, proxy-read-timeout, proxy-send-timeout, proxy-keepalive, send-timeout, underscore-in-headers, x-forwarded-for-value, x-forwarded-port-value, x-forwarded-proto-value, x-forwarded-ssl"
fi
if ! fn-in-array "$KEY" "${GLOBAL_KEYS[@]}" && [[ "$APP" == "--global" ]]; then

View File

@@ -39,6 +39,7 @@ func defaultVars() map[string]interface{} {
"NGINX_BIND_ADDRESS_IP4": "",
"NGINX_BIND_ADDRESS_IP6": "::",
"NGINX_DNS_RESOLVER": "127.0.0.1:1053",
"NGINX_DNS_RESOLVER_TIMEOUT": "5s",
"NGINX_DNS_ZONE": "docker",
"NGINX_ACCESS_LOG_PATH": "/var/log/nginx/app-access.log",
"NGINX_ACCESS_LOG_FORMAT": "",
@@ -99,6 +100,7 @@ func TestTemplate_HTTPOnlyBasicProxy(t *testing.T) {
out := renderTemplate(t, defaultVars())
mustContain(t, out, "listen [::]:80;")
mustContain(t, out, "resolver 127.0.0.1:1053 valid=10s ipv6=off;")
mustContain(t, out, "resolver_timeout 5s;")
mustContain(t, out, `set $dokku_upstream "app.web.docker:5000";`)
mustContain(t, out, "proxy_pass http://$dokku_upstream;")
mustNotContain(t, out, "upstream app-5000 {")
@@ -109,6 +111,14 @@ func TestTemplate_HTTPOnlyBasicProxy(t *testing.T) {
mustNotContain(t, out, "http2_push_preload")
}
func TestTemplate_ResolverTimeoutOverride(t *testing.T) {
v := defaultVars()
v["NGINX_DNS_RESOLVER_TIMEOUT"] = "2s"
out := renderTemplate(t, v)
mustContain(t, out, "resolver_timeout 2s;")
mustNotContain(t, out, "resolver_timeout 5s;")
}
func TestTemplate_HTTPOnlyBasicProxyWithResolverOff(t *testing.T) {
v := defaultVars()
v["NGINX_DNS_RESOLVER"] = "off"

View File

@@ -86,6 +86,7 @@ server {
{{ if ne $.NGINX_DNS_RESOLVER "off" }}
resolver {{ $.NGINX_DNS_RESOLVER }} valid=10s ipv6=off;
resolver_timeout {{ $.NGINX_DNS_RESOLVER_TIMEOUT }};
set $dokku_upstream "{{ $.DOKKU_APP_WEB_LISTENER_HOST }}:{{ $upstream_port }}";
proxy_pass http://$dokku_upstream;
{{ else }}
@@ -174,6 +175,7 @@ server {
location / {
{{ if ne $.NGINX_DNS_RESOLVER "off" }}
resolver {{ $.NGINX_DNS_RESOLVER }} valid=10s ipv6=off;
resolver_timeout {{ $.NGINX_DNS_RESOLVER_TIMEOUT }};
set $dokku_upstream "{{ $.DOKKU_APP_WEB_LISTENER_HOST }}:{{ $upstream_port }}";
grpc_pass grpc://$dokku_upstream;
{{ else }}