From 0a7aeea95d34f891d5dd4bae20479de23bb6b0b0 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Thu, 2 Jul 2026 01:42:04 -0700 Subject: [PATCH] fix(selfhost): add a healthcheck to the ollama service ollama had no healthcheck block, unlike every other optional backend (qdrant, postgres, tempo, the backup-exporter). Operators following the in-file --profile ollama instructions got no compose-level signal that Ollama finished starting and is accepting API calls before the app starts using it, and there was no way to add a depends_on: ollama: condition: service_healthy gate even if desired later. Use `ollama list` as the probe (the image ships the ollama CLI but no curl/wget/nc) -- it only succeeds once the daemon is actually serving. --- docker-compose.yml | 8 ++++++ .../selfhost-compose-ollama-health.test.ts | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 test/unit/selfhost-compose-ollama-health.test.ts diff --git a/docker-compose.yml b/docker-compose.yml index b3ebd56f22..bc5f890e6a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -222,6 +222,14 @@ services: profiles: ["ollama"] volumes: - ollama-models:/root/.ollama + # The image ships the ollama CLI but no curl/wget, so probe the daemon through it directly instead of a + # raw HTTP request: `ollama list` only succeeds once the server is accepting API calls. + healthcheck: + test: ["CMD", "ollama", "list"] + interval: 10s + timeout: 5s + start_period: 15s + retries: 5 # ── Litestream (--profile litestream) ───────────────────────────────────── # Continuous WAL backup of the SQLite DB to S3/B2/R2. Copy litestream.yml.example diff --git a/test/unit/selfhost-compose-ollama-health.test.ts b/test/unit/selfhost-compose-ollama-health.test.ts new file mode 100644 index 0000000000..45622522f9 --- /dev/null +++ b/test/unit/selfhost-compose-ollama-health.test.ts @@ -0,0 +1,28 @@ +import { readFileSync } from "node:fs"; +import { parse } from "yaml"; +import { describe, expect, it } from "vitest"; + +function readYaml(path: string): Record { + const value = parse(readFileSync(path, "utf8")); + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`${path} must be a YAML object`); + } + return value as Record; +} + +// Pure structural checks only (no `docker` CLI invocation): the self-hosted runner container this actually +// runs on does not have Docker-in-Docker access, so a test that shells out to `docker compose config` +// would be unreliable/environment-dependent here (same constraint as docker-compose-override-example.test.ts). +describe("docker-compose.yml — ollama healthcheck (#2504)", () => { + it("gives ollama a healthcheck so docker compose ps and a future depends_on can gate on it", () => { + const compose = readYaml("docker-compose.yml"); + const services = (compose.services as Record>) ?? {}; + const ollama = services.ollama ?? {}; + const healthcheck = ollama.healthcheck as { test?: unknown[]; retries?: number }; + + // The image ships the ollama CLI but no curl/wget/nc, so `ollama list` is the only in-image probe that + // only succeeds once the daemon is actually serving API calls. + expect(healthcheck.test).toEqual(["CMD", "ollama", "list"]); + expect(healthcheck.retries).toBeGreaterThan(0); + }); +});