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:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
21
plugins/nginx-vhosts/post-create
Normal file
21
plugins/nginx-vhosts/post-create
Normal 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 "$@"
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 " " }}
|
||||
|
||||
98
tests/unit/nginx-vhosts_13.bats
Normal file
98
tests/unit/nginx-vhosts_13.bats
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user