From 2993fa800ce228cb31186a5bbdd367a4e3b23c8a Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Fri, 24 Apr 2026 02:10:30 -0400 Subject: [PATCH 1/8] 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. --- docs/networking/proxies/nginx.md | 15 +++ plugins/nginx-vhosts/functions | 81 +++++++++++++-- plugins/nginx-vhosts/post-create | 21 ++++ plugins/nginx-vhosts/proxy-build-config | 2 +- .../nginx-vhosts/templates/nginx.conf.sigil | 12 +++ tests/unit/nginx-vhosts_13.bats | 98 +++++++++++++++++++ 6 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 plugins/nginx-vhosts/post-create create mode 100644 tests/unit/nginx-vhosts_13.bats diff --git a/docs/networking/proxies/nginx.md b/docs/networking/proxies/nginx.md index 330f5fa0f..8ca02d0f6 100644 --- a/docs/networking/proxies/nginx.md +++ b/docs/networking/proxies/nginx.md @@ -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] diff --git a/plugins/nginx-vhosts/functions b/plugins/nginx-vhosts/functions index 0f81141c4..dd5fdf453 100755 --- a/plugins/nginx-vhosts/functions +++ b/plugins/nginx-vhosts/functions @@ -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 diff --git a/plugins/nginx-vhosts/post-create b/plugins/nginx-vhosts/post-create new file mode 100644 index 000000000..161dd046e --- /dev/null +++ b/plugins/nginx-vhosts/post-create @@ -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 "$@" diff --git a/plugins/nginx-vhosts/proxy-build-config b/plugins/nginx-vhosts/proxy-build-config index c9c330e6c..ea95b757f 100755 --- a/plugins/nginx-vhosts/proxy-build-config +++ b/plugins/nginx-vhosts/proxy-build-config @@ -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 } diff --git a/plugins/nginx-vhosts/templates/nginx.conf.sigil b/plugins/nginx-vhosts/templates/nginx.conf.sigil index a7ec0e382..d30b8fe9a 100644 --- a/plugins/nginx-vhosts/templates/nginx.conf.sigil +++ b/plugins/nginx-vhosts/templates/nginx.conf.sigil @@ -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 " " }} diff --git a/tests/unit/nginx-vhosts_13.bats b/tests/unit/nginx-vhosts_13.bats new file mode 100644 index 000000000..049e1f46c --- /dev/null +++ b/tests/unit/nginx-vhosts_13.bats @@ -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 +} From 5bf82d258512249d70ed08ed3264b4dea427ca87 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Fri, 24 Apr 2026 02:40:27 -0400 Subject: [PATCH 2/8] docs: add note about when the new feature lands --- docs/networking/proxies/nginx.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/networking/proxies/nginx.md b/docs/networking/proxies/nginx.md index 8ca02d0f6..9e6c63d56 100644 --- a/docs/networking/proxies/nginx.md +++ b/docs/networking/proxies/nginx.md @@ -30,7 +30,7 @@ Nginx will proxy the requests in a [round-robin balancing fashion](http://nginx. ### Nginx Configuration for Undeployed Apps > [!IMPORTANT] -> New as of 0.37.0 +> New as of 0.38.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: From 20f2100d467acf8d6303ed7fc7d2c8c6429a5b71 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Fri, 24 Apr 2026 02:54:06 -0400 Subject: [PATCH 3/8] docs: add 0.38.0 migration guide with nginx.conf.sigil update notes Custom nginx.conf.sigil templates that reference DOKKU_APP_WEB_LISTENERS may now receive an empty value when rendered for apps without running web processes. The migration guide documents how to handle this with a conditional in the template. --- docs/README.md | 1 + docs/appendices/0.38.0-migration-guide.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 docs/appendices/0.38.0-migration-guide.md diff --git a/docs/README.md b/docs/README.md index 08ce6096d..b4e6d5c81 100644 --- a/docs/README.md +++ b/docs/README.md @@ -121,6 +121,7 @@ This documentation covers the installation, configuration, and usage of Dokku - ### Migration Guides +- [0.38.0](appendices/0.38.0-migration-guide.md) - [0.37.0](appendices/0.37.0-migration-guide.md) - [0.36.0](appendices/0.36.0-migration-guide.md) - [0.35.0](appendices/0.35.0-migration-guide.md) diff --git a/docs/appendices/0.38.0-migration-guide.md b/docs/appendices/0.38.0-migration-guide.md new file mode 100644 index 000000000..e547325ec --- /dev/null +++ b/docs/appendices/0.38.0-migration-guide.md @@ -0,0 +1,15 @@ +# 0.38.0 Migration Guide + +## Changes + +- Dokku now generates a minimal nginx configuration for apps without running `web` processes (undeployed apps, apps with no `web` process type, or apps with stopped web processes). This configuration returns `502 Bad Gateway` responses, ensuring the app's domain resolves and monitoring tools can detect non-200 status codes. The configuration is automatically replaced with the full proxy configuration once the app is deployed with running `web` processes. See the [nginx documentation](/docs/networking/proxies/nginx.md#nginx-configuration-for-undeployed-apps) for more details. +- Users with custom `nginx.conf.sigil` templates that reference `DOKKU_APP_WEB_LISTENERS` should be aware that this variable may now be empty when the template is rendered for apps without running web processes. Custom templates should handle this case gracefully, for example by using a conditional to serve an error page instead of proxying: + ``` + location / { + {{ if $.DOKKU_APP_WEB_LISTENERS }} + proxy_pass http://{{ $.APP }}-{{ $upstream_port }}; + {{ else }} + return 502; + {{ end }} + } + ``` From 7529d8cb081efd5f6ca708d29b9474a77443df2a Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Fri, 24 Apr 2026 03:04:04 -0400 Subject: [PATCH 4/8] refactor(nginx): deduplicate sigil params in nginx_build_config Merge the deployed and empty-listeners branches into a single flow that shares the SIGIL_PARAMS array, temp dir setup, sigil rendering, and nginx reload. Branch only for deployed-specific logic: image verification, custom template lookup, per-process listeners, and HSTS. --- plugins/nginx-vhosts/functions | 137 +++++++++++---------------------- 1 file changed, 47 insertions(+), 90 deletions(-) diff --git a/plugins/nginx-vhosts/functions b/plugins/nginx-vhosts/functions index dd5fdf453..8ff9386f6 100755 --- a/plugins/nginx-vhosts/functions +++ b/plugins/nginx-vhosts/functions @@ -318,28 +318,38 @@ nginx_build_config() { dokku_log_warn_quiet "No web listeners specified for $APP" fi + local IS_DEPLOYED_WITH_LISTENERS=false 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) - if ! verify_image "$IMAGE" 2>/dev/null; then - dokku_log_fail "Missing image for app" + IS_DEPLOYED_WITH_LISTENERS=true + fi + + if [[ "$IS_DEPLOYED_WITH_LISTENERS" == "true" ]] || { [[ -z "$DOKKU_APP_LISTENERS" ]] && [[ -z "$PASSED_LISTEN_IP_PORT" ]]; }; then + if [[ "$IS_DEPLOYED_WITH_LISTENERS" == "true" ]]; 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) + if ! verify_image "$IMAGE" 2>/dev/null; then + dokku_log_fail "Missing image for app" + fi fi fi 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") - local CUSTOM_NGINX_TEMPLATE="$NGINX_BUILD_CONFIG_TMP_WORK_DIR/$NGINX_TEMPLATE_NAME" trap "rm -rf '$NGINX_CONF' '$NGINX_BUILD_CONFIG_TMP_WORK_DIR' >/dev/null" RETURN INT TERM EXIT - get_custom_nginx_template "$APP" "$CUSTOM_NGINX_TEMPLATE" 2>/dev/null - if [[ -f "$CUSTOM_NGINX_TEMPLATE" ]]; then - dokku_log_info1 'Overriding default nginx.conf with detected nginx.conf.sigil' - local NGINX_TEMPLATE="$CUSTOM_NGINX_TEMPLATE" - local NGINX_TEMPLATE_SOURCE="app-supplied" + if [[ "$IS_DEPLOYED_WITH_LISTENERS" == "true" ]]; then + local CUSTOM_NGINX_TEMPLATE="$NGINX_BUILD_CONFIG_TMP_WORK_DIR/$NGINX_TEMPLATE_NAME" + get_custom_nginx_template "$APP" "$CUSTOM_NGINX_TEMPLATE" 2>/dev/null + if [[ -f "$CUSTOM_NGINX_TEMPLATE" ]]; then + dokku_log_info1 'Overriding default nginx.conf with detected nginx.conf.sigil' + local NGINX_TEMPLATE="$CUSTOM_NGINX_TEMPLATE" + local NGINX_TEMPLATE_SOURCE="app-supplied" + fi + + eval "$(config_export app "$APP")" fi - eval "$(config_export app "$APP")" local SIGIL_PARAMS=(-f "$NGINX_TEMPLATE" APP="$APP" DOKKU_ROOT="$DOKKU_ROOT" NOSSL_SERVER_NAME="$NOSSL_SERVER_NAME" # Deprecated: Remove this after a few versions @@ -384,87 +394,30 @@ nginx_build_config() { PROXY_X_FORWARDED_PROTO="$PROXY_X_FORWARDED_PROTO" PROXY_X_FORWARDED_SSL="$PROXY_X_FORWARDED_SSL") - while read -r line || [[ -n "$line" ]]; do - PROC_TYPE=${line%%=*} - LISTENERS="$(plugn trigger network-get-listeners "$APP" "$PROC_TYPE" | xargs)" - UPP_PROC_TYPE="${PROC_TYPE^^}" - UPP_PROC_TYPE="${UPP_PROC_TYPE//-/_}" - SIGIL_PARAMS+=("DOKKU_APP_${UPP_PROC_TYPE}_LISTENERS=$LISTENERS") - done < <(plugn trigger ps-current-scale "$APP") + if [[ "$IS_DEPLOYED_WITH_LISTENERS" == "true" ]]; then + while read -r line || [[ -n "$line" ]]; do + PROC_TYPE=${line%%=*} + LISTENERS="$(plugn trigger network-get-listeners "$APP" "$PROC_TYPE" | xargs)" + UPP_PROC_TYPE="${PROC_TYPE^^}" + UPP_PROC_TYPE="${UPP_PROC_TYPE//-/_}" + SIGIL_PARAMS+=("DOKKU_APP_${UPP_PROC_TYPE}_LISTENERS=$LISTENERS") + done < <(plugn trigger ps-current-scale "$APP") - if grep DOKKU_APP_LISTENERS "$NGINX_TEMPLATE"; then - dokku_log_warn "Deprecated: Usage of DOKKU_APP_LISTENERS within nginx.conf.sigil templates is deprecated in favor of DOKKU_APP_WEB_LISTENERS" + if grep DOKKU_APP_LISTENERS "$NGINX_TEMPLATE"; then + dokku_log_warn "Deprecated: Usage of DOKKU_APP_LISTENERS within nginx.conf.sigil templates is deprecated in favor of DOKKU_APP_WEB_LISTENERS" + fi + + if grep NGINX_SSL_PORT "$NGINX_TEMPLATE"; then + dokku_log_warn "Deprecated: Usage of NGINX_SSL_PORT within nginx.conf.sigil templates is deprecated in favor of PROXY_SSL_PORT" + fi + + if grep NGINX_PORT "$NGINX_TEMPLATE"; then + dokku_log_warn "Deprecated: Usage of NGINX_PORT within nginx.conf.sigil templates is deprecated in favor of PROXY_PORT" + fi + else + SIGIL_PARAMS+=(DOKKU_APP_WEB_LISTENERS="") fi - if grep NGINX_SSL_PORT "$NGINX_TEMPLATE"; then - dokku_log_warn "Deprecated: Usage of NGINX_SSL_PORT within nginx.conf.sigil templates is deprecated in favor of PROXY_SSL_PORT" - fi - - if grep NGINX_PORT "$NGINX_TEMPLATE"; then - dokku_log_warn "Deprecated: Usage of NGINX_PORT within nginx.conf.sigil templates is deprecated in favor of PROXY_PORT" - fi - - # execute sigil template processing - 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" - mv "$NGINX_CONF" "$DOKKU_ROOT/$APP/nginx.conf" - - fn-nginx-vhosts-manage-hsts "$APP" "$SSL_INUSE" - - 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" @@ -472,6 +425,10 @@ nginx_build_config() { mkdir -p "$DOKKU_ROOT/$APP/nginx.conf.d" mv "$NGINX_CONF" "$DOKKU_ROOT/$APP/nginx.conf" + if [[ "$IS_DEPLOYED_WITH_LISTENERS" == "true" ]]; then + fn-nginx-vhosts-manage-hsts "$APP" "$SSL_INUSE" + fi + plugn trigger nginx-pre-reload "$APP" "$DOKKU_APP_LISTEN_PORT" "$DOKKU_APP_LISTEN_IP" dokku_log_verbose "Reloading nginx" From fc7ddf34ba151101b70a91d682fa0aff38dadbf3 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Fri, 24 Apr 2026 03:57:22 -0400 Subject: [PATCH 5/8] fix(nginx): make post-create executable and guard empty port maps The post-create trigger was not executable so plugn skipped it. Also skip generating the 502 config when PROXY_PORT_MAP is empty (e.g. after ports:clear) to avoid a sigil template index error. --- plugins/nginx-vhosts/functions | 2 +- plugins/nginx-vhosts/post-create | 0 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 plugins/nginx-vhosts/post-create diff --git a/plugins/nginx-vhosts/functions b/plugins/nginx-vhosts/functions index 8ff9386f6..152bec39f 100755 --- a/plugins/nginx-vhosts/functions +++ b/plugins/nginx-vhosts/functions @@ -323,7 +323,7 @@ nginx_build_config() { IS_DEPLOYED_WITH_LISTENERS=true fi - if [[ "$IS_DEPLOYED_WITH_LISTENERS" == "true" ]] || { [[ -z "$DOKKU_APP_LISTENERS" ]] && [[ -z "$PASSED_LISTEN_IP_PORT" ]]; }; then + if [[ "$IS_DEPLOYED_WITH_LISTENERS" == "true" ]] || { [[ -z "$DOKKU_APP_LISTENERS" ]] && [[ -z "$PASSED_LISTEN_IP_PORT" ]] && [[ -n "$PROXY_PORT_MAP" ]]; }; then if [[ "$IS_DEPLOYED_WITH_LISTENERS" == "true" ]]; then if [[ "$(plugn trigger network-get-static-listeners "$APP" "web")" == "" ]]; then local IMAGE_TAG=$(get_running_image_tag "$APP") diff --git a/plugins/nginx-vhosts/post-create b/plugins/nginx-vhosts/post-create old mode 100644 new mode 100755 From dcce09f19f55cc88bcc2575e0676aa4273543fa8 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Fri, 24 Apr 2026 04:14:49 -0400 Subject: [PATCH 6/8] fix(nginx): skip post-create config when nginx is stopped When nginx is stopped (e.g. during traefik/caddy tests), the post-create trigger should not attempt to build and reload the nginx config as the service is not running. --- plugins/nginx-vhosts/post-create | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/nginx-vhosts/post-create b/plugins/nginx-vhosts/post-create index 161dd046e..04f15e915 100755 --- a/plugins/nginx-vhosts/post-create +++ b/plugins/nginx-vhosts/post-create @@ -15,6 +15,10 @@ trigger-nginx-vhosts-post-create() { return fi + if [[ "$(fn-plugin-property-get "nginx" "--global" "proxy-status")" == "stopped" ]]; then + return + fi + nginx_build_config "$APP" } From ddf470282e9d8c59c76b203c789f76b7f669e12e Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Fri, 24 Apr 2026 05:02:49 -0400 Subject: [PATCH 7/8] fix(tests): update config and nginx tests for post-create side effects The config:show and config:export tests now include DOKKU_PROXY_PORT in expected output since ports-configure runs at app creation time. Add sleep before HTTP assertion in nginx-vhosts_13 test to allow nginx async reload to complete. --- tests/unit/config.bats | 10 +++++----- tests/unit/nginx-vhosts_13.bats | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/unit/config.bats b/tests/unit/config.bats index e9bfb1fce..af758b96a 100644 --- a/tests/unit/config.bats +++ b/tests/unit/config.bats @@ -506,7 +506,7 @@ teardown() { run /bin/bash -c "dokku --app $TEST_APP config:show" echo "output: $output" echo "status: $status" - assert_output "=====> $TEST_APP env vars"$'\nBKEY: true\naKey: true\nbKey: true\nzKey: true' + assert_output "=====> $TEST_APP env vars"$'\nBKEY: true\nDOKKU_PROXY_PORT: 80\naKey: true\nbKey: true\nzKey: true' } @test "(config) config:export" { @@ -518,20 +518,20 @@ teardown() { run /bin/bash -c "dokku config:export --format docker-args $TEST_APP" echo "output: $output" echo "status: $status" - assert_output "--env=BKEY='true' --env=aKey='true' --env=bKey='true' --env=zKey='true'" + assert_output "--env=BKEY='true' --env=DOKKU_PROXY_PORT='80' --env=aKey='true' --env=bKey='true' --env=zKey='true'" run /bin/bash -c "dokku config:export --format shell $TEST_APP" echo "output: $output" echo "status: $status" - assert_output "BKEY='true' aKey='true' bKey='true' zKey='true' " + assert_output "BKEY='true' DOKKU_PROXY_PORT='80' aKey='true' bKey='true' zKey='true' " run /bin/bash -c "dokku config:export --format json $TEST_APP" echo "output: $output" echo "status: $status" - assert_output '{"BKEY":"true","aKey":"true","bKey":"true","zKey":"true"}' + assert_output '{"BKEY":"true","DOKKU_PROXY_PORT":"80","aKey":"true","bKey":"true","zKey":"true"}' run /bin/bash -c "dokku config:export --format json-list $TEST_APP" echo "output: $output" echo "status: $status" - assert_output '[{"name":"BKEY","value":"true"},{"name":"aKey","value":"true"},{"name":"bKey","value":"true"},{"name":"zKey","value":"true"}]' + assert_output '[{"name":"BKEY","value":"true"},{"name":"DOKKU_PROXY_PORT","value":"80"},{"name":"aKey","value":"true"},{"name":"bKey","value":"true"},{"name":"zKey","value":"true"}]' } diff --git a/tests/unit/nginx-vhosts_13.bats b/tests/unit/nginx-vhosts_13.bats index 049e1f46c..37845e6fc 100644 --- a/tests/unit/nginx-vhosts_13.bats +++ b/tests/unit/nginx-vhosts_13.bats @@ -36,6 +36,7 @@ teardown() { echo "output: $output" echo "status: $status" assert_success + sleep 2 assert_http_localhost_response "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/" "" "502" } From 2745e4605479f27c990de89eee19a5fe8862f7cd Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 25 Apr 2026 00:55:55 -0400 Subject: [PATCH 8/8] fix(tests): expect 502 for cloned app without deploy The apps:clone --skip-deploy test expected 404 for the cloned app's domain. With the new empty nginx config, undeployed apps now correctly return 502 Bad Gateway instead of falling through to nginx's default. --- tests/unit/apps_2.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/apps_2.bats b/tests/unit/apps_2.bats index a176cece0..661bc9822 100644 --- a/tests/unit/apps_2.bats +++ b/tests/unit/apps_2.bats @@ -166,7 +166,7 @@ teardown() { assert_success assert_output "http://$TEST_APP.$DOKKU_DOMAIN" assert_http_localhost_response "http" "$TEST_APP.$DOKKU_DOMAIN" "80" "/hello" - assert_http_localhost_response "http" "great-test-name.dokku.me" "80" "/hello" "" "404" + assert_http_localhost_response "http" "great-test-name.dokku.me" "80" "/hello" "" "502" run /bin/bash -c "dokku --force apps:destroy great-test-name" echo "output: $output" echo "status: $status"