From 6ad9501faafad7cb0a30cff007e6c61ba9dc155d Mon Sep 17 00:00:00 2001 From: interplanetarychris Date: Tue, 3 Feb 2026 16:36:21 -0800 Subject: [PATCH 1/2] feat: add Docker health check for bot container readiness Add HEALTHCHECK to bot containers that polls the OpenClaw /health endpoint. Dashboard now uses actual container health status instead of time-based heuristic to determine when bot is ready. - Add Healthcheck config when creating containers (2s interval, 30 retries) - Add health field to ContainerStatus type (none/starting/healthy/unhealthy) - Update getContainerStatus to extract health from Docker inspect - Update BotCard to use health status for starting/running distinction Co-Authored-By: Claude Opus 4.5 --- dashboard/src/dashboard/BotCard.tsx | 12 +++++++----- dashboard/src/types.ts | 3 +++ src/services/DockerService.ts | 17 ++++++++++++++++- src/types/container.ts | 4 ++++ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/dashboard/src/dashboard/BotCard.tsx b/dashboard/src/dashboard/BotCard.tsx index fc458df..592fcb3 100644 --- a/dashboard/src/dashboard/BotCard.tsx +++ b/dashboard/src/dashboard/BotCard.tsx @@ -19,11 +19,13 @@ interface BotCardProps { function getEffectiveStatus(bot: Bot): BotStatus { const containerState = bot.container_status?.state; if (containerState === 'running') { - // Check if recently started (within 8 seconds) - const startedAt = bot.container_status?.startedAt; - if (startedAt) { - const elapsed = Date.now() - new Date(startedAt).getTime(); - if (elapsed < 8000) return 'starting'; + // Use Docker health check status to determine if bot is ready + const health = bot.container_status?.health; + if (health === 'starting' || health === 'none') { + return 'starting'; + } + if (health === 'unhealthy') { + return 'error'; } return 'running'; } diff --git a/dashboard/src/types.ts b/dashboard/src/types.ts index d0dd984..4deea7f 100644 --- a/dashboard/src/types.ts +++ b/dashboard/src/types.ts @@ -1,5 +1,7 @@ export type BotStatus = 'created' | 'starting' | 'running' | 'stopped' | 'error'; +export type HealthStatus = 'none' | 'starting' | 'healthy' | 'unhealthy'; + export interface ContainerStatus { id: string; state: string; @@ -7,6 +9,7 @@ export interface ContainerStatus { exitCode: number; startedAt: string; finishedAt: string; + health: HealthStatus; } export interface Bot { diff --git a/src/services/DockerService.ts b/src/services/DockerService.ts index 193eef8..2cc92f2 100644 --- a/src/services/DockerService.ts +++ b/src/services/DockerService.ts @@ -55,6 +55,13 @@ export class DockerService { [LABEL_BOT_ID]: botId, [LABEL_BOT_HOSTNAME]: hostname }, + Healthcheck: { + Test: ['CMD', 'wget', '-q', '--spider', `http://localhost:${config.port}/health`], + Interval: 2_000_000_000, // 2s in nanoseconds + Timeout: 3_000_000_000, // 3s in nanoseconds + Retries: 30, + StartPeriod: 5_000_000_000, // 5s in nanoseconds + }, HostConfig: { Binds: [ `${config.hostSecretsPath}:/run/secrets:ro`, @@ -182,13 +189,21 @@ export class DockerService { const container = this.docker.getContainer(containerName); const info = await container.inspect(); + // Extract health status from Docker's Health field + const healthState = info.State.Health?.Status; + let health: ContainerStatus['health'] = 'none'; + if (healthState === 'starting') health = 'starting'; + else if (healthState === 'healthy') health = 'healthy'; + else if (healthState === 'unhealthy') health = 'unhealthy'; + return { id: info.Id, state: info.State.Status as ContainerStatus['state'], running: info.State.Running, exitCode: info.State.ExitCode, startedAt: info.State.StartedAt, - finishedAt: info.State.FinishedAt + finishedAt: info.State.FinishedAt, + health }; } catch (err) { const dockerErr = err as { statusCode?: number }; diff --git a/src/types/container.ts b/src/types/container.ts index bff5ad4..3c44c3c 100644 --- a/src/types/container.ts +++ b/src/types/container.ts @@ -14,6 +14,9 @@ export type ContainerState = | 'removing' | 'dead'; +/** Docker health check status */ +export type HealthStatus = 'none' | 'starting' | 'healthy' | 'unhealthy'; + /** Container status from Docker inspect */ export interface ContainerStatus { id: string; @@ -22,6 +25,7 @@ export interface ContainerStatus { exitCode: number; startedAt: string; finishedAt: string; + health: HealthStatus; } /** Container info from Docker list (human-readable) */ From cd205e452ca315a135b241ca6cd207d20fc72675 Mon Sep 17 00:00:00 2001 From: interplanetarychris Date: Tue, 3 Feb 2026 16:49:18 -0800 Subject: [PATCH 2/2] fix: treat containers without healthcheck as running Containers created before the healthcheck feature was added have health='none'. These should be treated as running, not starting. Co-Authored-By: Claude Opus 4.5 --- dashboard/src/dashboard/BotCard.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dashboard/src/dashboard/BotCard.tsx b/dashboard/src/dashboard/BotCard.tsx index 592fcb3..6e587d3 100644 --- a/dashboard/src/dashboard/BotCard.tsx +++ b/dashboard/src/dashboard/BotCard.tsx @@ -21,12 +21,13 @@ function getEffectiveStatus(bot: Bot): BotStatus { if (containerState === 'running') { // Use Docker health check status to determine if bot is ready const health = bot.container_status?.health; - if (health === 'starting' || health === 'none') { + if (health === 'starting') { return 'starting'; } if (health === 'unhealthy') { return 'error'; } + // 'healthy' or 'none' (no healthcheck configured) = running return 'running'; } if (containerState === 'exited' || containerState === 'dead') {