feat: ship default catch-all site on fresh apt install

Fresh apt installs now drop a catch-all server block at `/etc/nginx/conf.d/00-default-vhost.conf` that uses `ssl_reject_handshake on` and `return 444` to drop requests with unknown Host headers. Conflicting upstream nginx default vhosts are renamed to `*.dokku-disabled` rather than deleted, preserving any local edits. The new `dokku/install_default_site` debconf flag opts out of the install. Upgrades leave existing nginx config untouched.
This commit is contained in:
Jose Diaz-Gonzalez
2026-04-27 02:55:40 -04:00
parent a4f26a356a
commit d7c88ae298
7 changed files with 246 additions and 32 deletions

1
debian/config vendored
View File

@@ -14,5 +14,6 @@ db_input "high" "dokku/hostname" || true
db_input "high" "dokku/vhost_enable" || true
if [ "$ACTION" != "reconfigure" ]; then
db_input "high" "dokku/key_file" || true
db_input "high" "dokku/install_default_site" || true
fi
db_go || true

35
debian/postinst vendored
View File

@@ -135,6 +135,38 @@ setup-sshcommand() {
fi
}
setup-default-site() {
db_get "dokku/install_default_site"
if [ "$RET" != "true" ]; then
return
fi
local default_vhost_target="/etc/nginx/conf.d/00-default-vhost.conf"
local default_vhost_source="${DOKKU_LIB_ROOT}/core-plugins/available/nginx-vhosts/templates/default-site.conf"
if [ -e "$default_vhost_target" ]; then
return
fi
if [ ! -f "$default_vhost_source" ]; then
return
fi
for path in /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default /etc/nginx/conf.d/default.conf; do
if [ -e "$path" ] && [ ! -e "${path}.dokku-disabled" ]; then
mv -n "$path" "${path}.dokku-disabled"
echo "Disabled $path (renamed to ${path}.dokku-disabled)" 1>&2
fi
done
echo "Installing nginx default catch-all site at $default_vhost_target"
install -m 0644 -o root -g root "$default_vhost_source" "$default_vhost_target"
if command -v dokku &>/dev/null; then
dokku nginx:reload || true
fi
}
dpkg-handling() {
if [ -f "${DOKKU_ROOT}/VHOST" ]; then
echo "VHOST file detected, skipping modification"
@@ -170,6 +202,9 @@ case "$1" in
setup-plugins
setup-sshcommand
setup-docker-live-restore
if [ -z "$2" ]; then
setup-default-site
fi
;;
*)

5
debian/templates vendored
View File

@@ -22,3 +22,8 @@ Template: dokku/nginx_enable
Description: Enable nginx-vhosts plugin?
Type: boolean
Default: true
Template: dokku/install_default_site
Description: Install a catch-all nginx default site on fresh install?
Type: boolean
Default: true

View File

@@ -16,3 +16,9 @@
```
- The path on disk to both the global `ENV` file and app `ENV` files have been moved. Users should reference environment variables via the provided plugin triggers rather than directly sourcing the ENV files. Existing ENV files are left untouched and will be removed on the subsequent Dokku install.
- During a fresh apt install, the upstream nginx default vhost files (`/etc/nginx/sites-enabled/default`, `/etc/nginx/sites-available/default`, and `/etc/nginx/conf.d/default.conf`) are renamed to `${path}.dokku-disabled` (not deleted) to avoid a `duplicate default server for 0.0.0.0:80` error. Operators with local customizations can recover them by inspecting the `.dokku-disabled` siblings. Upgrade-in-place installs do not touch any existing nginx files.
- Fresh apt installs now ship a catch-all default site at `/etc/nginx/conf.d/00-default-vhost.conf` that rejects requests with unknown Host headers using `ssl_reject_handshake on` (HTTPS) and `return 444` (HTTP). This replaces the manual workaround previously documented in the nginx docs. The behavior can be opted out at install time via the `dokku/install_default_site` debconf prompt. See the [Default site documentation](/docs/networking/proxies/nginx.md#default-site).
### TLS handshake behavior change
With the new catch-all installed, an HTTPS request to a hostname that matches a configured dokku app but where the app has no TLS certificate configured will have its TLS handshake rejected by the catch-all (via `ssl_reject_handshake on`). Previously, nginx fell through to the lexicographically first port-443 server block and presented that block's certificate, producing a cert-mismatch error on the client. The new behavior is a correctness improvement, but operators who deliberately relied on the old fall-through certificate (for monitoring probes, for example) need to either configure a certificate for the target app or remove the catch-all on that host. Existing apps that already have certificates configured are unaffected: nginx selects the right server block via SNI before TLS completion, so the catch-all is never consulted for legitimate requests.

View File

@@ -5,6 +5,7 @@ Dokku uses nginx as its server for routing requests to specific applications. By
```
nginx:access-logs <app> [-t] # Show the nginx access logs for an application (-t follows)
nginx:error-logs <app> [-t] # Show the nginx error logs for an application (-t follows)
nginx:reload # Reloads the nginx server config
nginx:report [<app>] [<flag>|--format json] # Displays a nginx report for one or more apps
nginx:set <app> <property> (<value>) # Set or clear an nginx property for an app
nginx:show-config <app> # Display app nginx config
@@ -64,6 +65,19 @@ The nginx server can be stopped via `nginx:stop`.
dokku nginx:stop
```
### Reloading nginx
> [!IMPORTANT]
> New as of 0.38.0
The nginx server can be reloaded via `nginx:reload`. This validates the current nginx configuration and, if valid, signals nginx to reload without restarting the running process. This is the preferred command after editing files in `/etc/nginx/conf.d/` directly.
```shell
dokku nginx:reload
```
If the configuration is invalid, the command exits non-zero and prints the validation error without reloading.
### Checking access logs
> [!NOTE]
@@ -168,52 +182,91 @@ These are provided as an alternative to the generic Nginx error page, are shared
### Default site
By default, Dokku will route any received request with an unknown HOST header value to the lexicographically first site in the nginx config stack. This means that accessing the dokku server via its IP address or a bogus domain name may return a seemingly random website.
> [!IMPORTANT]
> New as of 0.38.0
> [!WARNING]
> Some versions of Nginx may create a default site when installed. This site is simply a static page which says "Welcome to Nginx", and if this default site is enabled, Nginx will not route any requests with an unknown HOST header to Dokku. If you want Dokku to receive all requests, run the following commands:
>
> ```
> rm /etc/nginx/sites-enabled/default
> dokku nginx:stop
> dokku nginx:start
> ```
On fresh apt installs, Dokku ships a catch-all default site at `/etc/nginx/conf.d/00-default-vhost.conf` that rejects requests whose Host header (or TLS SNI) does not match any deployed app. The catch-all uses [`ssl_reject_handshake on`](https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_reject_handshake) for HTTPS and [`return 444`](https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#return) for HTTP. Both close the connection without sending a response.
If services should only be accessed via their domain name, you may want to disable the default site by adding the following configuration to the global nginx configuration.
Create the file at `/etc/nginx/conf.d/00-default-vhost.conf`:
The shipped file looks like:
```nginx
server {
listen 80 default_server;
listen [::]:80 default_server;
# If services hosted by dokku are available via HTTPS, it is recommended
# to also uncomment the following section.
#
# Please note that in order to let this work, you need an SSL certificate. However
# it does not need to be valid. Users of Debian-based distributions can install the
# `ssl-cert` package with `sudo apt install ssl-cert` to automatically generate
# a self-signed certificate that is stored at `/etc/ssl/certs/ssl-cert-snakeoil.pem`.
#
#listen 443 ssl;
#listen [::]:443 ssl;
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
access_log off;
access_log off;
ssl_reject_handshake on;
return 444;
}
```
Make sure to reload nginx after creating this file by running `systemctl reload nginx.service`.
The `00-` prefix forces nginx to load this file before `/etc/nginx/conf.d/dokku.conf`, so its `default_server` markers establish the default for each port before any per-app server blocks are loaded.
This will catch all unknown HOST header values and close the connection without responding. You can replace the `return 444;` with `return 410;` which will cause nginx to respond with an error page.
#### TLS handshake behavior
The configuration file must be loaded before `/etc/nginx/conf.d/dokku.conf`, so it can not be arranged as a vhost in `/etc/nginx/sites-enabled` that is only processed afterwards.
The catch-all does not affect TLS handshakes for legitimate apps. nginx selects the matching server block via SNI before completing the handshake; only requests that have no matching app fall through to the catch-all.
Alternatively, you may push an app to your Dokku host with a name like "00-default". As long as it lists first in `ls /home/dokku/*/nginx.conf | head`, it will be used as the default nginx vhost.
| Request | Result |
| --- | --- |
| HTTPS to a configured app's hostname (with matching SNI) and the app has a cert | Handshake completes with the app's cert. Catch-all not consulted. |
| HTTPS to a configured app's hostname when the app has no cert configured | Handshake rejected by the catch-all. (Previously: nginx fell through to the lexicographically first port-443 server block and presented its cert, producing a confusing cert-mismatch error.) |
| HTTPS to the server's IP with no SNI, or with an SNI matching no app | Handshake rejected. |
| HTTP to a configured app's hostname | Routed normally to that app. Catch-all not consulted. |
| HTTP to a hostname matching no app | Catch-all `return 444`. |
#### Conflicting upstream nginx default vhost
A fresh nginx install ships its own default vhost that also claims port 80 with `default_server`, which would cause `nginx -t` to fail with `a duplicate default server for 0.0.0.0:80` once the dokku catch-all is in place. To avoid this, the dokku postinst renames any of the following files in place to `${path}.dokku-disabled` instead of deleting them, preserving any local customizations:
- `/etc/nginx/sites-enabled/default`
- `/etc/nginx/sites-available/default`
- `/etc/nginx/conf.d/default.conf`
To recover an original file, inspect the `.dokku-disabled` sibling.
#### Disabling the catch-all on first install
Select "No" at the `dokku/install_default_site` debconf prompt, or pre-seed the answer before installing:
```shell
echo 'dokku dokku/install_default_site boolean false' | debconf-set-selections
```
#### Customizing or removing the catch-all after install
To customize the reject behavior, edit `/etc/nginx/conf.d/00-default-vhost.conf` and reload:
```shell
dokku nginx:reload
```
For example, replace `return 444;` with `return 410;` to respond with an HTTP 410 Gone error page instead of dropping the connection.
To disable the catch-all without uninstalling dokku:
```shell
mv /etc/nginx/conf.d/00-default-vhost.conf{,.disabled}
dokku nginx:reload
```
#### Installing the catch-all on upgraded or non-apt installs
Existing dokku installs upgraded from earlier versions do not get the catch-all installed automatically; the upgrade leaves their nginx configuration untouched. To install it manually, copy the shipped template into place and reload nginx:
```shell
cp /var/lib/dokku/core-plugins/available/nginx-vhosts/templates/default-site.conf /etc/nginx/conf.d/00-default-vhost.conf
dokku nginx:reload
```
If the upstream nginx default vhost is also active, disable it first (e.g. `mv /etc/nginx/sites-enabled/default{,.disabled}`) to avoid a duplicate-default-server error.
#### App-name based alternative
As an alternative to the catch-all file, you may push an app to your Dokku host with a name like `00-default`. As long as it lists first in `ls /home/dokku/*/nginx.conf | head`, it will be used as the default nginx vhost.
### Customizing the nginx configuration

View File

@@ -0,0 +1,15 @@
# Catch-all server block for requests with unknown Host headers.
# Installed on fresh apt installs of dokku; safe to edit.
# See /etc/nginx/conf.d/dokku.conf for the per-app server blocks.
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
access_log off;
ssl_reject_handshake on;
return 444;
}

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env bats
# Tests for the nginx:reload command and the catch-all default site shipped
# at /etc/nginx/conf.d/00-default-vhost.conf. Postinst-driven behavior
# (debconf flag, fresh-install detection, sites-enabled/default rename) is
# not exercised here because the bats harness does not reinstall the apt
# package; the rename logic is exercised manually by simulating the
# conflicting upstream vhost.
load test_helper
NGINX_DEFAULT_VHOST_PATH="/etc/nginx/conf.d/00-default-vhost.conf"
NGINX_DEFAULT_VHOST_SOURCE="/var/lib/dokku/core-plugins/available/nginx-vhosts/templates/default-site.conf"
setup() {
global_setup
[[ -f "$DOKKU_ROOT/VHOST" ]] && cp -fp "$DOKKU_ROOT/VHOST" "$DOKKU_ROOT/VHOST.bak"
[[ -f "$NGINX_DEFAULT_VHOST_PATH" ]] && cp -fp "$NGINX_DEFAULT_VHOST_PATH" "${NGINX_DEFAULT_VHOST_PATH}.bats-bak"
rm -f "$NGINX_DEFAULT_VHOST_PATH"
}
teardown() {
rm -f "$NGINX_DEFAULT_VHOST_PATH"
if [[ -f "${NGINX_DEFAULT_VHOST_PATH}.bats-bak" ]]; then
mv "${NGINX_DEFAULT_VHOST_PATH}.bats-bak" "$NGINX_DEFAULT_VHOST_PATH"
fi
rm -f /etc/nginx/sites-enabled/default.bats-stub
rm -f /etc/nginx/sites-enabled/default.bats-stub.dokku-disabled
[[ -f "$DOKKU_ROOT/VHOST.bak" ]] && mv "$DOKKU_ROOT/VHOST.bak" "$DOKKU_ROOT/VHOST" && chown dokku:dokku "$DOKKU_ROOT/VHOST"
if sudo nginx -t &>/dev/null; then
sudo systemctl reload nginx || true
fi
global_teardown
}
@test "(nginx-vhosts:reload) reloads with valid config" {
run /bin/bash -c "dokku nginx:reload"
echo "output: $output"
echo "status: $status"
assert_success
}
@test "(nginx-vhosts:reload) fails with invalid config without restarting" {
local invalid_conf="/etc/nginx/conf.d/dokku-bats-invalid.conf"
echo "this is not valid nginx config" | sudo tee "$invalid_conf" >/dev/null
run /bin/bash -c "dokku nginx:reload"
echo "output: $output"
echo "status: $status"
assert_failure
sudo rm -f "$invalid_conf"
}
@test "(nginx-vhosts) [default-site] shipped template installs and validates" {
[[ -f "$NGINX_DEFAULT_VHOST_SOURCE" ]] || skip "default-site template not installed; run 'sudo make copyfiles'"
sudo install -m 0644 -o root -g root "$NGINX_DEFAULT_VHOST_SOURCE" "$NGINX_DEFAULT_VHOST_PATH"
run /bin/bash -c "sudo grep -F 'ssl_reject_handshake on' '$NGINX_DEFAULT_VHOST_PATH'"
assert_success
run /bin/bash -c "sudo grep -F 'return 444' '$NGINX_DEFAULT_VHOST_PATH'"
assert_success
run /bin/bash -c "sudo grep -F 'default_server' '$NGINX_DEFAULT_VHOST_PATH'"
assert_success
run /bin/bash -c "sudo nginx -t"
echo "output: $output"
echo "status: $status"
assert_success
}
@test "(nginx-vhosts) [default-site] coexists with stock sites-enabled/default" {
[[ -f "$NGINX_DEFAULT_VHOST_SOURCE" ]] || skip "default-site template not installed; run 'sudo make copyfiles'"
sudo mkdir -p /etc/nginx/sites-enabled
sudo tee /etc/nginx/sites-enabled/default.bats-stub >/dev/null <<'STOCK'
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 200 "stock default";
}
STOCK
sudo install -m 0644 -o root -g root "$NGINX_DEFAULT_VHOST_SOURCE" "$NGINX_DEFAULT_VHOST_PATH"
run /bin/bash -c "sudo nginx -t"
echo "output: $output"
echo "status: $status"
assert_failure
sudo mv /etc/nginx/sites-enabled/default.bats-stub /etc/nginx/sites-enabled/default.bats-stub.dokku-disabled
run /bin/bash -c "sudo nginx -t"
echo "output: $output"
echo "status: $status"
assert_success
}