diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index 38e184764..294289b7f 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -48,8 +48,8 @@ just composed. | Service | Image | Purpose | License | |---|---|---|---| | [SearXNG](https://github.com/searxng/searxng) | `searxng/searxng:2026.9.25-12f8b6515` (pinned tag; see compose) | Default metasearch backend | AGPL-3.0 | -| [ChromaDB](https://github.com/chroma-core/chroma) | `chromadb/chroma:latest` | Vector store for memory / RAG | Apache-2.0 | -| [ntfy](https://github.com/binwiederhier/ntfy) | `binwiederhier/ntfy` | Push notifications (self-hosted reminders) | Apache-2.0 / GPL-2.0 | +| [ChromaDB](https://github.com/chroma-core/chroma) | `chromadb/chroma:1.5.9` (pinned digest; see compose) | Vector store for memory / RAG | Apache-2.0 | +| [ntfy](https://github.com/binwiederhier/ntfy) | `binwiederhier/ntfy:v2.32.0` (pinned digest; see compose) | Push notifications (self-hosted reminders) | Apache-2.0 / GPL-2.0 | ## Bundled front-end libraries diff --git a/docker-compose.gpu-amd.yml b/docker-compose.gpu-amd.yml index 0e2cdbcb8..5c85014d6 100644 --- a/docker-compose.gpu-amd.yml +++ b/docker-compose.gpu-amd.yml @@ -127,7 +127,7 @@ services: - ${RENDER_GID:-render} chromadb: - image: docker.io/chromadb/chroma:latest + image: docker.io/chromadb/chroma:1.5.9@sha256:1e0b73a187a28757c572acba508c46f48c9e8b0acaf5c20e6d95cdedce1acdf6 ports: - "${CHROMADB_BIND:-127.0.0.1}:8100:8000" volumes: @@ -196,7 +196,7 @@ services: restart: unless-stopped ntfy: - image: docker.io/binwiederhier/ntfy + image: docker.io/binwiederhier/ntfy:v2.32.0@sha256:59250e1d8be2ee2186c4560322f625faef22ace380331f05b63f8842dbd62602 command: serve ports: - "${NTFY_BIND:-127.0.0.1}:8091:80" diff --git a/docker-compose.gpu-nvidia.yml b/docker-compose.gpu-nvidia.yml index 5d997f28b..5c628fcfb 100644 --- a/docker-compose.gpu-nvidia.yml +++ b/docker-compose.gpu-nvidia.yml @@ -130,7 +130,7 @@ services: capabilities: [gpu] chromadb: - image: docker.io/chromadb/chroma:latest + image: docker.io/chromadb/chroma:1.5.9@sha256:1e0b73a187a28757c572acba508c46f48c9e8b0acaf5c20e6d95cdedce1acdf6 ports: - "${CHROMADB_BIND:-127.0.0.1}:8100:8000" volumes: @@ -199,7 +199,7 @@ services: restart: unless-stopped ntfy: - image: docker.io/binwiederhier/ntfy + image: docker.io/binwiederhier/ntfy:v2.32.0@sha256:59250e1d8be2ee2186c4560322f625faef22ace380331f05b63f8842dbd62602 command: serve ports: - "${NTFY_BIND:-127.0.0.1}:8091:80" diff --git a/docker-compose.yml b/docker-compose.yml index 808471520..2b90227a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,7 +108,7 @@ services: restart: unless-stopped chromadb: - image: docker.io/chromadb/chroma:latest + image: docker.io/chromadb/chroma:1.5.9@sha256:1e0b73a187a28757c572acba508c46f48c9e8b0acaf5c20e6d95cdedce1acdf6 ports: - "${CHROMADB_BIND:-127.0.0.1}:8100:8000" volumes: @@ -177,7 +177,7 @@ services: restart: unless-stopped ntfy: - image: docker.io/binwiederhier/ntfy + image: docker.io/binwiederhier/ntfy:v2.32.0@sha256:59250e1d8be2ee2186c4560322f625faef22ace380331f05b63f8842dbd62602 command: serve ports: - "${NTFY_BIND:-127.0.0.1}:8091:80" diff --git a/tests/test_compose_sidecar_pins.py b/tests/test_compose_sidecar_pins.py new file mode 100644 index 000000000..6ba9cb901 --- /dev/null +++ b/tests/test_compose_sidecar_pins.py @@ -0,0 +1,34 @@ +"""Sidecar images must be pinned to a versioned tag and an immutable digest. + +A bare or ``:latest`` reference changes underneath an existing install on the +next ``docker compose pull``/``up``, so two hosts on the same commit can run +different ChromaDB or ntfy builds. The ``odysseus`` service is excluded: it is +the project's own image, selected through ``ODYSSEUS_IMAGE``. The standalone +GPU files are held equal to these services by test_gpu_compose_standalone.py. +""" + +import re +from pathlib import Path + +import pytest +import yaml + +COMPOSE = Path(__file__).resolve().parents[1] / "docker-compose.yml" +PINNED = re.compile(r"^[\w.\-/]+:(?P[\w.\-]+)@sha256:[0-9a-f]{64}$") + + +def _sidecars(): + services = yaml.safe_load(COMPOSE.read_text(encoding="utf-8"))["services"] + return {name: svc["image"] for name, svc in services.items() if name != "odysseus"} + + +def test_expected_sidecars_present(): + assert {"chromadb", "searxng", "ntfy"} <= set(_sidecars()) + + +@pytest.mark.parametrize("name", sorted(_sidecars())) +def test_sidecar_image_has_version_tag_and_digest(name): + image = _sidecars()[name] + match = PINNED.match(image) + assert match, f"{name}: expected :@sha256:, got {image!r}" + assert match.group("tag") != "latest", f"{name}: pin a release tag, not latest"