Files
dokku/tests/image/healthcheck.sh
Jose Diaz-Gonzalez 2a4393a63a test: cover dokku container healthcheck wiring
Adds a bats lint test that guards the static wiring (nginx conf, dokku-restore finish-script ordering, my_init sentinel reset, and the Dockerfile HEALTHCHECK line) plus a docker smoke test that boots the built image, waits for the health flip, exercises the loopback endpoint, asserts the port is not published to the host, and verifies the negative path. The smoke test is invoked via a new `make test-image-healthcheck` target and runs automatically in the build-image action after `docker buildx --load`.
2026-05-10 21:39:43 -04:00

129 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
IMAGE_TAG="${IMAGE_TAG:-dokku/dokku:latest}"
CONTAINER_NAME="${CONTAINER_NAME:-dokku-healthcheck-smoke}"
START_TIMEOUT_SECONDS="${START_TIMEOUT_SECONDS:-360}"
UNHEALTHY_TIMEOUT_SECONDS="${UNHEALTHY_TIMEOUT_SECONDS:-180}"
log-info() {
echo "-----> $*"
}
log-fail() {
echo " ! $*" >&2
exit 1
}
cleanup() {
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
}
trap cleanup EXIT
require-image() {
if ! docker image inspect "$IMAGE_TAG" >/dev/null 2>&1; then
log-fail "Image '$IMAGE_TAG' not found in local docker daemon. Build it first (eg. 'docker build -t $IMAGE_TAG .')."
fi
}
start-container() {
log-info "Starting container '$CONTAINER_NAME' from image '$IMAGE_TAG'"
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker run -d \
--name "$CONTAINER_NAME" \
--env DOKKU_HOSTNAME=dokku.me \
-v /var/run/docker.sock:/var/run/docker.sock \
"$IMAGE_TAG" >/dev/null
}
wait-for-catchall-vhost() {
local deadline=$((SECONDS + 30))
while (( SECONDS < deadline )); do
if docker exec "$CONTAINER_NAME" test -f /etc/nginx/conf.d/00-default-vhost.conf; then
return 0
fi
sleep 1
done
log-fail "Catch-all default vhost /etc/nginx/conf.d/00-default-vhost.conf was never created. \
Likely cause: a stale build/package/dokku-*.deb that predates the catch-all template shipped in \
plugins/nginx-vhosts/templates/default-site.conf. Rebuild the deb before running this test."
}
wait-for-healthy() {
local deadline=$((SECONDS + START_TIMEOUT_SECONDS))
local status
while (( SECONDS < deadline )); do
status=$(docker inspect --format='{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [[ "$status" == "healthy" ]]; then
log-info "Container reached healthy status"
return 0
fi
sleep 5
done
log-info "Container failed to become healthy within ${START_TIMEOUT_SECONDS}s. Last logs:"
docker logs --tail 100 "$CONTAINER_NAME" || true
log-fail "Container '$CONTAINER_NAME' did not reach healthy status (last status: ${status:-unknown})"
}
assert-endpoint-ok() {
log-info "Verifying /_dokku/health returns ok from inside the container"
local body
body=$(docker exec "$CONTAINER_NAME" curl -fsS http://127.0.0.1:18080/_dokku/health)
if [[ "$body" != "ok" ]]; then
log-fail "Expected health endpoint body 'ok', got: ${body}"
fi
}
assert-port-not-published() {
log-info "Verifying port 18080 is not published to the host"
local ports
ports=$(docker inspect --format='{{json .NetworkSettings.Ports}}' "$CONTAINER_NAME")
if echo "$ports" | grep -q "18080/tcp"; then
log-fail "Port 18080/tcp should not be published to the host. Inspect output: $ports"
fi
if curl --max-time 2 -fsS http://127.0.0.1:18080/_dokku/health >/dev/null 2>&1; then
log-fail "Health endpoint is reachable from the host on 127.0.0.1:18080 - it must be loopback-only inside the container."
fi
}
assert-unhealthy-after-sentinel-removed() {
log-info "Removing /var/run/dokku/ready and confirming endpoint returns failure"
docker exec "$CONTAINER_NAME" rm -f /var/run/dokku/ready
local exit_code=0
docker exec "$CONTAINER_NAME" curl -fsS http://127.0.0.1:18080/_dokku/health >/dev/null 2>&1 || exit_code=$?
if (( exit_code == 0 )); then
log-fail "Health endpoint still returned success after the readiness sentinel was removed"
fi
log-info "Waiting for docker to flip the health status to unhealthy"
local deadline=$((SECONDS + UNHEALTHY_TIMEOUT_SECONDS))
local status
while (( SECONDS < deadline )); do
status=$(docker inspect --format='{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [[ "$status" == "unhealthy" ]]; then
log-info "Container flipped to unhealthy"
return 0
fi
sleep 5
done
log-fail "Container did not flip to unhealthy within ${UNHEALTHY_TIMEOUT_SECONDS}s (last status: ${status:-unknown})"
}
main() {
require-image
start-container
wait-for-catchall-vhost
wait-for-healthy
assert-endpoint-ok
assert-port-not-published
assert-unhealthy-after-sentinel-removed
log-info "All healthcheck smoke tests passed."
}
main "$@"