From 8e82f5759d2ac407856b3dec8010fcced5fa9687 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Mon, 28 Sep 2026 09:31:56 +0000 Subject: [PATCH] Add a Docker HEALTHCHECK and make docker-smoke (closes #111) The runtime stage declares a HEALTHCHECK that probes /.well-known/healthcheck.json with busybox wget. script/docker-smoke (make docker-smoke) builds the image with script/docker, starts it with a random PIXA_SIGNING_KEY, and passes only once Docker reports the container healthy within 30 seconds; the container is removed on exit and its log printed on failure. The Gitea workflow runs it after script/cibuild; it is not part of make check. It waits on Docker's health status instead of polling a published host port because the Gitea job runs in its own container on its own network, where such a port is not reachable at localhost. Model: opus-5-5 --- .gitea/workflows/check.yml | 1 + Dockerfile | 3 +++ Makefile | 7 +++++- README.md | 1 + TODO.md | 6 ++++++ script/docker-smoke | 44 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 61 insertions(+), 1 deletion(-) create mode 100755 script/docker-smoke diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index ee73864..decade9 100644 --- a/.gitea/workflows/check.yml +++ b/.gitea/workflows/check.yml @@ -7,3 +7,4 @@ jobs: # actions/checkout v4.2.2, 2026-02-22 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - run: script/cibuild + - run: script/docker-smoke diff --git a/Dockerfile b/Dockerfile index 27ab84b..bec19d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,4 +76,7 @@ WORKDIR /var/lib/pixa EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --spider -q http://localhost:8080/.well-known/healthcheck.json || exit 1 + ENTRYPOINT ["/usr/local/bin/pixad", "--config", "/etc/pixa/config.yml"] diff --git a/Makefile b/Makefile index 2fbafdc..27001a8 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: bootstrap setup check lint test fmt fmt-check build clean docker docker-versioned docker-test devserver devserver-stop hooks +.PHONY: bootstrap setup check lint test fmt fmt-check build clean docker docker-smoke docker-versioned docker-test devserver devserver-stop hooks VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") LDFLAGS := -X main.Version=$(VERSION) @@ -54,6 +54,11 @@ clean: docker: @script/docker +# Build the image, start it, and wait for its healthcheck (needs Docker; +# not part of check) +docker-smoke: + @script/docker-smoke + # Build Docker image tagged pixad:$(VERSION) and pixad:latest docker-versioned: docker build --build-arg VERSION=$(VERSION) -t pixad:$(VERSION) -t pixad:latest . diff --git a/README.md b/README.md index fe32320..0356e75 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,7 @@ them. We provide: - `script/fmt-check` — check formatting (read-only) - `script/check` — run test, lint, and fmt-check - `script/docker` — build the Docker image tagged via `script/projectname` +- `script/docker-smoke` — build the image, start it, wait for it to be healthy - `script/cibuild` — CI entrypoint: `docker build .` (the Dockerfile runs the checks, so a green build implies a green repo) - `script/precommit` — pre-commit checks (`go mod tidy` guard, then diff --git a/TODO.md b/TODO.md index 715ad1b..0c52f04 100644 --- a/TODO.md +++ b/TODO.md @@ -30,6 +30,12 @@ exhaustion # Completed Steps +- 2026-09-28 Docker image healthcheck (closes #111): a `HEALTHCHECK` in + the runtime stage probing `/.well-known/healthcheck.json` with busybox + `wget`; `script/docker-smoke` (`make docker-smoke`) builds the image, + starts it with a throwaway `PIXA_SIGNING_KEY`, and passes only once + Docker reports it healthy within 30 seconds, removing the container on + exit; the Gitea workflow runs it after `script/cibuild`. - 2026-09-21 trusted-proxy client IP resolution (closes #94): a `trusted_proxies` config key taking a list of CIDRs, parsed by the same `net/netip` list parser as `blocked_networks` (an invalid entry aborts diff --git a/script/docker-smoke b/script/docker-smoke new file mode 100755 index 0000000..6116644 --- /dev/null +++ b/script/docker-smoke @@ -0,0 +1,44 @@ +#!/bin/sh +# script/docker-smoke: build the Docker image, start it, and wait up to +# 30 seconds for its HEALTHCHECK to report healthy. Needs a Docker +# daemon, so it is not part of script/check; the Gitea workflow runs it +# after script/cibuild. +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" + +main() { + cd "$ROOT" + "$SCRIPT_DIR/docker" + + # A fresh random key: the container publishes no port and is + # removed on exit. + key="$(head -c 32 /dev/urandom | base64)" + + # --health-interval=1s overrides the image's 30s interval so the + # first probe does not use up the whole wait. + cid="$(docker create --health-interval=1s \ + -e PIXA_SIGNING_KEY="$key" "$("$SCRIPT_DIR/projectname")")" + # Remove the container on any exit; turning signals into exit makes + # an interrupted run clean up too. + trap 'docker rm -f "$cid" >/dev/null' EXIT + trap 'exit 1' HUP INT TERM + docker start "$cid" >/dev/null + + deadline=$(($(date +%s) + 30)) + while [ "$(date +%s)" -lt "$deadline" ]; do + health="$(docker inspect --format '{{.State.Health.Status}}' "$cid")" + if [ "$health" = healthy ]; then + echo "docker-smoke: container is healthy" + return 0 + fi + sleep 1 + done + + echo "docker-smoke: container not healthy after 30 seconds; its log:" >&2 + docker logs "$cid" >&2 + return 1 +} + +main "$@"