From 1dfd307c925c9e91f1de6b1d50b23052169c9a96 Mon Sep 17 00:00:00 2001 From: Hyeonsang Cho Date: Sun, 27 Sep 2026 08:36:35 +0900 Subject: [PATCH] fix(docker): strip the IPv6 listen in the production nginx launcher too (#5900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(docker): strip the IPv6 listen in the production nginx launcher too docker/nginx/nginx.conf listens on 2026 and [::]:2026. On a host booted with ipv6.disable=1 (or a kernel without IPv6) nginx refuses to start — socket() [::]:2026 failed (97: Address family not supported by protocol) — so the container restart-loops under `restart: unless-stopped` and `make up` waits on a readiness probe that never turns healthy. #2027 added a launcher guard that deletes the IPv6 listen when /proc/net/if_inet6 is absent, but only to docker-compose-dev.yaml; the Helm chart mirrors it; docker-compose.yaml — the file behind `make up` and scripts/deploy.sh — still ran the bare copy-and-start command. Give the production nginx service the same launcher as the dev file: `set -e`, copy the template, guard, and `exec nginx` so nginx is PID 1 and receives docker stop's signal directly. The new test pins the guard in all three launchers (prod, dev, Helm), requires the three sed patterns to be identical so they cannot drift again, checks the guard edits the file nginx actually loads, and applies the pattern to the shipped compose and Helm configs to confirm it removes only the IPv6 listen. Verified on nginx:alpine: BusyBox sed strips exactly that line and `nginx -t` accepts the result; the rendered production launcher runs unchanged when IPv6 is present. * docs(changelog): reference #5900 in the nginx IPv6 guard entries --- CHANGELOG.md | 9 ++ CHANGELOG_zh.md | 7 ++ .../tests/test_compose_nginx_ipv6_guard.py | 115 ++++++++++++++++++ docker/docker-compose.yaml | 17 ++- 4 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 backend/tests/test_compose_nginx_ipv6_guard.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e0e4b1035c..dfefa9b9e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -323,6 +323,14 @@ This release closes that milestone with **181 merged pull requests**. ### Fixed +- **docker:** The production stack (`make up` / `scripts/deploy.sh`) now starts + on hosts with IPv6 disabled. `docker/nginx/nginx.conf` also listens on + `[::]:2026`; on a kernel booted with `ipv6.disable=1` that listen makes nginx + exit at startup, so the container restart-looped and `make up` never became + healthy. The dev compose file has stripped the IPv6 listen when + `/proc/net/if_inet6` is absent since #2027, and the Helm chart mirrors it; + the production compose file was the one launcher still without the guard. + It now uses the same launcher, and starts nginx with `exec` so it is PID 1. ([#5900]) - **skills:** `skill_manage(action="remove_file")` and `write_file` now work on binary support files, and reject directories cleanly. A `.skill` archive may carry `assets/logo.png` (the installer only rejects *executable* binaries), @@ -6120,4 +6128,5 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5879]: https://github.com/bytedance/deer-flow/pull/5879 [#5881]: https://github.com/bytedance/deer-flow/pull/5881 [#5893]: https://github.com/bytedance/deer-flow/pull/5893 +[#5900]: https://github.com/bytedance/deer-flow/pull/5900 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index bca5d78285..88fc98341f 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -328,6 +328,12 @@ ### 修复 +- **Docker:** 生产栈(`make up` / `scripts/deploy.sh`)现在可以在禁用 IPv6 的主机上启动。 + `docker/nginx/nginx.conf` 同时监听 `[::]:2026`;在以 `ipv6.disable=1` 启动的内核上,这条监听会让 + nginx 在启动时退出,容器因此反复重启,`make up` 永远无法就绪。开发用 compose 文件自 #2027 起 + 会在 `/proc/net/if_inet6` 不存在时去掉 IPv6 监听,Helm chart 也做了同样处理;生产用 compose + 文件是唯一还没有这层保护的启动器。现在它使用相同的启动脚本,并以 `exec` 启动 nginx 使其成为 + PID 1。([#5900]) - **技能:** `skill_manage(action="remove_file")` 与 `write_file` 现在可以处理二进制支持文件, 并会干净地拒绝目录。`.skill` 压缩包可以包含 `assets/logo.png`(安装器只拒绝*可执行*二进制), 但这两个操作在改动文件之前会先把原有内容按 UTF-8 文本读出(仅用于历史记录),于是二进制 @@ -5205,3 +5211,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5879]: https://github.com/bytedance/deer-flow/pull/5879 [#5881]: https://github.com/bytedance/deer-flow/pull/5881 [#5893]: https://github.com/bytedance/deer-flow/pull/5893 +[#5900]: https://github.com/bytedance/deer-flow/pull/5900 diff --git a/backend/tests/test_compose_nginx_ipv6_guard.py b/backend/tests/test_compose_nginx_ipv6_guard.py new file mode 100644 index 0000000000..9b4e12e7ff --- /dev/null +++ b/backend/tests/test_compose_nginx_ipv6_guard.py @@ -0,0 +1,115 @@ +"""Every nginx launcher must survive a host with IPv6 disabled. + +``docker/nginx/nginx.conf`` listens on both ``2026`` and ``[::]:2026``. On a +host booted with ``ipv6.disable=1`` (or a kernel without IPv6) nginx refuses to +start — ``socket() [::]:2026 failed (97: Address family not supported by +protocol)`` — and a ``restart: unless-stopped`` container loops until the +``--wait`` readiness check gives up. #2027 added a launcher guard that strips +the IPv6 ``listen`` when ``/proc/net/if_inet6`` is absent, but only to the dev +compose file; the Helm chart mirrors it; the production compose file used by +``make up`` / ``scripts/deploy.sh`` did not. These tests pin the guard in every +launcher, keep the three copies identical, and check the pattern really +removes the IPv6 line from the config it is applied to. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest +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", +} +HELM_NGINX_DEPLOYMENT = REPO_ROOT / "deploy" / "helm" / "deer-flow" / "templates" / "nginx-deployment.yaml" +NGINX_CONFS = { + "compose": REPO_ROOT / "docker" / "nginx" / "nginx.conf", + "helm": REPO_ROOT / "deploy" / "helm" / "deer-flow" / "templates" / "configmap-nginx.yaml", +} + +GUARD_RE = re.compile(r"test -e /proc/net/if_inet6 \|\| sed -i '(?P[^']+)' (?P\S+)") + + +def _compose_nginx_script(compose_path: Path) -> str: + compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) + command = compose["services"]["nginx"]["command"] + if isinstance(command, list): + assert command[:2] == ["sh", "-c"], f"{compose_path.name}: nginx command must be `sh -c