Merge pull request #8595 from dokku/8313-coredns-docker

Resolve nginx upstreams via coredns-docker
This commit is contained in:
Jose Diaz-Gonzalez
2026-11-07 23:41:44 -05:00
committed by GitHub
19 changed files with 691 additions and 11 deletions

View File

@@ -1,5 +1,13 @@
{
"dependencies": [
{
"name": "coredns-docker",
"version": "0.8.0",
"urls": {
"amd64": "https://github.com/dokku/coredns-docker/releases/download/0.8.0/coredns-docker-linux-amd64",
"arm64": "https://github.com/dokku/coredns-docker/releases/download/0.8.0/coredns-docker-linux-arm64"
}
},
{
"name": "docker-container-healthchecker",
"version": "0.16.1",

2
debian/control vendored
View File

@@ -3,7 +3,7 @@ Version: 0.38.34
Section: web
Priority: optional
Architecture: amd64
Depends: apache2-utils, locales, git, cpio, cron | cron-daemon, curl, logrotate, man-db, netcat, sshcommand, docker-engine-cs (>= 19.03.0) | docker-engine (>= 19.03.0) | docker-io (>= 19.03.0) | docker.io (>= 19.03.0) | docker-ce (>= 19.03.0) | docker-ee (>= 19.03.0) | moby-engine, docker-compose-plugin | moby-compose | docker-compose, docker-buildx-plugin | moby-buildx | docker-buildx, docker-container-healthchecker, docker-image-labeler, lambda-builder, net-tools, netrc, parallel, procfile-util, rsync, dos2unix, unzip, util-linux
Depends: apache2-utils, locales, git, cpio, cron | cron-daemon, curl, logrotate, man-db, netcat, sshcommand, docker-engine-cs (>= 19.03.0) | docker-engine (>= 19.03.0) | docker-io (>= 19.03.0) | docker.io (>= 19.03.0) | docker-ce (>= 19.03.0) | docker-ee (>= 19.03.0) | moby-engine, docker-compose-plugin | moby-compose | docker-compose, docker-buildx-plugin | moby-buildx | docker-buildx, coredns-docker, docker-container-healthchecker, docker-image-labeler, lambda-builder, net-tools, netrc, parallel, procfile-util, rsync, dos2unix, unzip, util-linux
Recommends: herokuish, bash-completion, dokku-update, dokku-event-listener
Pre-Depends: gliderlabs-sigil, jq, nginx (>= 1.8.0) | openresty, bind9-dnsutils, debconf, plugn, sudo, python3, rsyslog
Maintainer: Jose Diaz-Gonzalez <dokku@josediazgonzalez.com>

View File

@@ -0,0 +1,7 @@
#!/bin/bash
exec >>/var/log/services/coredns-docker
exec 2>&1
echo "Running coredns-docker"
exec /usr/bin/coredns-docker -conf /etc/coredns/Corefile

View File

@@ -7,6 +7,23 @@
- The `--buildpacks-stack`, `--buildpacks-global-stack`, and `--buildpacks-computed-stack` report flags (and the corresponding `stack`, `global-stack`, `computed-stack` JSON keys) have been removed from `buildpacks:report`. Use `builder-herokuish:report` and `builder-pack:report` instead. The `buildpacks:report` command continues to report the buildpacks `list`.
- The internal `buildpack-stack-name` plugin trigger has been removed. Each builder now reads its own `stack` property directly during the build.
- Changing a builder's `stack` now clears that builder's build cache so the next deploy starts fresh. For herokuish this removes the `cache-<app>` Docker volume; for pack the next `pack build` runs with `--clear-cache`.
- The `dokku` Debian package now declares `coredns-docker (>= 0.8.0)` as a hard `Depends:`. `apt install dokku` and `apt upgrade dokku` pull it in automatically. The deb ships a default `/etc/coredns/Corefile` that binds `127.0.0.1:1053`, serves the `docker.` zone, and synthesizes `<app>.<process-type>.docker` and `<app>.docker` `A` records from the `com.dokku.app-name` and `com.dokku.process-type` labels Dokku already stamps on every deployed container. The Docker image of Dokku ships coredns-docker preinstalled and runs it under runit on the same port.
- The bundled `nginx.conf.sigil` can now use an nginx `resolver` directive and a variable-based `proxy_pass` against `<app>.<process-type>.docker` instead of pinning the upstream to a literal IP. This addresses the long-standing post-reboot and post-`apt-upgrade-docker` stale-vhost bug ([#8313](https://github.com/dokku/dokku/issues/8313)) - the rendered upstream re-resolves at request time via coredns-docker, so a host reboot or single-container restart no longer leaves nginx routing to a stale IP. This behavior is opt-in, and upstreams remain IP-based unless `dns-resolver` is set:
```shell
dokku nginx:set --global dns-resolver 127.0.0.1:1053
dokku proxy:build-config --all
```
See the [nginx documentation](/docs/networking/proxies/nginx.md#upstream-resolution-via-coredns-docker) for details.
- Three new nginx properties control the resolver path:
- `dns-resolver` (default: none) - the address nginx talks to. Resolver-based upstreams are only rendered when this is set.
- `dns-resolver-timeout` (default: `5s`) - how long nginx waits for a DNS response before giving up and returning 502. The short default keeps clients from waiting on nginx's built-in 30s timeout when coredns-docker is unreachable.
- `dns-zone` (default: `docker`) - the suffix appended to `<app>.<process-type>` when constructing the upstream hostname.
All three can be set per app or globally via `dokku nginx:set`.
- Vendored `nginx.conf.sigil` templates are unaffected by the change. The existing `DOKKU_APP_<PROC>_LISTENERS` variable continues to be populated with the IP-based listener list exactly as before, so templates that only reference it continue to render IP-based upstreams regardless of the new properties. To opt a vendored template into the resolver path, switch its `proxy_pass` line to use the new `DOKKU_APP_<PROC>_LISTENER_HOST`, `NGINX_DNS_RESOLVER`, `NGINX_DNS_RESOLVER_TIMEOUT`, and `NGINX_DNS_ZONE` sigil variables - see the [nginx.conf.sigil reference](/docs/appendices/file-formats/nginx-conf-sigil.md#upstream-rendering-ip-based-vs-dns-based) for the exact snippet.
### Stack property migrated to builder plugins

View File

@@ -4,7 +4,10 @@ The `nginx.conf.sigil` file is used to configure the nginx server for an applica
## Validation
A custom `nginx.conf.sigil` is pre-validated at the start of every deploy, immediately after it is extracted from the source tree and before the build phase runs. Pre-validation renders the template via sigil with the same parameters used at deploy time, wraps the rendered config in a minimal `events`/`http` scaffold, and runs `nginx -t` against the result. The deploy is aborted if either the sigil render or the `nginx -t` check fails, so build work is not wasted on a syntactically invalid template. When no app listeners exist yet (typical for first deploys), pre-validation injects a placeholder `127.0.0.1:5000` listener for `DOKKU_APP_WEB_LISTENERS` so that the rendered upstream block has a static server entry and `nginx -t` does not bail out on "host not found in upstream".
A custom `nginx.conf.sigil` is pre-validated at the start of every deploy, immediately after it is extracted from the source tree and before the build phase runs. Pre-validation renders the template via sigil with the same parameters used at deploy time, wraps the rendered config in a minimal `events`/`http` scaffold, and runs `nginx -t` against the result. The deploy is aborted if either the sigil render or the `nginx -t` check fails, so build work is not wasted on a syntactically invalid template. When no app listeners exist yet (typical for first deploys), pre-validation injects placeholders so the rendered config parses cleanly under `nginx -t`:
- `DOKKU_APP_WEB_LISTENERS` is set to `127.0.0.1:5000` so the legacy `upstream` block has a static server entry.
- `DOKKU_APP_WEB_LISTENER_HOST` is set to `127.0.0.1` so the resolver-based `set $dokku_upstream "...";` line renders to a syntactically valid value.
Pre-validation is skipped when the proxy type is not `nginx` or when `disable-custom-config` is set to `true` for the app.
@@ -68,3 +71,50 @@ listen {{ if $.NGINX_BIND_ADDRESS_IP4 }}{{ $.NGINX_BIND_ADDRESS_IP4 }}:{{en
```
Dokku logs a deprecation warning during deploys when a custom template still uses the `listen ... http2` form, so the offending template can be located via the deploy output.
## Upstream Rendering: IP-based vs DNS-based
> [!IMPORTANT]
> New as of 0.39.0
The bundled `nginx.conf.sigil` supports two rendering paths for the upstream, and both sets of sigil variables are populated on every render. Vendored templates can use either path independently of the global `dns-resolver` setting.
### IP-based upstream (legacy path)
The classic form uses `DOKKU_APP_<PROC>_LISTENERS`: a space-separated `<ip>:<port>` list, one entry per scaled instance of the process type. The IPs come from `/home/dokku/<APP>/IP.<proc>.<index>` and the ports from `/home/dokku/<APP>/PORT.<proc>.<index>`. A typical use is an `upstream` block with one `server <ip>:<port>;` per instance:
```nginx
upstream {{ $.APP }}-{{ $upstream_port }} {
{{ range $listeners := $.DOKKU_APP_WEB_LISTENERS | split " " }}
{{ $listener_list := $listeners | split ":" }}
{{ $listener_ip := index $listener_list 0 }}
server {{ $listener_ip }}:{{ $upstream_port }};{{ end }}
}
```
This path renders the IP that Dokku saw when the config was last built. After a host reboot, an `apt upgrade docker` without live-restore, or a single container restart, those IPs can become stale until `dokku proxy:build-config` runs again. The DNS-based path below addresses that staleness.
### DNS-based upstream (resolver path)
The new variables (`DOKKU_APP_<PROC>_LISTENER_HOST` + `NGINX_DNS_RESOLVER` + `NGINX_DNS_RESOLVER_TIMEOUT` + `NGINX_DNS_ZONE`) describe a single hostname per process type of the form `<app>.<proctype>.<dns-zone>` (for example, `myapp.web.docker`). The hostname is served by [coredns-docker](https://github.com/dokku/coredns-docker) (>= `0.8.0`), which watches Docker events and collapses every running instance of the process type onto one multi-A record set. Combined with an nginx `resolver` directive and a variable-based `proxy_pass`, the upstream re-resolves at request time honoring DNS TTL - which gives runtime IP refresh on container restarts and DNS round-robin across scaled instances.
The bundled template emits this snippet:
```nginx
{{ if $.NGINX_DNS_RESOLVER }}
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 }}
proxy_pass http://{{ $.APP }}-{{ $upstream_port }};
{{ end }}
```
The `$dokku_upstream` variable name is intentionally namespaced (rather than `$upstream`) so vendored templates that already define `$upstream` will not collide if they adopt the resolver branch.
`NGINX_DNS_RESOLVER` is empty by default, `NGINX_DNS_RESOLVER_TIMEOUT` defaults to `5s`, and `NGINX_DNS_ZONE` defaults to `docker`; all three are configurable via `dokku nginx:set --global dns-resolver ...`, `dokku nginx:set --global dns-resolver-timeout ...`, and `dokku nginx:set --global dns-zone ...`. The bundled template only renders the resolver branch when `NGINX_DNS_RESOLVER` is set, for example to `127.0.0.1:1053`. Unsetting `dns-resolver` switches the bundled template back to the IP-based upstream. The short default `dns-resolver-timeout` keeps clients from waiting on nginx's built-in 30s timeout when coredns-docker is unreachable; nginx returns 502 after the configured interval instead.
### Choosing between the two
The IP-based path is the default for the bundled template, and the DNS-based path is used once `dns-resolver` is set. Vendored templates that have not been updated continue to use `DOKKU_APP_<PROC>_LISTENERS` and are unaffected by the `dns-resolver` setting. The `DOKKU_APP_<PROC>_LISTENER_HOST` and `NGINX_DNS_ZONE` variables are populated regardless of whether `dns-resolver` is set, so a vendored template can adopt the resolver path on its own schedule by switching its rendering snippet, even if other apps on the host are still using IP-based upstreams.

View File

@@ -88,6 +88,7 @@ Dokku is run in the following configuration:
- Image build cache is set to the data dir + `/home/dokku`.
- The docker socket is mounted into container.
- `DOKKU_HOST_ROOT` and `DOKKU_LIB_HOST_ROOT` tell Dokku how the host Docker daemon sees those same trees when creating bind mounts (app repos and plugin data such as named storage).
- `coredns-docker` (a CoreDNS plugin that serves DNS records for sibling Docker containers) is installed transitively via the dokku deb's `Depends:`, runs under runit on `127.0.0.1:1053` inside the container, and is what the `nginx-vhosts` `dns-resolver` property can point at to enable `<app>.<process-type>.docker` upstream resolution - see the [nginx proxy doc](/docs/networking/proxies/nginx.md#upstream-resolution-via-coredns-docker) for the operator-facing behavior.
Application repositories, plugin config, as well as plugin data are persisted to disk within the specified host directory for `/var/lib/dokku`. `DOKKU_LIB_HOST_ROOT` must point at that host path's `var/lib/dokku` subtree so Docker `-v` mounts resolve under the data volume rather than under the host's own `/var/lib/dokku`.

View File

@@ -28,6 +28,42 @@ 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.
### Upstream Resolution via coredns-docker
> [!IMPORTANT]
> New as of 0.39.0
Dokku can resolve nginx upstreams through [coredns-docker](https://github.com/dokku/coredns-docker), a CoreDNS plugin that watches the Docker daemon and serves fresh `A` records keyed off container labels. When enabled, the nginx `resolver` directive points at coredns-docker's local socket (`127.0.0.1:1053`) and the rendered upstream is a variable-based `proxy_pass`, which forces nginx to re-resolve at request time honoring DNS TTL. This is how nginx avoids serving the wrong vhost after a host reboot, after `apt upgrade docker`, or after a single container restarts mid-runtime: the on-disk nginx config no longer pins a stale IP. This behavior is opt-in, and nginx uses IP-based `upstream` blocks by default.
`coredns-docker` (>= `0.8.0`) is installed automatically as a transitive `Depends:` of the `dokku` Debian package, so `apt install dokku` and `apt upgrade dokku` pull it in without operator action. The shipped `/etc/coredns/Corefile` already binds `127.0.0.1:1053`, serves the `docker.` zone, and synthesizes `<app>.<process-type>.docker` and `<app>.docker` `A` records from Dokku's existing `com.dokku.app-name` and `com.dokku.process-type` container labels. Multiple containers for the same process type collapse onto one multi-A record set, so scaled apps load-balance across instances via DNS without any per-container configuration. The bundled Docker image of Dokku ships coredns-docker preinstalled and runs it under runit on the same port.
No `systemd-resolved` or `/etc/resolv.conf` change is required - nginx talks to `coredns-docker` directly via the `resolver` directive in each app's `nginx.conf`.
To enable resolver-based upstreams:
```shell
dokku nginx:set --global dns-resolver 127.0.0.1:1053
dokku proxy:build-config --all
```
To revert to the IP-based upstream block, unset the property and rebuild the config:
```shell
dokku nginx:set --global dns-resolver
dokku proxy:build-config --all
```
The behavior is controlled by three nginx properties, each settable per app or globally:
- `dns-resolver` (default: none) - the resolver address nginx talks to. Resolver-based upstreams are only rendered when this is set.
- `dns-resolver-timeout` (default: `5s`) - how long nginx waits for a DNS response before giving up and returning 502. The default is short so requests fail fast when coredns-docker is unreachable rather than hanging on the nginx built-in default (30s).
- `dns-zone` (default: `docker`) - the suffix appended to `<app>.<process-type>` when constructing the upstream hostname.
> [!NOTE]
> When `dns-resolver` is set, no `upstream` block is rendered for the `web` process, so `proxy-keepalive` has no effect on it. Upstreams for [proxy routes](/docs/networking/proxy-management.md#path-based-routing-to-non-web-processes) to other process types remain IP-based.
See [the `nginx.conf.sigil` reference](/docs/appendices/file-formats/nginx-conf-sigil.md) for the sigil variables that drive the resolver-based render, and how vendored templates can adopt or skip it.
### Nginx Configuration for Undeployed Apps
> [!IMPORTANT]
@@ -643,6 +679,9 @@ All nginx-vhosts properties are settable at both the app and global scope.
| `client-header-timeout` | app + global | `60s` | `--nginx-client-header-timeout`, `--nginx-global-client-header-timeout`, `--nginx-computed-client-header-timeout` | Time allowed to read the request header from the client |
| `client-max-body-size` | app + global | `1m` | `--nginx-client-max-body-size`, `--nginx-global-client-max-body-size`, `--nginx-computed-client-max-body-size` | Maximum allowed request body size |
| `disable-custom-config` | app + global | `false` | `--nginx-disable-custom-config`, `--nginx-global-disable-custom-config`, `--nginx-computed-disable-custom-config` | When `true`, ignores app-supplied `nginx.conf.d/*.conf` snippets |
| `dns-resolver` | app + global | none | `--nginx-dns-resolver`, `--nginx-global-dns-resolver`, `--nginx-computed-dns-resolver` | Resolver address used for DNS-based upstreams. When unset, upstreams are IP-based |
| `dns-resolver-timeout` | app + global | `5s` | `--nginx-dns-resolver-timeout`, `--nginx-global-dns-resolver-timeout`, `--nginx-computed-dns-resolver-timeout` | Time nginx waits for a DNS response from the resolver |
| `dns-zone` | app + global | `docker` | `--nginx-dns-zone`, `--nginx-global-dns-zone`, `--nginx-computed-dns-zone` | Zone appended to `<app>.<process-type>` for DNS-based upstream hostnames |
| `error-log-path` | app + global | _`{nginx-log-root}/{app}-error.log`_ | `--nginx-error-log-path`, `--nginx-global-error-log-path`, `--nginx-computed-error-log-path` | Path inside the nginx container where error logs are written |
| `hsts` | app + global | `true` | `--nginx-hsts`, `--nginx-global-hsts`, `--nginx-computed-hsts` | When `true`, emits a `Strict-Transport-Security` header on HTTPS responses |
| `hsts-include-subdomains` | app + global | `true` | `--nginx-hsts-include-subdomains`, `--nginx-global-hsts-include-subdomains`, `--nginx-computed-hsts-include-subdomains` | Adds the `includeSubDomains` directive to the HSTS header |

View File

@@ -324,6 +324,9 @@ 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")"
local PROXY_X_FORWARDED_PORT="$(fn-nginx-computed-x-forwarded-port-value "$APP")"
@@ -354,6 +357,9 @@ fn-nginx-vhosts-render-template() {
NGINX_ERROR_LOG_PATH="$NGINX_ERROR_LOG_PATH"
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"
GRPC_SUPPORTED="$GRPC_SUPPORTED"
@@ -386,21 +392,29 @@ fn-nginx-vhosts-render-template() {
PROXY_X_FORWARDED_SSL="$PROXY_X_FORWARDED_SSL")
if [[ "$IS_DEPLOYED_WITH_LISTENERS" == "true" ]]; then
local line PROC_TYPE LISTENERS UPP_PROC_TYPE
local line PROC_TYPE LISTENERS UPP_PROC_TYPE LISTENER_HOST
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")
LISTENER_HOST=""
if [[ -n "$NGINX_DNS_ZONE" ]]; then
LISTENER_HOST="${APP}.${PROC_TYPE}.${NGINX_DNS_ZONE}"
fi
SIGIL_PARAMS+=("DOKKU_APP_${UPP_PROC_TYPE}_LISTENER_HOST=$LISTENER_HOST")
done < <(plugn trigger ps-current-scale "$APP")
elif [[ "$PRE_VALIDATE" == "true" ]]; then
# During pre-validation no listeners exist yet; supply a placeholder so
# any upstream block in the template renders with a static server entry
# and nginx -t does not bail out on "host not found in upstream".
# During pre-validation no listeners exist yet; supply placeholders so
# both the legacy IP-based upstream block and the DNS-based resolver
# branch render syntactically valid nginx config that nginx -t can
# parse (no "host not found in upstream" and no empty set $var).
SIGIL_PARAMS+=(DOKKU_APP_WEB_LISTENERS="127.0.0.1:5000")
SIGIL_PARAMS+=(DOKKU_APP_WEB_LISTENER_HOST="127.0.0.1")
else
SIGIL_PARAMS+=(DOKKU_APP_WEB_LISTENERS="")
SIGIL_PARAMS+=(DOKKU_APP_WEB_LISTENER_HOST="")
fi
sigil "${SIGIL_PARAMS[@]}" | cat -s >"$OUTPUT_PATH"

View File

@@ -172,6 +172,69 @@ fn-nginx-global-disable-custom-config() {
fn-get-property --app "$APP" --global "disable-custom-config"
}
fn-nginx-dns-resolver() {
declare desc="get the configured dns-resolver value"
declare APP="$1"
fn-get-property --app "$APP" "dns-resolver"
}
fn-nginx-computed-dns-resolver() {
declare desc="get the computed dns-resolver value"
declare APP="$1"
fn-get-property --app "$APP" --computed "dns-resolver"
}
fn-nginx-global-dns-resolver() {
declare desc="get the global dns-resolver value"
declare APP="$1"
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"
fn-get-property --app "$APP" "dns-zone"
}
fn-nginx-computed-dns-zone() {
declare desc="get the computed dns-zone value"
declare APP="$1"
fn-get-property --app "$APP" --computed "dns-zone"
}
fn-nginx-global-dns-zone() {
declare desc="get the global dns-zone value"
declare APP="$1"
fn-get-property --app "$APP" --global "dns-zone"
}
fn-nginx-error-log-path() {
declare desc="get the configured error log path"
declare APP="$1"

View File

@@ -150,6 +150,57 @@ func AppDisableCustomConfig(appName string) string {
return common.PropertyGet("nginx", appName, "disable-custom-config")
}
func AppDnsResolver(appName string) string {
return common.PropertyGet("nginx", appName, "dns-resolver")
}
func ComputedDnsResolver(appName string) string {
appValue := AppDnsResolver(appName)
if appValue != "" {
return appValue
}
return GlobalDnsResolver()
}
func GlobalDnsResolver() string {
return common.PropertyGet("nginx", "--global", "dns-resolver")
}
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")
}
func ComputedDnsZone(appName string) string {
appValue := AppDnsZone(appName)
if appValue != "" {
return appValue
}
return GlobalDnsZone()
}
func GlobalDnsZone() string {
return common.PropertyGetDefault("nginx", "--global", "dns-zone", "docker")
}
func ComputedDisableCustomConfig(appName string) string {
appValue := AppDisableCustomConfig(appName)
if appValue != "" {

View File

@@ -29,6 +29,9 @@ func nginxReportProps() []nginxReportProp {
{"client-header-timeout", AppClientHeaderTimeout, ComputedClientHeaderTimeout, wrapGlobal(GlobalClientHeaderTimeout)},
{"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)},
{"hsts-max-age", AppHSTSMaxAge, ComputedHSTSMaxAge, wrapGlobal(GlobalHSTSMaxAge)},

View File

@@ -51,6 +51,12 @@ func appValue(appName string, property string) string {
value = nginx_vhosts.AppClientHeaderTimeout(appName)
case "disable-custom-config":
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":
value = nginx_vhosts.AppErrorLogPath(appName)
case "hsts-include-subdomains":
@@ -123,6 +129,12 @@ func computedValue(appName string, property string) string {
value = nginx_vhosts.ComputedClientMaxBodySize(appName)
case "disable-custom-config":
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":
value = nginx_vhosts.ComputedErrorLogPath(appName)
case "hsts-include-subdomains":
@@ -195,6 +207,12 @@ func globalValue(appName string, property string) string {
value = nginx_vhosts.GlobalClientMaxBodySize()
case "disable-custom-config":
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":
value = nginx_vhosts.GlobalErrorLogPath()
case "hsts-include-subdomains":

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" "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" "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, 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

@@ -30,6 +30,7 @@ func defaultVars() map[string]interface{} {
"SSL_INUSE": "",
"APP_SSL_PATH": "/home/dokku/app/tls",
"DOKKU_APP_WEB_LISTENERS": "127.0.0.1:5000",
"DOKKU_APP_WEB_LISTENER_HOST": "app.web.docker",
"PROXY_PORT_MAP": "http:80:5000",
"PROXY_UPSTREAM_PORTS": "5000",
"PROXY_PORT": "80",
@@ -37,6 +38,9 @@ func defaultVars() map[string]interface{} {
"PROXY_KEEPALIVE": "",
"NGINX_BIND_ADDRESS_IP4": "",
"NGINX_BIND_ADDRESS_IP6": "::",
"NGINX_DNS_RESOLVER": "",
"NGINX_DNS_RESOLVER_TIMEOUT": "5s",
"NGINX_DNS_ZONE": "docker",
"NGINX_ACCESS_LOG_PATH": "/var/log/nginx/app-access.log",
"NGINX_ACCESS_LOG_FORMAT": "",
"NGINX_ERROR_LOG_PATH": "/var/log/nginx/app-error.log",
@@ -96,6 +100,9 @@ func TestTemplate_HTTPOnlyBasicProxy(t *testing.T) {
out := renderTemplate(t, defaultVars())
mustContain(t, out, "listen [::]:80;")
mustContain(t, out, "proxy_pass http://app-5000;")
mustContain(t, out, "upstream app-5000 {")
mustNotContain(t, out, "resolver ")
mustNotContain(t, out, "$dokku_upstream")
mustContain(t, out, "error_page 500 501 502 503")
mustContain(t, out, "server_name app.example.com;")
mustNotContain(t, out, "ssl_certificate")
@@ -103,6 +110,27 @@ func TestTemplate_HTTPOnlyBasicProxy(t *testing.T) {
mustNotContain(t, out, "http2_push_preload")
}
func TestTemplate_ResolverTimeoutOverride(t *testing.T) {
v := defaultVars()
v["NGINX_DNS_RESOLVER"] = "127.0.0.1:1053"
v["NGINX_DNS_RESOLVER_TIMEOUT"] = "2s"
out := renderTemplate(t, v)
mustContain(t, out, "resolver_timeout 2s;")
mustNotContain(t, out, "resolver_timeout 5s;")
}
func TestTemplate_HTTPOnlyBasicProxyWithResolver(t *testing.T) {
v := defaultVars()
v["NGINX_DNS_RESOLVER"] = "127.0.0.1:1053"
out := renderTemplate(t, v)
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 {")
}
func TestTemplate_HTTPSEmitsPushPreloadWhenSupported(t *testing.T) {
v := defaultVars()
v["PROXY_PORT_MAP"] = "https:443:5000"
@@ -170,6 +198,7 @@ func TestTemplate_NoWebListenersReturns502(t *testing.T) {
v["DOKKU_APP_WEB_LISTENERS"] = ""
out := renderTemplate(t, v)
mustContain(t, out, "return 502;")
mustNotContain(t, out, "proxy_pass http://$dokku_upstream;")
mustNotContain(t, out, "proxy_pass http://app-5000;")
}
@@ -183,6 +212,17 @@ func TestTemplate_GRPCNoSSL(t *testing.T) {
mustNotContain(t, out, "ssl_certificate")
}
func TestTemplate_GRPCNoSSLWithResolver(t *testing.T) {
v := defaultVars()
v["NGINX_DNS_RESOLVER"] = "127.0.0.1:1053"
v["PROXY_PORT_MAP"] = "grpc:50051:50051"
v["PROXY_UPSTREAM_PORTS"] = "50051"
out := renderTemplate(t, v)
mustContain(t, out, "resolver 127.0.0.1:1053 valid=10s ipv6=off;")
mustContain(t, out, `set $dokku_upstream "app.web.docker:50051";`)
mustContain(t, out, "grpc_pass grpc://$dokku_upstream;")
}
func TestTemplate_GRPCS(t *testing.T) {
v := defaultVars()
v["PROXY_PORT_MAP"] = "grpcs:443:50051"
@@ -230,6 +270,18 @@ func TestTemplate_UpstreamBlock(t *testing.T) {
mustContain(t, out, "server 10.0.0.2:5001;")
}
func TestTemplate_UpstreamBlockOmittedInResolverMode(t *testing.T) {
v := defaultVars()
v["NGINX_DNS_RESOLVER"] = "127.0.0.1:1053"
v["PROXY_PORT_MAP"] = "http:80:5000 http:8080:5001"
v["PROXY_UPSTREAM_PORTS"] = "5000 5001"
v["DOKKU_APP_WEB_LISTENERS"] = "10.0.0.1:5000 10.0.0.2:5000"
out := renderTemplate(t, v)
mustNotContain(t, out, "upstream app-5000 {")
mustNotContain(t, out, "upstream app-5001 {")
mustNotContain(t, out, "server 10.0.0.1:5000;")
}
func TestTemplate_UpstreamWithKeepalive(t *testing.T) {
v := defaultVars()
v["PROXY_KEEPALIVE"] = "16"
@@ -280,6 +332,18 @@ func TestTemplate_RouteLocationAndUpstream(t *testing.T) {
mustContain(t, out, "server 10.0.0.4:5001;")
}
func TestTemplate_RouteUpstreamRenderedInResolverMode(t *testing.T) {
v := defaultVars()
v["NGINX_DNS_RESOLVER"] = "127.0.0.1:1053"
v["PROXY_ROUTES"] = "api|5001|/api/v0|0"
v["PROXY_ROUTE_LISTENERS"] = "api|5001|10.0.0.3:5001"
out := renderTemplate(t, v)
mustContain(t, out, "proxy_pass http://$dokku_upstream;")
mustContain(t, out, "proxy_pass http://app-api-5001;")
mustContain(t, out, "upstream app-api-5001 {")
mustNotContain(t, out, "upstream app-5000 {")
}
func TestTemplate_RouteStripPrefix(t *testing.T) {
v := defaultVars()
v["PROXY_ROUTES"] = "api|5001|/api/v0|1"

View File

@@ -84,7 +84,14 @@ server {
gzip_vary on;
gzip_comp_level 6;
{{ if $.NGINX_DNS_RESOLVER }}
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 }}
proxy_pass http://{{ $.APP }}-{{ $upstream_port }};
{{ end }}
{{ if and $is_ssl (eq $.HTTP2_PUSH_SUPPORTED "true") }}http2_push_preload on; {{ end }}
proxy_http_version {{ $.PROXY_HTTP_VERSION }};
proxy_connect_timeout {{ $.PROXY_CONNECT_TIMEOUT }};
@@ -166,7 +173,14 @@ server {
{{ end }}
location / {
{{ if $.NGINX_DNS_RESOLVER }}
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 }}
grpc_pass grpc://{{ $.APP }}-{{ $upstream_port }};
{{ end }}
}
client_max_body_size {{ $.CLIENT_MAX_BODY_SIZE }};
@@ -176,6 +190,7 @@ server {
{{ end }}
{{ end }}
{{ if not $.NGINX_DNS_RESOLVER }}
{{ if $.DOKKU_APP_WEB_LISTENERS }}
{{ range $upstream_port := $.PROXY_UPSTREAM_PORTS | split " " }}
upstream {{ $.APP }}-{{ $upstream_port }} {
@@ -185,7 +200,7 @@ upstream {{ $.APP }}-{{ $upstream_port }} {
{{ $listener_ip := index $listener_list 0 }}
server {{ $listener_ip }}:{{ $upstream_port }};{{ end }}
}
{{ end }}{{ end }}
{{ end }}{{ end }}{{ end }}
{{ range $entry := $.PROXY_ROUTE_LISTENERS | split " " }}
{{ if $entry }}

View File

@@ -239,6 +239,10 @@ deploy-test-config:
@echo deploying config app...
cd tests && ./test_deploy ./apps/config $(DOKKU_DOMAIN)
deploy-test-coredns-docker-outage:
@echo testing nginx-vhosts behavior when coredns-docker is unavailable...
cd tests && ./test_coredns_outage $(DOKKU_DOMAIN) docker
deploy-test-dockerfile:
@echo deploying dockerfile app...
cd tests && ./test_deploy ./apps/dockerfile $(DOKKU_DOMAIN)
@@ -342,7 +346,7 @@ deploy-test-storage-lib-host-root:
@echo deploy-test-storage-lib-host-root
cd tests/docker && bats storage.bats
test-ci-docker: setup-docker-deploy-tests deploy-test-checks-root deploy-test-config deploy-test-multi deploy-test-go-fail-predeploy deploy-test-go-fail-postdeploy deploy-test-storage-lib-host-root test-ci-binaries
test-ci-docker: setup-docker-deploy-tests deploy-test-checks-root deploy-test-config deploy-test-multi deploy-test-go-fail-predeploy deploy-test-go-fail-postdeploy deploy-test-storage-lib-host-root deploy-test-coredns-docker-outage test-ci-binaries
generate-ssl-tars: generate-ssl-tar generate-ssl-sans-tar generate-ssl-wildcard-tar

View File

@@ -20,6 +20,12 @@ install_dependencies() {
curl -L "https://packagecloud.io/dokku/dokku/packages/ubuntu/noble/docker-container-healthchecker_${DOCKER_CONTAINER_HEALTHCHECKER_VERSION}_amd64.deb/download.deb" -o "$ROOT_DIR/build/${DOCKER_CONTAINER_HEALTHCHECKER_PACKAGE_NAME}"
fi
COREDNS_DOCKER_VERSION=$(jq -r --arg name coredns-docker '.dependencies[] | select(.name == $name) | .version' contrib/dependencies.json)
COREDNS_DOCKER_PACKAGE_NAME="coredns-docker_${COREDNS_DOCKER_VERSION}_amd64.deb"
if [[ ! -f "$ROOT_DIR/build/${COREDNS_DOCKER_PACKAGE_NAME}" ]]; then
curl -L "https://packagecloud.io/dokku/dokku/packages/ubuntu/noble/coredns-docker_${COREDNS_DOCKER_VERSION}_amd64.deb/download.deb" -o "$ROOT_DIR/build/${COREDNS_DOCKER_PACKAGE_NAME}"
fi
HEROKUISH_VERSION=$(jq -r --arg name herokuish '.recommendations[] | select(.name == $name) | .version' contrib/dependencies.json)
HEROKUISH_PACKAGE_NAME="herokuish_${HEROKUISH_VERSION}_amd64.deb"
if [[ ! -f "$ROOT_DIR/build/${HEROKUISH_PACKAGE_NAME}" ]]; then
@@ -68,6 +74,7 @@ install_dependencies() {
ls -lah "${ROOT_DIR}/build/"
sudo dpkg -i \
"${ROOT_DIR}/build/$COREDNS_DOCKER_PACKAGE_NAME" \
"${ROOT_DIR}/build/$DOCKER_CONTAINER_HEALTHCHECKER_PACKAGE_NAME" \
"${ROOT_DIR}/build/$DOCKER_IMAGE_LABELER_PACKAGE_NAME" \
"${ROOT_DIR}/build/$HEROKUISH_PACKAGE_NAME" \

108
tests/test_coredns_outage Executable file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -xeo pipefail
SELF=$(command -v "$0")
TARGET="$1"
SCOPE="${2:-host}"
REMOTE="dokku@$TARGET"
APP="test-coredns-outage-$RANDOM"
cleanup() {
local exit_code=$?
set +e
start_coredns_docker
ssh "$REMOTE" apps:destroy "$APP" --force >/dev/null 2>&1
ssh "$REMOTE" nginx:set --global dns-resolver >/dev/null 2>&1
ssh "$REMOTE" events:off >/dev/null 2>&1
exit $exit_code
}
stop_coredns_docker() {
case "$SCOPE" in
docker)
docker exec dokku sv stop coredns-docker
;;
host)
sudo systemctl stop coredns-docker
;;
*)
echo "Unknown scope: $SCOPE"
exit 1
;;
esac
}
start_coredns_docker() {
case "$SCOPE" in
docker)
docker exec dokku sv start coredns-docker
;;
host)
sudo systemctl start coredns-docker
;;
*)
echo "Unknown scope: $SCOPE"
exit 1
;;
esac
}
curl_status() {
curl -kSso /dev/null -w "%{http_code}" -m 10 "http://$APP.$TARGET/"
}
wait_for_status() {
local expected="$1" attempts="${2:-15}" actual
while [[ $attempts -gt 0 ]]; do
actual="$(curl_status || true)"
[[ "$actual" == "$expected" ]] && return 0
attempts=$((attempts - 1))
sleep 1
done
echo "Expected HTTP $expected, last observed $actual"
return 1
}
trap cleanup INT TERM EXIT
ssh "$REMOTE" events:on
ssh "$REMOTE" nginx:set --global dns-resolver 127.0.0.1:1053
TMP=$(mktemp -d "/tmp/$TARGET.XXXXX")
rmdir "$TMP" && cp -r "$(dirname "$SELF")/apps/python" "$TMP"
pushd "$TMP" >/dev/null
git init -q
git config user.email "robot@example.com"
git config user.name "Test Robot"
git remote add target "${REMOTE}:${APP}"
[[ -f gitignore ]] && mv gitignore .gitignore
git add .
git commit -qm 'initial commit'
git push target master
popd >/dev/null
rm -rf "$TMP"
echo "-----> Verifying app responds before outage"
wait_for_status 200
echo "-----> Stopping coredns-docker ($SCOPE)"
stop_coredns_docker
# Wait long enough for any cached DNS entry (resolver valid=10s in the template)
sleep 12
echo "-----> Verifying nginx returns 502 with coredns-docker stopped"
actual="$(curl_status || true)"
if [[ "$actual" != "502" ]]; then
echo "Expected HTTP 502 with coredns-docker stopped, observed $actual"
exit 1
fi
echo "-----> Restarting coredns-docker"
start_coredns_docker
echo "-----> Verifying app recovers"
wait_for_status 200
echo "-----> coredns-docker outage test PASSED"

View File

@@ -0,0 +1,211 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
[[ -f "$DOKKU_ROOT/VHOST" ]] && cp -fp "$DOKKU_ROOT/VHOST" "$DOKKU_ROOT/VHOST.bak"
create_app
}
teardown() {
sudo systemctl start coredns-docker >/dev/null 2>&1 || true
dokku --quiet nginx:set --global dns-resolver
dokku --quiet nginx:set --global dns-zone
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:set) dns-resolver defaults to empty and dns-zone defaults to docker" {
run /bin/bash -c "dokku nginx:report $TEST_APP --nginx-computed-dns-resolver"
echo "output: $output"
echo "status: $status"
assert_success
assert_output ""
run /bin/bash -c "dokku nginx:report $TEST_APP --nginx-computed-dns-zone"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "docker"
}
@test "(nginx-vhosts:set) dns-resolver and dns-zone can be overridden globally" {
run /bin/bash -c "dokku nginx:set --global dns-resolver 127.0.0.2:5353"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku nginx:set --global dns-zone test"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku nginx:report $TEST_APP --nginx-computed-dns-resolver"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "127.0.0.2:5353"
run /bin/bash -c "dokku nginx:report $TEST_APP --nginx-computed-dns-zone"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "test"
}
@test "(nginx-vhosts) generated nginx.conf uses IP-based upstream by default" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_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_not_contains "resolver "
assert_output_not_contains "\$dokku_upstream"
assert_output_contains "upstream ${TEST_APP}-"
}
@test "(nginx-vhosts) generated nginx.conf uses resolver and variable-based proxy_pass when dns-resolver is set" {
run /bin/bash -c "dokku nginx:set --global dns-resolver 127.0.0.1:1053"
echo "output: $output"
echo "status: $status"
assert_success
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_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 "resolver 127.0.0.1:1053 valid=10s ipv6=off"
assert_output_contains "set \$dokku_upstream"
assert_output_contains "${TEST_APP}.web.docker:"
assert_output_contains "proxy_pass http://\$dokku_upstream"
assert_output_not_contains "upstream ${TEST_APP}-"
}
@test "(nginx-vhosts) unsetting dns-resolver reverts to IP-based upstream" {
run /bin/bash -c "dokku nginx:set --global dns-resolver 127.0.0.1:1053"
echo "output: $output"
echo "status: $status"
assert_success
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_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 "resolver 127.0.0.1:1053 valid=10s ipv6=off"
run /bin/bash -c "dokku nginx:set --global dns-resolver"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku proxy:build-config $TEST_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_not_contains "resolver 127.0.0.1:1053"
assert_output_contains "upstream ${TEST_APP}-"
}
@test "(nginx-vhosts) vendored nginx.conf.sigil renders IP-based upstream regardless of dns-resolver" {
run /bin/bash -c "dokku nginx:set --global dns-resolver 127.0.0.1:1053"
echo "output: $output"
echo "status: $status"
assert_success
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP custom_nginx_template
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_not_contains "resolver 127.0.0.1:1053"
assert_output_not_contains "\$dokku_upstream"
assert_output_contains "upstream ${TEST_APP}-"
}
@test "(nginx-vhosts) nginx returns 502 when coredns-docker is stopped" {
run /bin/bash -c "dokku nginx:set --global dns-resolver 127.0.0.1:1053"
echo "output: $output"
echo "status: $status"
assert_success
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
echo "output: $output"
echo "status: $status"
assert_success
run sudo systemctl stop coredns-docker
echo "output: $output"
echo "status: $status"
assert_success
run curl --connect-to "$TEST_APP.$DOKKU_DOMAIN:80:localhost:80" -kSso /dev/null -w "%{http_code}" -m 10 "http://$TEST_APP.$DOKKU_DOMAIN/"
echo "output: $output"
echo "status: $status"
assert_output "502"
run sudo systemctl start coredns-docker
echo "output: $output"
echo "status: $status"
assert_success
assert_http_localhost_response http "$TEST_APP.$DOKKU_DOMAIN" 80 / "" 200
}
@test "(nginx-vhosts) nginx returns 502 when the app container is not running" {
run /bin/bash -c "dokku nginx:set --global dns-resolver 127.0.0.1:1053"
echo "output: $output"
echo "status: $status"
assert_success
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
echo "output: $output"
echo "status: $status"
assert_success
assert_http_localhost_response http "$TEST_APP.$DOKKU_DOMAIN" 80 / "" 200
run /bin/bash -c "docker container stop $TEST_APP.web.1"
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 "proxy_pass http://\$dokku_upstream"
assert_http_localhost_response http "$TEST_APP.$DOKKU_DOMAIN" 80 / "" 502
run /bin/bash -c "docker container start $TEST_APP.web.1"
echo "output: $output"
echo "status: $status"
assert_success
assert_http_localhost_response http "$TEST_APP.$DOKKU_DOMAIN" 80 / "" 200
}