diff --git a/backend/tests/test_compose_nginx_image_pin.py b/backend/tests/test_compose_nginx_image_pin.py new file mode 100644 index 0000000000..f83388c5ec --- /dev/null +++ b/backend/tests/test_compose_nginx_image_pin.py @@ -0,0 +1,48 @@ +"""Regression test pinning the nginx image tag in the shipped compose files. + +Both compose files deployed ``image: nginx:alpine`` — a floating tag. When the +``nginx:alpine`` alias moved to the 1.31.3 alpine build (musl 1.2.6), nginx's +pid-file ``pwrite()`` started issuing the ``pwritev2(RWF_NOAPPEND)`` syscall, +which the default seccomp profile of older Docker releases (and EOL kernels +such as CentOS 7) does not allowlist. The master then dies with ``pwrite() +"/run/nginx.pid" failed (1: Operation not permitted)`` and the container +restart-loops, so ``make docker-start`` / ``make up`` never become healthy +(#7615; upstream nginx/docker-nginx#1059 — the image cannot fix this, only a +pinned pre-musl-1.2.6 build or a host upgrade helps). + +This test pins the exact-tag policy so the alias (or any other floating tag) +cannot silently return. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[2] +COMPOSE_PATHS = { + "prod": REPO_ROOT / "docker" / "docker-compose.yaml", + "dev": REPO_ROOT / "docker" / "docker-compose-dev.yaml", +} + +# An exact tag carries a full major.minor.patch; `alpine` / `latest` / +# `1.31` style tags float and can re-resolve to a broken build overnight. +EXACT_TAG = re.compile(r"^[^:]+:\d+\.\d+\.\d+(-[a-z]+)?$") + + +def test_nginx_image_is_pinned_to_an_exact_tag() -> None: + for name, compose_path in COMPOSE_PATHS.items(): + compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) + image = (compose.get("services") or {}).get("nginx", {}).get("image") + assert isinstance(image, str) and image, f"{name}: nginx service must declare an image" + assert EXACT_TAG.match(image), f"{name}: nginx image {image!r} must pin an exact tag — a floating tag resolved to the musl-1.2.6 alpine build whose pwritev2 syscall older Docker seccomp profiles reject (#7615)" + + +def test_both_compose_files_pin_the_same_nginx_image() -> None: + images = set() + for compose_path in COMPOSE_PATHS.values(): + compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) + images.add((compose.get("services") or {}).get("nginx", {}).get("image")) + assert len(images) == 1, "prod and dev compose files must pin the same nginx image" diff --git a/docker/docker-compose-dev.yaml b/docker/docker-compose-dev.yaml index 422009db05..b3c2fecbc0 100644 --- a/docker/docker-compose-dev.yaml +++ b/docker/docker-compose-dev.yaml @@ -123,7 +123,13 @@ services: # ── Reverse Proxy ────────────────────────────────────────────────────── # Routes API traffic to gateway and (optionally) provisioner. nginx: - image: nginx:alpine + # Pinned tag, not `nginx:alpine`: from 1.31.3 the alpine builds ship musl + # 1.2.6, whose pwrite() issues pwritev2(RWF_NOAPPEND) — seccomp profiles + # on older Docker/EOL kernels reject that syscall and the master dies in a + # restart loop with `pwrite() "/run/nginx.pid" failed (1: Operation not + # permitted)` (#7615, nginx/docker-nginx#1059). 1.31.2 is the last musl + # 1.2.5 (pwrite64) alpine build. Keep in lockstep with docker-compose.yaml. + image: nginx:1.31.2-alpine container_name: deer-flow-nginx # Loopback-only by default; see the note in docker-compose.yaml. Override # with BIND_HOST when you deliberately need the dev stack reachable from diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index a474bed90d..54b77050d7 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -50,7 +50,13 @@ services: # ── Reverse Proxy ────────────────────────────────────────────────────────── nginx: - image: nginx:alpine + # Pinned tag, not `nginx:alpine`: from 1.31.3 the alpine builds ship musl + # 1.2.6, whose pwrite() issues pwritev2(RWF_NOAPPEND) — seccomp profiles + # on older Docker/EOL kernels reject that syscall and the master dies in a + # restart loop with `pwrite() "/run/nginx.pid" failed (1: Operation not + # permitted)` (#7615, nginx/docker-nginx#1059). 1.31.2 is the last musl + # 1.2.5 (pwrite64) alpine build. + image: nginx:1.31.2-alpine container_name: deer-flow-nginx # Loopback-only by default: DeerFlow's agent can execute commands, so the # documented default deployment is a local trusted environment. A bare