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:
Hyeonsang Cho 2026-10-10 16:47:30 +09:00 committed by GitHub
parent ab9920f25f
commit f8b419ab38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 320 additions and 24 deletions

View File

@ -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
View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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`

View File

@ -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.

View File

@ -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` 创建管理员账号。

View File

@ -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

View 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()

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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.

View File

@ -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`).
---

View File

@ -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
View 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"

View File

@ -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"

View File

@ -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"