feat(nginx): generate 502 config for apps without web listeners

When an app has no web listeners (not yet deployed, no web process type,
or web processes not running), generate a minimal nginx config that
returns 502 Bad Gateway instead of having no config at all. This ensures
domains resolve, monitoring tools detect non-200 status codes, and SSL
certificate provisioning tools like letsencrypt can function.
This commit is contained in:
Jose Diaz-Gonzalez
2026-04-24 02:10:30 -04:00
parent 71ec612fbd
commit 2993fa800c
6 changed files with 218 additions and 11 deletions

View File

@@ -27,6 +27,21 @@ Nginx will proxy the requests in a [round-robin balancing fashion](http://nginx.
> [!NOTE]
> Due to how the plugin is implemented, if an app successfully starts up `web` containers but fails to deploy some other containers, nginx may eventually stop routing requests. Users should revert their code in these cases, or manually trigger `dokku proxy:build-config $APP` in order to ensure requests route to the new web containers.
### Nginx Configuration for Undeployed Apps
> [!IMPORTANT]
> New as of 0.37.0
When an app is created but not yet deployed, has no `web` process type, or has no running web processes, Dokku generates a minimal nginx configuration that returns `502 Bad Gateway` responses. This ensures that:
- The app's domain resolves and returns a non-200 status code, allowing monitoring tools to detect the issue.
- SSL certificate provisioning tools such as letsencrypt can function, as an nginx server is listening on the domain.
- The `nginx.conf.d/` include directory is available for plugin customization.
The 502 error page includes auto-retry JavaScript that will automatically reload the page when the application becomes available.
Once the app is deployed with running `web` processes, the placeholder configuration is automatically replaced with the full proxy configuration.
### Starting nginx
> [!IMPORTANT]

View File

@@ -306,9 +306,19 @@ nginx_build_config() {
local PROXY_BUSY_BUFFERS_SIZE="$(fn-nginx-computed-proxy-busy-buffers-size "$APP")"
local PROXY_KEEPALIVE="$(fn-nginx-computed-proxy-keepalive "$APP")"
if [[ -z "$DOKKU_APP_LISTENERS" ]]; then
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_UNDERSCORE_IN_HEADERS="$(fn-nginx-computed-underscore-in-headers "$APP")"
local PROXY_X_FORWARDED_FOR="$(fn-nginx-computed-x-forwarded-for-value "$APP")"
local PROXY_X_FORWARDED_PORT="$(fn-nginx-computed-x-forwarded-port-value "$APP")"
local PROXY_X_FORWARDED_PROTO="$(fn-nginx-computed-x-forwarded-proto-value "$APP")"
local PROXY_X_FORWARDED_SSL="$(fn-nginx-computed-x-forwarded-ssl "$APP")"
if [[ -z "$DOKKU_APP_LISTENERS" ]] && [[ -z "$PASSED_LISTEN_IP_PORT" ]]; then
dokku_log_warn_quiet "No web listeners specified for $APP"
elif (is_deployed "$APP"); then
fi
if [[ -n "$DOKKU_APP_LISTENERS" ]] && (is_deployed "$APP"); then
if [[ "$(plugn trigger network-get-static-listeners "$APP" "web")" == "" ]]; then
local IMAGE_TAG=$(get_running_image_tag "$APP")
local IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG" 2>/dev/null)
@@ -329,14 +339,6 @@ nginx_build_config() {
local NGINX_TEMPLATE_SOURCE="app-supplied"
fi
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_UNDERSCORE_IN_HEADERS="$(fn-nginx-computed-underscore-in-headers "$APP")"
local PROXY_X_FORWARDED_FOR="$(fn-nginx-computed-x-forwarded-for-value "$APP")"
local PROXY_X_FORWARDED_PORT="$(fn-nginx-computed-x-forwarded-port-value "$APP")"
local PROXY_X_FORWARDED_PROTO="$(fn-nginx-computed-x-forwarded-proto-value "$APP")"
local PROXY_X_FORWARDED_SSL="$(fn-nginx-computed-x-forwarded-ssl "$APP")"
eval "$(config_export app "$APP")"
local SIGIL_PARAMS=(-f "$NGINX_TEMPLATE" APP="$APP" DOKKU_ROOT="$DOKKU_ROOT"
NOSSL_SERVER_NAME="$NOSSL_SERVER_NAME"
@@ -413,6 +415,65 @@ nginx_build_config() {
plugn trigger nginx-pre-reload "$APP" "$DOKKU_APP_LISTEN_PORT" "$DOKKU_APP_LISTEN_IP"
dokku_log_verbose "Reloading nginx"
validate_nginx && restart_nginx >/dev/null
elif [[ -z "$DOKKU_APP_LISTENERS" ]] && [[ -z "$PASSED_LISTEN_IP_PORT" ]]; then
local NGINX_BUILD_CONFIG_TMP_WORK_DIR=$(mktemp -d "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX")
local NGINX_CONF=$(mktemp --tmpdir="${NGINX_BUILD_CONFIG_TMP_WORK_DIR}" "nginx.conf.XXXXXX")
trap "rm -rf '$NGINX_CONF' '$NGINX_BUILD_CONFIG_TMP_WORK_DIR' >/dev/null" RETURN INT TERM EXIT
local SIGIL_PARAMS=(-f "$NGINX_TEMPLATE" APP="$APP" DOKKU_ROOT="$DOKKU_ROOT"
NOSSL_SERVER_NAME="$NOSSL_SERVER_NAME"
DOKKU_APP_LISTENERS=""
DOKKU_APP_WEB_LISTENERS=""
DOKKU_LIB_ROOT="$DOKKU_LIB_ROOT"
PASSED_LISTEN_IP_PORT=""
SPDY_SUPPORTED="$SPDY_SUPPORTED"
TLS13_SUPPORTED="$TLS13_SUPPORTED"
HTTP2_SUPPORTED="$HTTP2_SUPPORTED"
NGINX_LOG_ROOT="$NGINX_LOG_ROOT"
NGINX_ACCESS_LOG_FORMAT="$NGINX_ACCESS_LOG_FORMAT"
NGINX_ACCESS_LOG_PATH="$NGINX_ACCESS_LOG_PATH"
NGINX_ERROR_LOG_PATH="$NGINX_ERROR_LOG_PATH"
NGINX_BIND_ADDRESS_IP4="$NGINX_BIND_ADDRESS_IP4"
NGINX_BIND_ADDRESS_IP6="$NGINX_BIND_ADDRESS_IP6"
HTTP2_DIRECTIVE_SUPPORTED="$HTTP2_DIRECTIVE_SUPPORTED"
HTTP2_PUSH_SUPPORTED="$HTTP2_PUSH_SUPPORTED"
GRPC_SUPPORTED="$GRPC_SUPPORTED"
DOKKU_APP_LISTEN_PORT="" DOKKU_APP_LISTEN_IP=""
APP_SSL_PATH="$APP_SSL_PATH" SSL_INUSE="$SSL_INUSE" SSL_SERVER_NAME="$SSL_SERVER_NAME"
CLIENT_BODY_TIMEOUT="$CLIENT_BODY_TIMEOUT"
CLIENT_HEADER_TIMEOUT="$CLIENT_HEADER_TIMEOUT"
CLIENT_MAX_BODY_SIZE="$CLIENT_MAX_BODY_SIZE"
KEEPALIVE_TIMEOUT="$KEEPALIVE_TIMEOUT"
LINGERING_TIMEOUT="$LINGERING_TIMEOUT"
PROXY_CONNECT_TIMEOUT="$PROXY_CONNECT_TIMEOUT"
PROXY_READ_TIMEOUT="$PROXY_READ_TIMEOUT"
PROXY_SEND_TIMEOUT="$PROXY_SEND_TIMEOUT"
SEND_TIMEOUT="$SEND_TIMEOUT"
PROXY_BUFFER_SIZE="$PROXY_BUFFER_SIZE"
PROXY_BUFFERING="$PROXY_BUFFERING"
PROXY_BUFFERS="$PROXY_BUFFERS"
PROXY_BUSY_BUFFERS_SIZE="$PROXY_BUSY_BUFFERS_SIZE"
PROXY_KEEPALIVE="$PROXY_KEEPALIVE"
NGINX_UNDERSCORE_IN_HEADERS="$NGINX_UNDERSCORE_IN_HEADERS"
NGINX_PORT="$PROXY_PORT" NGINX_SSL_PORT="$PROXY_SSL_PORT"
PROXY_PORT="$PROXY_PORT" PROXY_SSL_PORT="$PROXY_SSL_PORT"
PROXY_PORT_MAP="$PROXY_PORT_MAP" PROXY_UPSTREAM_PORTS="$PROXY_UPSTREAM_PORTS"
PROXY_X_FORWARDED_FOR="$PROXY_X_FORWARDED_FOR"
PROXY_X_FORWARDED_PORT="$PROXY_X_FORWARDED_PORT"
PROXY_X_FORWARDED_PROTO="$PROXY_X_FORWARDED_PROTO"
PROXY_X_FORWARDED_SSL="$PROXY_X_FORWARDED_SSL")
xargs -i echo "-----> Configuring {}...(using $NGINX_TEMPLATE_SOURCE template)" <<<"$(echo "${SSL_VHOSTS}" "${NONSSL_VHOSTS}" | tr ' ' '\n' | sort -u)"
sigil "${SIGIL_PARAMS[@]}" | cat -s >"$NGINX_CONF"
dokku_log_info1 "Creating $SCHEME nginx.conf"
mkdir -p "$DOKKU_ROOT/$APP/nginx.conf.d"
mv "$NGINX_CONF" "$DOKKU_ROOT/$APP/nginx.conf"
plugn trigger nginx-pre-reload "$APP" "$DOKKU_APP_LISTEN_PORT" "$DOKKU_APP_LISTEN_IP"
dokku_log_verbose "Reloading nginx"
validate_nginx && restart_nginx >/dev/null
fi

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions"
source "$PLUGIN_AVAILABLE_PATH/nginx-vhosts/functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
trigger-nginx-vhosts-post-create() {
declare desc="nginx-vhosts post-create trigger"
declare trigger="post-create"
declare APP="$1"
fn-plugin-property-setup-app "nginx" "$APP"
if [[ "$(plugn trigger proxy-type "$APP")" != "nginx" ]]; then
return
fi
nginx_build_config "$APP"
}
trigger-nginx-vhosts-post-create "$@"

View File

@@ -26,7 +26,7 @@ trigger-nginx-vhosts-proxy-build-config() {
rm -f "${DOKKU_LIB_ROOT}/data/nginx-vhosts/app-$APP/nginx.conf.sigil"
fi
else
nginx_clear_config "$APP"
nginx_build_config "$APP"
fi
}

View File

@@ -26,6 +26,7 @@ server {
}
{{ else }}
location / {
{{ if $.DOKKU_APP_WEB_LISTENERS }}
gzip on;
gzip_min_length 1100;
@@ -51,6 +52,9 @@ server {
proxy_set_header X-Forwarded-Proto {{ $.PROXY_X_FORWARDED_PROTO }};
proxy_set_header X-Request-Start $msec;
{{ if $.PROXY_X_FORWARDED_SSL }}proxy_set_header X-Forwarded-Ssl {{ $.PROXY_X_FORWARDED_SSL }};{{ end }}
{{ else }}
return 502;
{{ end }}
}
client_max_body_size {{ $.CLIENT_MAX_BODY_SIZE }};
@@ -103,6 +107,7 @@ server {
send_timeout {{ $.SEND_TIMEOUT }};
location / {
{{ if $.DOKKU_APP_WEB_LISTENERS }}
gzip on;
gzip_min_length 1100;
@@ -129,6 +134,9 @@ server {
proxy_set_header X-Forwarded-Proto {{ $.PROXY_X_FORWARDED_PROTO }};
proxy_set_header X-Request-Start $msec;
{{ if $.PROXY_X_FORWARDED_SSL }}proxy_set_header X-Forwarded-Ssl {{ $.PROXY_X_FORWARDED_SSL }};{{ end }}
{{ else }}
return 502;
{{ end }}
}
client_max_body_size {{ $.CLIENT_MAX_BODY_SIZE }};
@@ -159,6 +167,7 @@ server {
include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
}
{{ else if eq $scheme "grpc"}}
{{ if $.DOKKU_APP_WEB_LISTENERS }}
server {
{{ if eq $.HTTP2_DIRECTIVE_SUPPORTED "true" }}
listen [{{ $.NGINX_BIND_ADDRESS_IP6 }}]:{{ $listen_port }};
@@ -179,7 +188,9 @@ server {
client_max_body_size {{ $.CLIENT_MAX_BODY_SIZE }};
include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
}
{{ end }}
{{ else if eq $scheme "grpcs"}}
{{ if $.DOKKU_APP_WEB_LISTENERS }}
server {
{{ if eq $.HTTP2_DIRECTIVE_SUPPORTED "true" }}
listen [{{ $.NGINX_BIND_ADDRESS_IP6 }}]:{{ $listen_port }} ssl;
@@ -208,6 +219,7 @@ server {
}
{{ end }}
{{ end }}
{{ end }}
{{ if $.DOKKU_APP_WEB_LISTENERS }}
{{ range $upstream_port := $.PROXY_UPSTREAM_PORTS | split " " }}

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
[[ -f "$DOKKU_ROOT/VHOST" ]] && cp -fp "$DOKKU_ROOT/VHOST" "$DOKKU_ROOT/VHOST.bak"
}
teardown() {
destroy_app
[[ -f "$DOKKU_ROOT/VHOST.bak" ]] && mv "$DOKKU_ROOT/VHOST.bak" "$DOKKU_ROOT/VHOST" && chown dokku:dokku "$DOKKU_ROOT/VHOST"
global_teardown
}
@test "(nginx-vhosts) proxy:build-config generates 502 config for undeployed app" {
run create_app
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "test -f $DOKKU_ROOT/$TEST_APP/nginx.conf"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku nginx:show-config $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "return 502" -1
}
@test "(nginx-vhosts) proxy:build-config undeployed app returns 502" {
run create_app
echo "output: $output"
echo "status: $status"
assert_success
assert_http_localhost_response "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/" "" "502"
}
@test "(nginx-vhosts) proxy:build-config 502 config replaced after deploy" {
run deploy_app
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku nginx:show-config $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "return 502" 0
assert_output_contains "proxy_pass" -1
assert_http_success "http://${TEST_APP}.${DOKKU_DOMAIN}"
}
@test "(nginx-vhosts) proxy:build-config domains:add updates 502 config" {
run create_app
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku domains:add $TEST_APP custom.${DOKKU_DOMAIN}"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku nginx:show-config $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "custom.${DOKKU_DOMAIN}" -1
assert_output_contains "return 502" -1
}
@test "(nginx-vhosts) proxy:build-config no nginx.conf when proxy disabled" {
run create_app
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "test -f $DOKKU_ROOT/$TEST_APP/nginx.conf"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku proxy:disable $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "test -f $DOKKU_ROOT/$TEST_APP/nginx.conf"
echo "output: $output"
echo "status: $status"
assert_failure
}