Merge pull request #8595 from dokku/8313-coredns-docker
Resolve nginx upstreams via coredns-docker
This commit is contained in:
@@ -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
2
debian/control
vendored
@@ -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>
|
||||
|
||||
7
docker/etc/runit/runsvdir/default/coredns-docker/run
Executable file
7
docker/etc/runit/runsvdir/default/coredns-docker/run
Executable 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
6
tests.mk
6
tests.mk
@@ -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
|
||||
|
||||
|
||||
@@ -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
108
tests/test_coredns_outage
Executable 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"
|
||||
211
tests/unit/nginx-vhosts_17.bats
Normal file
211
tests/unit/nginx-vhosts_17.bats
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user