From 8cad6c3179e429dfa796b714df521279b7a56dce Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 7 Jan 2026 19:44:53 -0500 Subject: [PATCH] feat: configure Traefik readiness healthchecks based on app.json config Closes #6834 --- docs/networking/proxies/traefik.md | 36 ++++++++++++ .../traefik-vhosts/docker-args-process-deploy | 27 +++++++++ tests/apps/python/app-traefik.json | 13 +++++ tests/unit/traefik.bats | 58 +++++++++++++++++++ 4 files changed, 134 insertions(+) create mode 100644 tests/apps/python/app-traefik.json diff --git a/docs/networking/proxies/traefik.md b/docs/networking/proxies/traefik.md index 5e13daa1a..4e996d6d5 100644 --- a/docs/networking/proxies/traefik.md +++ b/docs/networking/proxies/traefik.md @@ -32,6 +32,42 @@ The Traefik plugin has specific rules for routing requests: - If no `https:443` mapping is found, the first `https` port mapping is used for https requests. - If no `https` mapping is found, the container port from `http:80` will be used for https requests. - Requests are routed as soon as the container is running and passing healthchecks. +- Readiness healthchecks defined in `app.json` with a `path` property are automatically transformed into Traefik healthcheck labels. + +### Healthchecks + +When an app has a readiness healthcheck defined in its `app.json` file with a `path` property, Dokku automatically generates Traefik healthcheck labels. These labels configure Traefik to perform health checks on the container before routing traffic to it. + +The following `app.json` healthcheck properties are mapped to Traefik labels: + +| app.json Property | Traefik Label Property | Description | +|-------------------|------------------------|-------------| +| `path` | `healthcheck.path` | The HTTP path to check (required) | +| `scheme` | `healthcheck.scheme` | The scheme to use (`http` or `https`) | +| `port` | `healthcheck.port` | The port to check | +| `timeout` | `healthcheck.timeout` | Timeout in seconds (formatted as `Xs`) | +| `wait` | `healthcheck.interval` | Interval between checks in seconds (formatted as `Xs`) | + +Example `app.json` configuration: + +```json +{ + "healthchecks": { + "web": [ + { + "name": "web readiness check", + "path": "/health", + "timeout": 5, + "type": "readiness", + "wait": 10 + } + ] + } +} +``` + +> [!NOTE] +> Only the first readiness healthcheck with a `path` property is used. Liveness, startup, and command-based healthchecks are not transformed into Traefik labels. ### Switching to Traefik diff --git a/plugins/traefik-vhosts/docker-args-process-deploy b/plugins/traefik-vhosts/docker-args-process-deploy index d127288d8..be8196862 100755 --- a/plugins/traefik-vhosts/docker-args-process-deploy +++ b/plugins/traefik-vhosts/docker-args-process-deploy @@ -129,6 +129,33 @@ trigger-traefik-vhosts-docker-args-process-deploy() { output="$output --label \"traefik.http.routers.$APP-$PROC_TYPE-https.rule=$traefik_domains\"" fi fi + + local app_json hc_path hc_scheme hc_port hc_timeout hc_interval hc_filter + app_json="$(plugn trigger app-json-get-content "$APP" 2>/dev/null || echo "{}")" + hc_filter='.healthchecks.web // [] | map(select(.type == "readiness" and .path != null and .path != "")) | .[0]' + hc_path=$(echo "$app_json" | jq -r "$hc_filter | .path // empty") + if [[ -n "$hc_path" ]]; then + hc_scheme=$(echo "$app_json" | jq -r "$hc_filter | .scheme // empty") + hc_port=$(echo "$app_json" | jq -r "$hc_filter | .port // empty") + hc_timeout=$(echo "$app_json" | jq -r "$hc_filter | .timeout // empty") + hc_interval=$(echo "$app_json" | jq -r "$hc_filter | .wait // empty") + + if [[ -n "$proxy_container_http_port" ]]; then + output="$output --label traefik.http.services.$APP-$PROC_TYPE-http.loadbalancer.healthcheck.path=$hc_path" + [[ -n "$hc_scheme" ]] && output="$output --label traefik.http.services.$APP-$PROC_TYPE-http.loadbalancer.healthcheck.scheme=$hc_scheme" + [[ -n "$hc_port" ]] && output="$output --label traefik.http.services.$APP-$PROC_TYPE-http.loadbalancer.healthcheck.port=$hc_port" + [[ -n "$hc_timeout" ]] && output="$output --label traefik.http.services.$APP-$PROC_TYPE-http.loadbalancer.healthcheck.timeout=${hc_timeout}s" + [[ -n "$hc_interval" ]] && output="$output --label traefik.http.services.$APP-$PROC_TYPE-http.loadbalancer.healthcheck.interval=${hc_interval}s" + fi + + if [[ -n "$proxy_container_https_port" ]]; then + output="$output --label traefik.http.services.$APP-$PROC_TYPE-https.loadbalancer.healthcheck.path=$hc_path" + [[ -n "$hc_scheme" ]] && output="$output --label traefik.http.services.$APP-$PROC_TYPE-https.loadbalancer.healthcheck.scheme=$hc_scheme" + [[ -n "$hc_port" ]] && output="$output --label traefik.http.services.$APP-$PROC_TYPE-https.loadbalancer.healthcheck.port=$hc_port" + [[ -n "$hc_timeout" ]] && output="$output --label traefik.http.services.$APP-$PROC_TYPE-https.loadbalancer.healthcheck.timeout=${hc_timeout}s" + [[ -n "$hc_interval" ]] && output="$output --label traefik.http.services.$APP-$PROC_TYPE-https.loadbalancer.healthcheck.interval=${hc_interval}s" + fi + fi fi local proxy_labels_file_path=$(fn-proxy-get-labels-file-path "traefik" "$APP") diff --git a/tests/apps/python/app-traefik.json b/tests/apps/python/app-traefik.json new file mode 100644 index 000000000..25c42a9d8 --- /dev/null +++ b/tests/apps/python/app-traefik.json @@ -0,0 +1,13 @@ +{ + "healthchecks": { + "web": [ + { + "name": "web readiness check", + "path": "/", + "timeout": 5, + "type": "readiness", + "wait": 2 + } + ] + } +} diff --git a/tests/unit/traefik.bats b/tests/unit/traefik.bats index b627a8968..d94808ec1 100644 --- a/tests/unit/traefik.bats +++ b/tests/unit/traefik.bats @@ -734,3 +734,61 @@ teardown() { run /bin/bash -c "dokku traefik:set --global dns-provider-cf_api_email" run /bin/bash -c "dokku traefik:set --global dns-provider-cf_api_key" } + + +@test "(traefik) healthcheck labels from app.json" { + run /bin/bash -c "dokku proxy:set $TEST_APP traefik" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP setup_traefik_healthcheck + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "docker inspect $TEST_APP.web.1 --format '{{ index .Config.Labels \"traefik.http.services.$TEST_APP-web-http.loadbalancer.healthcheck.path\" }}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "/" + + run /bin/bash -c "docker inspect $TEST_APP.web.1 --format '{{ index .Config.Labels \"traefik.http.services.$TEST_APP-web-http.loadbalancer.healthcheck.timeout\" }}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "5s" + + run /bin/bash -c "docker inspect $TEST_APP.web.1 --format '{{ index .Config.Labels \"traefik.http.services.$TEST_APP-web-http.loadbalancer.healthcheck.interval\" }}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "2s" +} + +@test "(traefik) no healthcheck labels without readiness check" { + run /bin/bash -c "dokku proxy:set $TEST_APP traefik" + 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 "docker inspect $TEST_APP.web.1 --format '{{ index .Config.Labels \"traefik.http.services.$TEST_APP-web-http.loadbalancer.healthcheck.path\" }}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_not_exists +} + +setup_traefik_healthcheck() { + local APP="$1" + local APP_REPO_DIR="$2" + [[ -z "$APP" ]] && local APP="$TEST_APP" + APP_REPO_DIR="$(realpath "$APP_REPO_DIR")" + + mv "$APP_REPO_DIR/app-traefik.json" "$APP_REPO_DIR/app.json" +}