mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 13:40:37 +00:00
fix(scripts): bind local make dev and make start to loopback (#6587)
* fix(scripts): bind local make dev and make start to loopback serve.sh and backend/Makefile started the Gateway with --host 0.0.0.0, nginx.local.conf listened on every interface, and Next.js kept its all-interfaces default outside Windows, so a local stack on a LAN or VPN exposed ports 2026, 8001 and 3000, including /setup before the first admin existed. The Docker stack and the README already document a loopback-only default. Bind the Gateway and frontend to 127.0.0.1 and keep nginx on loopback unless BIND_HOST is set, the variable the Docker stack already honors. nginx-local-conf.sh renders a copy listening on BIND_HOST, and serve.sh resolves it before stopping anything so an invalid value cannot tear down a running stack. * docs(changelog): reference #6587 * docs(changelog): separate the loopback entry from the next bullet --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
ab9920f25f
commit
f8b419ab38
@ -27,9 +27,11 @@ UNBROWSE_API_KEY=your-unbrowse-api-key
|
||||
# Leave unset when using the unified nginx endpoint, e.g. http://localhost:2026.
|
||||
# GATEWAY_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
||||
|
||||
# Host interface the Docker stack publishes its entry port on. Defaults to
|
||||
# 127.0.0.1 (loopback only), matching the local-trusted-environment deployment
|
||||
# model documented in README.md -- the agent can execute commands.
|
||||
# Host interface the nginx entry port listens on, for the Docker stack and for
|
||||
# local `make dev` / `make start` (where the Gateway and frontend always stay on
|
||||
# loopback). Defaults to 127.0.0.1 (loopback only), matching the
|
||||
# local-trusted-environment deployment model documented in README.md -- the
|
||||
# agent can execute commands.
|
||||
# Set 0.0.0.0 only when the host is protected by your own TLS/auth front door
|
||||
# or firewall, and complete first-run setup before it becomes reachable.
|
||||
# BIND_HOST=0.0.0.0
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -46,6 +46,8 @@ coverage/
|
||||
skills/custom/*
|
||||
logs/
|
||||
log/
|
||||
# nginx temp dirs and the BIND_HOST-rendered nginx config from local runs
|
||||
/temp/
|
||||
debug.log
|
||||
|
||||
# Local git hooks (keep only on this machine, do not push)
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@ -4337,6 +4337,21 @@ This release closes that milestone with **439 merged pull requests**.
|
||||
package. Previously a `hooks/install.jse` carrying one stray byte received
|
||||
no static analysis and no executable review. ([#6321])
|
||||
|
||||
- **scripts:** Bind local `make dev` / `make start` to loopback. `serve.sh` and
|
||||
`backend/Makefile` started the Gateway with `--host 0.0.0.0`,
|
||||
`nginx.local.conf` listened on every interface, and Next.js kept its
|
||||
all-interfaces default outside Windows, so on a LAN or VPN other machines could
|
||||
reach ports `2026`, `8001`, and `3000`, including `/setup` before the first
|
||||
admin existed. The Docker stack and the README's deployment model were already
|
||||
loopback-only. The Gateway and frontend now bind `127.0.0.1`, and nginx
|
||||
listens on `127.0.0.1` and `[::1]` unless `BIND_HOST` is set, the same
|
||||
variable the Docker stack honors. An invalid `BIND_HOST` fails before any
|
||||
running service is stopped. ([#6587])
|
||||
|
||||
**Behavior change:** a local stack opened from another device needs
|
||||
`BIND_HOST` (e.g. `BIND_HOST=0.0.0.0` in `.env`) and the `2026` entry; the
|
||||
Gateway and frontend ports are no longer reachable from other machines.
|
||||
|
||||
- **skills:** Stop `review_skill_package` from reading other users' skills.
|
||||
Local path targets were allowed anywhere under the Gateway cwd or `/tmp`,
|
||||
and every documented deployment keeps `DEER_FLOW_HOME` under the cwd, so a
|
||||
@ -9480,6 +9495,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#6558]: https://github.com/bytedance/deer-flow/pull/6558
|
||||
[#6580]: https://github.com/bytedance/deer-flow/pull/6580
|
||||
[#6582]: https://github.com/bytedance/deer-flow/pull/6582
|
||||
[#6587]: https://github.com/bytedance/deer-flow/pull/6587
|
||||
[#6588]: https://github.com/bytedance/deer-flow/pull/6588
|
||||
[#6590]: https://github.com/bytedance/deer-flow/pull/6590
|
||||
[#6595]: https://github.com/bytedance/deer-flow/pull/6595
|
||||
|
||||
@ -3353,6 +3353,19 @@
|
||||
文件。此前带一个杂散字节的 `hooks/install.jse` 既得不到静态分析,也
|
||||
不经过可执行代码审查。([#6321])
|
||||
|
||||
- **脚本:** 本地 `make dev` / `make start` 改为绑定回环地址。此前 `serve.sh`
|
||||
和 `backend/Makefile` 以 `--host 0.0.0.0` 启动 Gateway,`nginx.local.conf`
|
||||
监听所有网卡,Next.js 在 Windows 之外也沿用监听所有网卡的默认值,因此在局域网
|
||||
或 VPN 中,其他机器可以访问 `2026`、`8001` 和 `3000` 端口,包括首个管理员创建
|
||||
之前的 `/setup`。Docker 部署栈和 README 的部署模型原本就只监听回环地址。现在
|
||||
Gateway 和前端绑定 `127.0.0.1`,nginx 监听 `127.0.0.1` 和 `[::1]`,除非设置了
|
||||
`BIND_HOST`(与 Docker 部署栈使用同一个变量)。`BIND_HOST` 无效时,会在停止任何
|
||||
正在运行的服务之前报错退出。([#6587])
|
||||
|
||||
**行为变更:** 需要从其他设备访问本地部署时,请设置 `BIND_HOST`(例如在 `.env`
|
||||
中设置 `BIND_HOST=0.0.0.0`)并使用 `2026` 入口;Gateway 和前端端口不再对其他
|
||||
机器开放。
|
||||
|
||||
- **技能:** `review_skill_package` 不再能读取其他用户的技能。此前本地路径
|
||||
目标只要位于 Gateway 工作目录或 `/tmp` 之下即被放行,而所有文档化的部署都把
|
||||
`DEER_FLOW_HOME` 放在工作目录之下,因此知道他人用户 id 的用户可以传入
|
||||
@ -7636,6 +7649,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#6556]: https://github.com/bytedance/deer-flow/pull/6556
|
||||
[#6580]: https://github.com/bytedance/deer-flow/pull/6580
|
||||
[#6582]: https://github.com/bytedance/deer-flow/pull/6582
|
||||
[#6587]: https://github.com/bytedance/deer-flow/pull/6587
|
||||
[#6588]: https://github.com/bytedance/deer-flow/pull/6588
|
||||
[#6590]: https://github.com/bytedance/deer-flow/pull/6590
|
||||
[#6595]: https://github.com/bytedance/deer-flow/pull/6595
|
||||
|
||||
11
README.md
11
README.md
@ -2914,10 +2914,13 @@ persisted injected instruction.
|
||||
|
||||
### Deployment Defaults
|
||||
|
||||
The Docker stack publishes its entry port on `127.0.0.1` only, matching the
|
||||
local-trusted-environment model described above. To reach it from another
|
||||
machine, set `BIND_HOST` in `.env` (e.g. `BIND_HOST=0.0.0.0`) — and only after
|
||||
putting the security measures below in place.
|
||||
The Docker stack publishes its entry port on `127.0.0.1` only, and local
|
||||
`make dev` / `make start` bind nginx, the Gateway, and the frontend to loopback,
|
||||
matching the local-trusted-environment model described above. To reach it from
|
||||
another machine, set `BIND_HOST` in `.env` (e.g. `BIND_HOST=0.0.0.0`) — and only
|
||||
after putting the security measures below in place. Local runs apply `BIND_HOST`
|
||||
to nginx on port `2026` only; the Gateway and frontend stay on loopback behind
|
||||
it.
|
||||
|
||||
**Complete first-run setup before the host becomes reachable.** A fresh
|
||||
instance has no accounts yet, so create the admin account through `/setup`
|
||||
|
||||
@ -1653,7 +1653,7 @@ O PoC não faz limpeza automática. Concluída a verificação, exclua o chat de
|
||||
|
||||
### Padrões de implantação
|
||||
|
||||
A stack Docker publica a porta de entrada apenas em `127.0.0.1`, de acordo com o modelo de ambiente local confiável descrito acima. Para acessá-la de outra máquina, defina `BIND_HOST` no `.env` (por exemplo `BIND_HOST=0.0.0.0`), e só depois de adotar as medidas de segurança abaixo.
|
||||
A stack Docker publica a porta de entrada apenas em `127.0.0.1`, e o `make dev` / `make start` local vinculam o nginx, o Gateway e o frontend ao loopback, de acordo com o modelo de ambiente local confiável descrito acima. Para acessá-la de outra máquina, defina `BIND_HOST` no `.env` (por exemplo `BIND_HOST=0.0.0.0`), e só depois de adotar as medidas de segurança abaixo. Na execução local, `BIND_HOST` vale apenas para o nginx na porta `2026`; o Gateway e o frontend continuam no loopback, atrás dele.
|
||||
|
||||
**Conclua a configuração de primeiro uso antes que o host fique acessível.** Uma instância nova ainda não tem contas, então crie a conta de admin por `/setup` logo depois de iniciar qualquer implantação que não seja só de loopback.
|
||||
|
||||
|
||||
@ -1104,7 +1104,7 @@ PoC 不会自动清理。验证完成后,请用网页侧边栏的删除操作
|
||||
|
||||
### 部署默认值
|
||||
|
||||
Docker 部署栈默认只把入口端口发布在 `127.0.0.1` 上,与上文所述的本地可信环境模型一致。若需要从其他机器访问,请在 `.env` 中设置 `BIND_HOST`(例如 `BIND_HOST=0.0.0.0`),并且必须在落实下方的安全措施之后再这样做。
|
||||
Docker 部署栈默认只把入口端口发布在 `127.0.0.1` 上,本地 `make dev` / `make start` 也把 nginx、Gateway 和前端绑定在回环地址上,与上文所述的本地可信环境模型一致。若需要从其他机器访问,请在 `.env` 中设置 `BIND_HOST`(例如 `BIND_HOST=0.0.0.0`),并且必须在落实下方的安全措施之后再这样做。本地运行时 `BIND_HOST` 只作用于 `2026` 端口上的 nginx,Gateway 和前端始终留在其后的回环地址上。
|
||||
|
||||
**请在主机变为可访问之前完成首次初始化设置。** 全新实例尚未创建任何账号,因此对于任何非仅回环访问的部署,请在启动后立即通过 `/setup` 创建管理员账号。
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ install:
|
||||
|
||||
dev:
|
||||
mkdir -p "$(DEER_FLOW_HOME)" "$(BACKEND_SANDBOX_HOME)"
|
||||
PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 DEER_FLOW_HOME="$(DEER_FLOW_HOME)" uv run --locked uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001 \
|
||||
PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 DEER_FLOW_HOME="$(DEER_FLOW_HOME)" uv run --locked uvicorn app.gateway.app:app --host 127.0.0.1 --port 8001 \
|
||||
--reload \
|
||||
--reload-include='*.yaml' \
|
||||
--reload-include='.env' \
|
||||
@ -17,7 +17,7 @@ dev:
|
||||
--reload-exclude="$(DEER_FLOW_HOME)"
|
||||
|
||||
gateway:
|
||||
PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run --locked uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001
|
||||
PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run --locked uvicorn app.gateway.app:app --host 127.0.0.1 --port 8001
|
||||
|
||||
# Run all four duration-balanced shards locally; wait for every shard on failure.
|
||||
TEST_JOBS ?= 4
|
||||
|
||||
163
backend/tests/test_local_dev_loopback_bind.py
Normal file
163
backend/tests/test_local_dev_loopback_bind.py
Normal file
@ -0,0 +1,163 @@
|
||||
"""Regression test for the local (non-Docker) launch bind addresses.
|
||||
|
||||
``README.md`` documents DeerFlow as deployed by default "in a local trusted
|
||||
environment (accessible only via the 127.0.0.1 loopback interface)", and the
|
||||
Docker stack publishes only its nginx entry, on ``${BIND_HOST:-127.0.0.1}``
|
||||
(``test_compose_default_bind_host.py``). ``make dev`` / ``make start`` instead
|
||||
started the Gateway with ``--host 0.0.0.0``, nginx with ``listen 2026`` and the
|
||||
frontend on Next's ``0.0.0.0`` default, so on a LAN or VPN every one of those
|
||||
ports, including ``/setup`` before the first admin exists, was reachable from
|
||||
other machines.
|
||||
|
||||
Gateway and frontend now always bind loopback; nginx listens on loopback unless
|
||||
``BIND_HOST`` opts in, the same variable the Docker stack honors.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from support.shell import require_script_bash
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SERVE_SH = REPO_ROOT / "scripts" / "serve.sh"
|
||||
NGINX_SH = REPO_ROOT / "scripts" / "nginx.sh"
|
||||
NGINX_LOCAL_CONF_SH = REPO_ROOT / "scripts" / "nginx-local-conf.sh"
|
||||
NGINX_LOCAL_CONF = REPO_ROOT / "docker" / "nginx" / "nginx.local.conf"
|
||||
BACKEND_MAKEFILE = REPO_ROOT / "backend" / "Makefile"
|
||||
|
||||
|
||||
def _listen_directives(conf: str) -> list[str]:
|
||||
return re.findall(r"^\s*listen\s+([^;]+);", conf, re.M)
|
||||
|
||||
|
||||
# ── Launch commands ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", [SERVE_SH, BACKEND_MAKEFILE], ids=["serve.sh", "backend-Makefile"])
|
||||
def test_gateway_launch_commands_bind_loopback(path: Path):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
launches = [line for line in text.splitlines() if "uvicorn app.gateway.app:app --" in line]
|
||||
|
||||
assert launches, f"no Gateway launch found in {path.name}"
|
||||
for line in launches:
|
||||
assert "--host 127.0.0.1" in line, f"{path.name} must start the Gateway on loopback: {line.strip()}"
|
||||
|
||||
|
||||
def test_frontend_launch_commands_bind_loopback():
|
||||
"""Every serve.sh frontend mode must pass a loopback --hostname to Next.
|
||||
|
||||
``frontend/scripts/dev.mjs`` keeps Next's all-interfaces default off Windows
|
||||
because the Docker dev frontend container needs it, so serve.sh passes the
|
||||
hostname itself.
|
||||
"""
|
||||
commands = re.findall(r"^\s*FRONTEND_CMD=(.*)$", SERVE_SH.read_text(encoding="utf-8"), re.M)
|
||||
|
||||
assert len(commands) == 3, f"expected dev, start and preview frontend commands; got {commands}"
|
||||
for command in commands:
|
||||
assert "--hostname 127.0.0.1" in command, f"frontend command must bind loopback: {command}"
|
||||
|
||||
|
||||
def test_local_nginx_config_listens_on_loopback_only():
|
||||
assert _listen_directives(NGINX_LOCAL_CONF.read_text(encoding="utf-8")) == ["127.0.0.1:2026", "[::1]:2026"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", [SERVE_SH, NGINX_SH], ids=["serve.sh", "nginx.sh"])
|
||||
def test_nginx_launchers_use_the_bind_host_aware_config(path: Path):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
|
||||
assert 'NGINX_CONF="$(bash "$REPO_ROOT/scripts/nginx-local-conf.sh")"' in text
|
||||
launches = [line for line in text.splitlines() if "daemon off;" in line]
|
||||
assert launches and all("$NGINX_CONF" in line for line in launches), launches
|
||||
|
||||
|
||||
def test_serve_resolves_nginx_config_before_stopping_services():
|
||||
"""An invalid BIND_HOST must fail before restart/start tear down a running stack."""
|
||||
serve = SERVE_SH.read_text(encoding="utf-8")
|
||||
|
||||
assert serve.index("scripts/nginx-local-conf.sh") < serve.index('if [ "$ACTION" = "restart" ]; then')
|
||||
|
||||
|
||||
# ── nginx-local-conf.sh ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def checkout(tmp_path: Path) -> Path:
|
||||
"""A minimal checkout: the script resolves the repo root from its own location."""
|
||||
(tmp_path / "scripts").mkdir()
|
||||
(tmp_path / "docker" / "nginx").mkdir(parents=True)
|
||||
shutil.copy2(NGINX_LOCAL_CONF_SH, tmp_path / "scripts" / NGINX_LOCAL_CONF_SH.name)
|
||||
shutil.copy2(NGINX_LOCAL_CONF, tmp_path / "docker" / "nginx" / NGINX_LOCAL_CONF.name)
|
||||
return tmp_path
|
||||
|
||||
|
||||
def _run(checkout: Path, bind_host: str | None) -> subprocess.CompletedProcess[str]:
|
||||
env = {key: value for key, value in os.environ.items() if key != "BIND_HOST"}
|
||||
if bind_host is not None:
|
||||
env["BIND_HOST"] = bind_host
|
||||
return subprocess.run(
|
||||
[require_script_bash(), str(checkout / "scripts" / NGINX_LOCAL_CONF_SH.name)],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bind_host", [None, "", "127.0.0.1"], ids=["unset", "blank", "loopback"])
|
||||
def test_default_bind_host_uses_the_tracked_config(checkout: Path, bind_host: str | None):
|
||||
result = _run(checkout, bind_host)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert Path(result.stdout.strip()) == (checkout / "docker" / "nginx" / "nginx.local.conf").resolve()
|
||||
assert not (checkout / "temp").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("bind_host", "expected"),
|
||||
[
|
||||
("0.0.0.0", ["2026", "[::]:2026"]),
|
||||
("192.0.2.10", ["192.0.2.10:2026"]),
|
||||
("fd00::1", ["[fd00::1]:2026"]),
|
||||
("[fd00::1]", ["[fd00::1]:2026"]),
|
||||
],
|
||||
)
|
||||
def test_bind_host_renders_a_config_listening_there(checkout: Path, bind_host: str, expected: list[str]):
|
||||
result = _run(checkout, bind_host)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
rendered = Path(result.stdout.strip())
|
||||
assert rendered == (checkout / "temp" / "nginx.local.conf").resolve()
|
||||
content = rendered.read_text(encoding="utf-8")
|
||||
assert _listen_directives(content) == expected
|
||||
|
||||
# Only the listen directives change.
|
||||
def without_listen(text: str) -> list[str]:
|
||||
return [line for line in text.splitlines() if not line.strip().startswith("listen ")]
|
||||
|
||||
assert without_listen(content) == without_listen(NGINX_LOCAL_CONF.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bind_host", ["0.0.0.0; return 200", "a b", "$(id)"])
|
||||
def test_bind_host_rejects_values_that_are_not_addresses(checkout: Path, bind_host: str):
|
||||
result = _run(checkout, bind_host)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "BIND_HOST must be an IP address or hostname" in result.stderr
|
||||
assert not (checkout / "temp" / "nginx.local.conf").exists()
|
||||
|
||||
|
||||
def test_render_fails_loudly_when_the_listen_directive_drifts(checkout: Path):
|
||||
conf = checkout / "docker" / "nginx" / "nginx.local.conf"
|
||||
conf.write_text(conf.read_text(encoding="utf-8").replace("listen 127.0.0.1:2026;", "listen 127.0.0.1:2027;"), encoding="utf-8")
|
||||
|
||||
result = _run(checkout, "0.0.0.0")
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "Could not find the loopback listen directive" in result.stderr
|
||||
assert not (checkout / "temp" / "nginx.local.conf").exists()
|
||||
@ -126,7 +126,7 @@ def test_websocket_handshake_is_not_redirected(config_path: str, host: str) -> N
|
||||
@pytest.mark.parametrize("config_path", NGINX_CONFIGS)
|
||||
def test_main_server_redirects_to_the_canonical_origin(config_path: str) -> None:
|
||||
content = read_config(config_path)
|
||||
server = _extract_block(content, re.compile(r"server\s*\{(?=[^}]*?listen\s+(?:\[::\]:)?2026\b)"))
|
||||
server = _extract_block(content, re.compile(r"server\s*\{(?=[^}]*?listen\s+(?:\S+:)?2026\b)"))
|
||||
assert 'if ($loopback_origin != "")' in server
|
||||
assert "return 301 $scheme://$loopback_origin$request_uri;" in server
|
||||
|
||||
|
||||
@ -150,8 +150,8 @@ def test_official_entrypoints_route_pnpm_through_shared_runner():
|
||||
assert "PNPM = $(PYTHON) ../scripts/pnpm.py" in frontend_makefile
|
||||
assert '"$DEERFLOW_PNPM_PYTHON" "$DEERFLOW_PNPM_RUNNER" install --silent' in serve_script
|
||||
assert 'DEERFLOW_PNPM_RUNNER="$REPO_ROOT/scripts/pnpm.py"' in serve_script
|
||||
assert 'FRONTEND_CMD=\'env PORT=3000 "$DEERFLOW_PNPM_PYTHON" "$DEERFLOW_PNPM_RUNNER" run dev\'' in serve_script
|
||||
assert '"\\$DEERFLOW_PNPM_RUNNER\\" run preview"' in serve_script
|
||||
assert 'FRONTEND_CMD=\'env PORT=3000 "$DEERFLOW_PNPM_PYTHON" "$DEERFLOW_PNPM_RUNNER" run dev --hostname 127.0.0.1\'' in serve_script
|
||||
assert '"\\$DEERFLOW_PNPM_RUNNER\\" run preview --hostname 127.0.0.1"' in serve_script
|
||||
assert 'Path(__file__).resolve().with_name("pnpm.py")' in doctor_script
|
||||
assert 'project_root / "scripts" / "pnpm.py"' in support_bundle_script
|
||||
|
||||
|
||||
@ -81,6 +81,18 @@ def test_repo_nginx_pid_accepts_macos_rewritten_master_command(tmp_path):
|
||||
)
|
||||
|
||||
|
||||
def test_repo_nginx_pid_accepts_bind_host_rendered_config(tmp_path):
|
||||
# With BIND_HOST set, nginx-local-conf.sh starts nginx on temp/nginx.local.conf.
|
||||
repo_root = (tmp_path / "deer-flow").as_posix()
|
||||
nginx_conf = f"{repo_root}/temp/nginx.local.conf"
|
||||
|
||||
assert _is_repo_nginx_pid(
|
||||
command="nginx",
|
||||
args=f"nginx: master process nginx -g daemon off; -c {nginx_conf} -p {repo_root}",
|
||||
repo_root=repo_root,
|
||||
)
|
||||
|
||||
|
||||
def test_repo_nginx_pid_accepts_macos_rewritten_worker_after_repo_check(tmp_path):
|
||||
repo_root = tmp_path / "deer-flow"
|
||||
|
||||
|
||||
@ -74,8 +74,11 @@ http {
|
||||
}
|
||||
|
||||
server {
|
||||
listen 2026;
|
||||
listen [::]:2026;
|
||||
# Loopback only, like the Docker stack's default BIND_HOST. Setting
|
||||
# BIND_HOST makes scripts/nginx-local-conf.sh render a copy that
|
||||
# listens on that address instead.
|
||||
listen 127.0.0.1:2026;
|
||||
listen [::1]:2026;
|
||||
server_name _;
|
||||
|
||||
# Canonicalize the loopback origin (#6156); see the maps above.
|
||||
|
||||
@ -44,7 +44,9 @@ This lets standard LangGraph SDK clients talk to DeerFlow without a separate Lan
|
||||
server. Both compose files publish nginx as `"${BIND_HOST:-127.0.0.1}:${PORT:-2026}:2026"`
|
||||
— **loopback by default**; the Gateway's `8001` is never published. Any new published port
|
||||
must carry an explicit bind address (`backend/tests/test_compose_default_bind_host.py`
|
||||
pins this for every service in both compose files).
|
||||
pins this for every service in both compose files). Local `make dev` / `make start` follow
|
||||
the same model: the Gateway and frontend bind `127.0.0.1`, and nginx listens on loopback
|
||||
unless `BIND_HOST` is set (`backend/tests/test_local_dev_loopback_bind.py`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -55,7 +55,12 @@ including after an Apple Container failure, and retain the final image-config
|
||||
note rather than exiting early on Apple Container success.
|
||||
|
||||
The root `PORT` value configures Docker's published nginx ingress only; local
|
||||
orchestration pins Next.js to `3000`. Runtime commands launch from the already
|
||||
orchestration pins Next.js to `3000`. Local runs bind the Gateway and Next.js to
|
||||
`127.0.0.1`; `nginx-local-conf.sh` keeps nginx on loopback or, for a set
|
||||
`BIND_HOST`, renders `temp/nginx.local.conf` listening there. `serve.sh`
|
||||
resolves it before stopping anything, so a bad value cannot tear down a running
|
||||
stack. Keep `dev.mjs`'s all-interfaces default: the Docker dev frontend needs
|
||||
it. Runtime commands launch from the already
|
||||
synchronized environment with `uv run --no-sync`. Production Compose probes
|
||||
Gateway `/health`, and `deploy.sh` waits for all services before reporting
|
||||
success; failures print Compose status and recent Gateway logs.
|
||||
|
||||
62
scripts/nginx-local-conf.sh
Executable file
62
scripts/nginx-local-conf.sh
Executable file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# nginx-local-conf.sh — Print the nginx config path for local (non-Docker) runs.
|
||||
#
|
||||
# docker/nginx/nginx.local.conf listens on loopback only, matching the Docker
|
||||
# stack's default BIND_HOST. When BIND_HOST names another address, render a
|
||||
# copy under temp/ that listens there instead and print that path, so local
|
||||
# runs honor the same opt-in as the Docker stack. Gateway and frontend stay on
|
||||
# loopback either way: nginx is the only entry point.
|
||||
#
|
||||
# Usage: NGINX_CONF="$(bash ./scripts/nginx-local-conf.sh)"
|
||||
|
||||
set -e
|
||||
|
||||
REPO_ROOT="$(builtin cd "$(dirname "${BASH_SOURCE[0]}")/.." >/dev/null 2>&1 && pwd -P)"
|
||||
SOURCE_CONF="$REPO_ROOT/docker/nginx/nginx.local.conf"
|
||||
RENDERED_CONF="$REPO_ROOT/temp/nginx.local.conf"
|
||||
|
||||
bind_host="${BIND_HOST:-127.0.0.1}"
|
||||
if [ "$bind_host" = "127.0.0.1" ]; then
|
||||
printf '%s\n' "$SOURCE_CONF"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The value is written into an nginx directive, so accept address and hostname
|
||||
# characters only.
|
||||
bind_host="${bind_host#[}"
|
||||
bind_host="${bind_host%]}"
|
||||
case "$bind_host" in
|
||||
''|*[!A-Za-z0-9.:_-]*)
|
||||
echo "BIND_HOST must be an IP address or hostname: ${BIND_HOST}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$bind_host" in
|
||||
0.0.0.0) listen_lines='listen 2026;\nlisten [::]:2026;' ;;
|
||||
*:*) listen_lines="listen [$bind_host]:2026;" ;;
|
||||
*) listen_lines="listen $bind_host:2026;" ;;
|
||||
esac
|
||||
|
||||
# awk -v expands the \n separator; BSD awk rejects a literal newline there.
|
||||
mkdir -p "$REPO_ROOT/temp"
|
||||
awk -v listen_lines="$listen_lines" '
|
||||
/^[[:space:]]*listen 127\.0\.0\.1:2026;[[:space:]]*$/ {
|
||||
match($0, /^[[:space:]]*/)
|
||||
indent = substr($0, 1, RLENGTH)
|
||||
n = split(listen_lines, lines, "\n")
|
||||
for (i = 1; i <= n; i++) print indent lines[i]
|
||||
replaced++
|
||||
next
|
||||
}
|
||||
/^[[:space:]]*listen \[::1\]:2026;[[:space:]]*$/ { next }
|
||||
{ print }
|
||||
END { if (replaced != 1) exit 1 }
|
||||
' "$SOURCE_CONF" > "$RENDERED_CONF.tmp" || {
|
||||
rm -f "$RENDERED_CONF.tmp"
|
||||
echo "Could not find the loopback listen directive in $SOURCE_CONF" >&2
|
||||
exit 1
|
||||
}
|
||||
mv "$RENDERED_CONF.tmp" "$RENDERED_CONF"
|
||||
printf '%s\n' "$RENDERED_CONF"
|
||||
@ -15,4 +15,5 @@ cd "$REPO_ROOT"
|
||||
mkdir -p logs
|
||||
mkdir -p temp/client_body_temp temp/proxy_temp temp/fastcgi_temp temp/uwsgi_temp temp/scgi_temp
|
||||
|
||||
exec nginx -g 'daemon off;' -c "$REPO_ROOT/docker/nginx/nginx.local.conf" -p "$REPO_ROOT"
|
||||
NGINX_CONF="$(bash "$REPO_ROOT/scripts/nginx-local-conf.sh")"
|
||||
exec nginx -g 'daemon off;' -c "$NGINX_CONF" -p "$REPO_ROOT"
|
||||
|
||||
@ -290,6 +290,13 @@ if [ "$ACTION" != "stop" ] && ! $DEV_MODE && $SKIP_FRONTEND_BUILD; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Resolve the nginx config before any stop_all too: an invalid BIND_HOST must
|
||||
# not tear down a running stack. Gateway and frontend always bind loopback;
|
||||
# nginx is the only entry point, as in the Docker stack.
|
||||
if [ "$ACTION" != "stop" ]; then
|
||||
NGINX_CONF="$(bash "$REPO_ROOT/scripts/nginx-local-conf.sh")" || exit 1
|
||||
fi
|
||||
|
||||
# ── Action routing ───────────────────────────────────────────────────────────
|
||||
|
||||
if [ "$ACTION" = "stop" ]; then
|
||||
@ -326,15 +333,15 @@ export DEERFLOW_PNPM_PYTHON DEERFLOW_PNPM_RUNNER
|
||||
|
||||
# Frontend command
|
||||
if $DEV_MODE; then
|
||||
FRONTEND_CMD='env PORT=3000 "$DEERFLOW_PNPM_PYTHON" "$DEERFLOW_PNPM_RUNNER" run dev'
|
||||
FRONTEND_CMD='env PORT=3000 "$DEERFLOW_PNPM_PYTHON" "$DEERFLOW_PNPM_RUNNER" run dev --hostname 127.0.0.1'
|
||||
if $SKIP_FRONTEND_BUILD; then
|
||||
echo " Note: --skip-frontend-build is ignored in dev mode (next dev does not build)."
|
||||
fi
|
||||
elif $SKIP_FRONTEND_BUILD; then
|
||||
# The BUILD_ID preflight above already guarantees a reusable build exists.
|
||||
FRONTEND_CMD="env PORT=3000 BETTER_AUTH_SECRET=$($DEERFLOW_PNPM_PYTHON -c 'import secrets; print(secrets.token_hex(16))') \"\$DEERFLOW_PNPM_PYTHON\" \"\$DEERFLOW_PNPM_RUNNER\" run start"
|
||||
FRONTEND_CMD="env PORT=3000 BETTER_AUTH_SECRET=$($DEERFLOW_PNPM_PYTHON -c 'import secrets; print(secrets.token_hex(16))') \"\$DEERFLOW_PNPM_PYTHON\" \"\$DEERFLOW_PNPM_RUNNER\" run start --hostname 127.0.0.1"
|
||||
else
|
||||
FRONTEND_CMD="env PORT=3000 BETTER_AUTH_SECRET=$($DEERFLOW_PNPM_PYTHON -c 'import secrets; print(secrets.token_hex(16))') \"\$DEERFLOW_PNPM_PYTHON\" \"\$DEERFLOW_PNPM_RUNNER\" run preview"
|
||||
FRONTEND_CMD="env PORT=3000 BETTER_AUTH_SECRET=$($DEERFLOW_PNPM_PYTHON -c 'import secrets; print(secrets.token_hex(16))') \"\$DEERFLOW_PNPM_PYTHON\" \"\$DEERFLOW_PNPM_RUNNER\" run preview --hostname 127.0.0.1"
|
||||
fi
|
||||
|
||||
# Runtime path defaults. Local `make dev` launches Gateway from `backend/`,
|
||||
@ -495,7 +502,7 @@ mkdir -p temp/client_body_temp temp/proxy_temp temp/fastcgi_temp temp/uwsgi_temp
|
||||
|
||||
# 1. Gateway API
|
||||
run_service "Gateway" \
|
||||
"cd backend && PYTHONPATH=. uv run --no-sync uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001 $GATEWAY_EXTRA_FLAGS > ../logs/gateway.log 2>&1" \
|
||||
"cd backend && PYTHONPATH=. uv run --no-sync uvicorn app.gateway.app:app --host 127.0.0.1 --port 8001 $GATEWAY_EXTRA_FLAGS > ../logs/gateway.log 2>&1" \
|
||||
8001 30
|
||||
|
||||
# 2. Frontend
|
||||
@ -505,7 +512,7 @@ run_service "Frontend" \
|
||||
|
||||
# 3. Nginx
|
||||
run_service "Nginx" \
|
||||
"nginx -g 'daemon off;' -c '$REPO_ROOT/docker/nginx/nginx.local.conf' -p '$REPO_ROOT' > logs/nginx.log 2>&1" \
|
||||
"nginx -g 'daemon off;' -c '$NGINX_CONF' -p '$REPO_ROOT' > logs/nginx.log 2>&1" \
|
||||
2026 10
|
||||
|
||||
# ── Ready ────────────────────────────────────────────────────────────────────
|
||||
@ -516,6 +523,10 @@ echo " ✓ DeerFlow is running! [$MODE_LABEL]"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo " 🌐 http://localhost:2026"
|
||||
if [ "${BIND_HOST:-127.0.0.1}" != "127.0.0.1" ]; then
|
||||
echo " ⚠ Nginx also accepts connections on BIND_HOST=$BIND_HOST."
|
||||
echo " Complete first-run setup at /setup before other machines can reach it."
|
||||
fi
|
||||
echo ""
|
||||
echo " Routing: Frontend → Nginx → Gateway"
|
||||
echo " API: /api/langgraph/* → Gateway agent runtime"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user