From 70001858f58a05c8dacd410647f5766f1f4e9f4b Mon Sep 17 00:00:00 2001 From: Exaviz Date: Wed, 1 Jul 2026 22:04:19 -0400 Subject: [PATCH 1/3] feat: add healthcheck.sh for Docker Compose deployments Reports each service's container status (and Docker health where a healthcheck is defined) plus front reachability, and exits non-zero if anything is down, so it works as a cron or monitoring probe. Read-only. Portable (bash 3.2+, no mapfile). Documented in the README. Validated: exit 0 when all services run; exit 1 with the down service flagged when one is stopped. Signed-off-by: Exaviz --- healthcheck.sh | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100755 healthcheck.sh diff --git a/healthcheck.sh b/healthcheck.sh new file mode 100755 index 0000000..73e4a3b --- /dev/null +++ b/healthcheck.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# +# healthcheck.sh - quick health check of a Docker Compose deployment. +# +# Reports every service's container status (and Docker health, where a service defines +# a healthcheck), and optionally whether the front is reachable. Exits 0 if all services +# are running and none are unhealthy, non-zero otherwise - so it is usable in cron/CI or +# a monitoring probe. Read-only; does not touch the stack. +# +# Usage: ./healthcheck.sh [--help] + +set -uo pipefail + +cd "$(dirname "$0")" || exit 1 + +case "${1:-}" in + --help) + echo "Usage: $0" + echo "Checks that all Compose services are running (and healthy where a healthcheck" + echo "is defined), plus front reachability if the port is known. Exit 0 = all good." + exit 0 + ;; +esac + +if docker compose version >/dev/null 2>&1; then + COMPOSE="docker compose" +elif command -v docker-compose >/dev/null 2>&1; then + COMPOSE="docker-compose" +else + echo "Error: docker compose is not available." >&2 + exit 1 +fi + +# Portable array fill (avoid mapfile; not present in bash 3.x). +SERVICES=() +while IFS= read -r line; do + [ -n "$line" ] && SERVICES+=("$line") +done < <($COMPOSE config --services 2>/dev/null | sort) +if [ "${#SERVICES[@]}" -eq 0 ]; then + echo "No services found. Run this from the directory with compose.yml." >&2 + exit 1 +fi + +bad=0 +printf '%-14s %-12s %s\n' "SERVICE" "STATE" "HEALTH" +for svc in "${SERVICES[@]}"; do + cid=$($COMPOSE ps -aq "$svc" 2>/dev/null | head -1) + if [ -z "$cid" ]; then + printf '%-14s %-12s %s\n' "$svc" "no-container" "-" + bad=1 + continue + fi + state=$(docker inspect "$cid" --format '{{.State.Status}}' 2>/dev/null) + health=$(docker inspect "$cid" --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}-{{end}}' 2>/dev/null) + printf '%-14s %-12s %s\n' "$svc" "${state:-unknown}" "${health:-'-'}" + [ "$state" = "running" ] || bad=1 + [ "$health" = "unhealthy" ] && bad=1 +done + +# Optional: front reachability, if we can determine the local HTTP port from config. +PORT=$(grep -hE '^HTTP_PORT=' .env huly.conf huly_v7.conf 2>/dev/null | tail -1 | cut -d= -f2 | tr -d '"' | tr -d '[:space:]') +if [ -n "${PORT:-}" ]; then + code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 "http://localhost:${PORT}" 2>/dev/null || true) + case "$code" in + 200|301|302) echo "front: http://localhost:${PORT} -> HTTP $code (reachable)" ;; + *) echo "front: http://localhost:${PORT} -> ${code:-no response}"; bad=1 ;; + esac +fi + +if [ "$bad" -eq 0 ]; then + echo -e "\033[1;32mOK: all services running.\033[0m" + exit 0 +else + echo -e "\033[31mPROBLEM: one or more services are not running/healthy.\033[0m Check 'docker compose ps' and logs." + exit 1 +fi From 0e79a6c52e91232615cef120960301c40f60178e Mon Sep 17 00:00:00 2001 From: Exaviz Date: Wed, 1 Jul 2026 22:04:49 -0400 Subject: [PATCH 2/3] docs: link healthcheck.sh in the README Signed-off-by: Exaviz --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 5aae414..2238302 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,16 @@ Now, launch your web browser and enjoy Huly! To stop all services, run `docker c > [!IMPORTANT] > Provided configrations include deployments of CockroachDB and Redpanda which might not be production-ready. Please inspect them carefully before using in production. For more information on the recommended deployment configurations, please refer to the [CockroachDB](https://www.cockroachlabs.com/docs/stable/recommended-production-settings) and [Redpanda](https://docs.redpanda.com/24.3/deploy/) documentation. +## Health check + +Run `./healthcheck.sh` to see whether every service is up (and healthy, where a service +defines a Docker healthcheck) plus front reachability. It exits non-zero if anything is +down, so it doubles as a cron or monitoring probe: + +```bash +./healthcheck.sh +``` + ## Troubleshooting ### Huly opens, but user sign-up fails From 083102e001e7fe192db2afa5a040e613b736ab6a Mon Sep 17 00:00:00 2001 From: Exaviz Date: Wed, 1 Jul 2026 22:43:13 -0400 Subject: [PATCH 3/3] fix: healthcheck.sh treats cleanly-exited one-shot containers as OK Found by running against a real deployment: the huly-tools init container has restart: "no" and exits 0 after doing its work, which the check wrongly reported as a failure. Now a container that exited 0 with restart policy 'no' is treated as a completed one-shot, not a problem. Signed-off-by: Exaviz --- healthcheck.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/healthcheck.sh b/healthcheck.sh index 73e4a3b..66119b2 100755 --- a/healthcheck.sh +++ b/healthcheck.sh @@ -42,19 +42,29 @@ if [ "${#SERVICES[@]}" -eq 0 ]; then fi bad=0 -printf '%-14s %-12s %s\n' "SERVICE" "STATE" "HEALTH" +printf '%-16s %-22s %s\n' "SERVICE" "STATE" "HEALTH" for svc in "${SERVICES[@]}"; do cid=$($COMPOSE ps -aq "$svc" 2>/dev/null | head -1) if [ -z "$cid" ]; then - printf '%-14s %-12s %s\n' "$svc" "no-container" "-" + printf '%-16s %-22s %s\n' "$svc" "no-container" "-" bad=1 continue fi state=$(docker inspect "$cid" --format '{{.State.Status}}' 2>/dev/null) health=$(docker inspect "$cid" --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}-{{end}}' 2>/dev/null) - printf '%-14s %-12s %s\n' "$svc" "${state:-unknown}" "${health:-'-'}" - [ "$state" = "running" ] || bad=1 - [ "$health" = "unhealthy" ] && bad=1 + exitcode=$(docker inspect "$cid" --format '{{.State.ExitCode}}' 2>/dev/null) + restart=$(docker inspect "$cid" --format '{{.HostConfig.RestartPolicy.Name}}' 2>/dev/null) + label="$state" + if [ "$state" = "running" ]; then + [ "$health" = "unhealthy" ] && bad=1 + elif [ "$state" = "exited" ] && [ "${exitcode:-1}" = "0" ] && { [ "$restart" = "no" ] || [ -z "$restart" ]; }; then + # A one-shot/init container that ran and exited cleanly (e.g. huly-tools, + # which has restart: "no"). Expected, not a failure. + label="exited (one-shot ok)" + else + bad=1 + fi + printf '%-16s %-22s %s\n' "$svc" "$label" "${health:--}" done # Optional: front reachability, if we can determine the local HTTP port from config.