From 42ffe694864ab48b4661621f1fa07e59b976f94e Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Tue, 13 Jan 2026 22:53:40 -0500 Subject: [PATCH] feat: add docker health check --- CHANGELOG.md | 1 + Dockerfile | 14 ++++++++++++-- build.sh | 21 +++++++++++++++++++- cmd/hp_healthcheck/hp_healthcheck.go | 29 ++++++++++++++++++++++++++++ docs/install/docker.md | 22 +++++++++++++++++++++ 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 cmd/hp_healthcheck/hp_healthcheck.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f704d..94f7563 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Correctly apply Gravatar profile pictures on the user page if applicable (closes [#405](https://github.com/tale/headplane/issues/405)). - Machine key registration no longer works if the key isn't 24 characters long (closes [#415](https://github.com/tale/headplane/issues/415)). - Fixed some mobile CSS issues across the application (closes [#401](https://github.com/tale/headplane/issues/401)). +- Added a Docker healthcheck to the container (closes [#411](https://github.com/tale/headplane/issues/411)). --- # 0.6.1 (October 12, 2025) diff --git a/Dockerfile b/Dockerfile index 7160ba2..93c9f25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,14 +11,16 @@ ARG TARGETOS ARG TARGETARCH ARG IMAGE_TAG RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 IMAGE_TAG=$IMAGE_TAG \ - ./build.sh --wasm --agent --fake-shell \ + ./build.sh --wasm --agent --fake-shell --healthcheck \ --wasm-output /bin/hp_ssh.wasm \ --agent-output /bin/hp_agent \ - --fake-shell-output /bin/fake-sh + --fake-shell-output /bin/fake-sh \ + --healthcheck-output /bin/hp_healthcheck RUN chmod +x /bin/hp_ssh.wasm RUN chmod +x /bin/hp_agent RUN chmod +x /bin/fake-sh +RUN chmod +x /bin/hp_healthcheck # Folder needs to exist for later stages RUN mkdir -p /var/lib/headplane/agent @@ -49,6 +51,10 @@ COPY --from=go-base /var/lib/headplane /var/lib/headplane COPY --from=go-base /bin/fake-sh /bin/sh COPY --from=go-base /bin/fake-sh /bin/bash +COPY --from=go-base /bin/hp_healthcheck /bin/hp_healthcheck +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD ["/bin/hp_healthcheck"] + WORKDIR /app CMD [ "/app/build/server/index.js" ] @@ -61,6 +67,10 @@ COPY --from=js-base /run/node_modules /app/node_modules COPY --from=go-base /bin/hp_agent /usr/libexec/headplane/agent COPY --from=go-base /var/lib/headplane /var/lib/headplane +COPY --from=go-base /bin/hp_healthcheck /bin/hp_healthcheck + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD ["/bin/hp_healthcheck"] WORKDIR /app CMD [ "node", "/app/build/server/index.js" ] diff --git a/build.sh b/build.sh index 30769f7..00ec6f4 100755 --- a/build.sh +++ b/build.sh @@ -16,6 +16,7 @@ BUILD_WASM=0 BUILD_APP=0 BUILD_AGENT=0 BUILD_FAKE_SHELL=0 +BUILD_HEALTHCHECK=0 SKIP_PATH_CHECKS=0 SKIP_PNPM_PRUNE=0 APP_INSTALL_ONLY=0 @@ -23,6 +24,7 @@ APP_INSTALL_ONLY=0 WASM_OUTPUT="$PUBLIC_DIR/hp_ssh.wasm" AGENT_OUTPUT="$BUILD_DIR/hp_agent" FAKE_SHELL_OUTPUT="$BUILD_DIR/hp_fake_sh" +HEALTHCHECK_OUTPUT="$BUILD_DIR/hp_healthcheck" die() { echo "error: $*" >&2; exit 1; } run() { echo ">> $*"; "$@"; } @@ -33,6 +35,7 @@ while [ $# -gt 0 ]; do --app) BUILD_APP=1 ;; --agent) BUILD_AGENT=1 ;; --fake-shell) BUILD_FAKE_SHELL=1 ;; + --healthcheck) BUILD_HEALTHCHECK=1 ;; --skip-path-checks) SKIP_PATH_CHECKS=1 ;; --skip-pnpm-prune) SKIP_PNPM_PRUNE=1 ;; --app-install-only) APP_INSTALL_ONLY=1 ;; @@ -55,6 +58,12 @@ while [ $# -gt 0 ]; do FAKE_SHELL_OUTPUT=$1 ;; + --healthcheck-output) + shift + [ $# -gt 0 ] || die "--healthcheck-output requires a path" + HEALTHCHECK_OUTPUT=$1 + ;; + --help) cat < override wasm output path --agent-output override agent output path --fake-shell-output override fake shell output path + --healthcheck-output override healthcheck output path EOF exit 0 ;; @@ -84,6 +95,8 @@ if [ "$BUILD_WASM" -eq 0 ] && [ "$BUILD_APP" -eq 0 ] && \ BUILD_WASM=1 BUILD_APP=1 BUILD_AGENT=1 + BUILD_HEALTHCHECK=1 + BUILD_FAKE_SHELL=0 fi if [ "$SKIP_PATH_CHECKS" -eq 0 ]; then @@ -96,6 +109,7 @@ if [ "$SKIP_PATH_CHECKS" -eq 0 ]; then [ "$BUILD_APP" -eq 1 ] && need_pnpm=1 [ "$BUILD_AGENT" -eq 1 ] && need_go=1 [ "$BUILD_FAKE_SHELL" -eq 1 ] && need_go=1 + [ "$BUILD_HEALTHCHECK" -eq 1 ] && need_go=1 if [ $need_go -eq 1 ]; then echo "==> Checking for Go toolchain" @@ -160,11 +174,16 @@ build_fake_shell() { -o "$FAKE_SHELL_OUTPUT" ./cmd/fake_sh } -[ "$BUILD_FAKE_SHELL" = 1 ] && build_fake_shell +build_healthcheck() { + echo "==> Building healthcheck binary → $HEALTHCHECK_OUTPUT" + mkdir -p "$(dirname "$HEALTHCHECK_OUTPUT")" + go build -o "$HEALTHCHECK_OUTPUT" ./cmd/hp_healthcheck +} [ "$BUILD_WASM" = 1 ] && build_wasm [ "$BUILD_APP" = 1 ] && build_app [ "$BUILD_AGENT" = 1 ] && build_agent [ "$BUILD_FAKE_SHELL" = 1 ] && build_fake_shell +[ "$BUILD_HEALTHCHECK" = 1 ] && build_healthcheck echo "✅ Build complete." diff --git a/cmd/hp_healthcheck/hp_healthcheck.go b/cmd/hp_healthcheck/hp_healthcheck.go new file mode 100644 index 0000000..89df82b --- /dev/null +++ b/cmd/hp_healthcheck/hp_healthcheck.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "time" +) + +func main() { + client := http.Client{ + Timeout: 5 * time.Second, + } + + resp, err := client.Get("http://localhost:3000/admin/healthz") + if err != nil { + fmt.Fprintf(os.Stderr, "Health check failed: %v\n", err) + os.Exit(1) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Fprintf(os.Stderr, "Health check returned non-OK status: %d\n", resp.StatusCode) + os.Exit(1) + } + + fmt.Println("Health check passed.") + os.Exit(0) +} diff --git a/docs/install/docker.md b/docs/install/docker.md index 8700144..f00edba 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -39,6 +39,28 @@ It's important to mount your configuration file and also provide a persistent storage location for Headplane to store its own data. You can also change the port mapping if you want to run it on a different port. +## Health Checks + +The Docker image includes a built-in healthcheck that verifies the Headplane +server is running and responding. Docker will automatically monitor the +container and report its health status. No additional configuration is required. + +The healthcheck binary is located at `/bin/hp_healthcheck` inside the container. +If you need to override the default healthcheck behavior, you can do so in your +`compose.yaml`: + +```yaml +services: + headplane: + image: ghcr.io/tale/headplane:latest + healthcheck: + test: ["/bin/hp_healthcheck"] + interval: 30s + timeout: 5s + start_period: 5s + retries: 3 +``` + ## Accessing Headplane After starting the container, you can access the Headplane web interface by