diff --git a/AGENTS.md b/AGENTS.md
index 3b1a1e496..84dbadfa4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -124,7 +124,7 @@ make extension-enable NAME=... # Enable an installed extension (restart requ
make extension-disable NAME=... # Disable without uninstalling (restart required)
make extension-remove NAME=... # Remove package and config entry (restart required)
make dev # Start all services with hot-reload (Gateway + Frontend + Nginx)
-make start # Start all services in production mode (local, optimized)
+make start # Start all services in production mode (local, optimized); SKIP_FRONTEND_BUILD=1 reuses the last frontend build
make stop # Stop all running services
make up / down # Build/stop the production Docker stack (browser at localhost:2026)
make docker-start / docker-stop / docker-logs # Docker development environment
diff --git a/Makefile b/Makefile
index b1c8fc9a7..687baf467 100644
--- a/Makefile
+++ b/Makefile
@@ -137,10 +137,12 @@ dev:
@$(PYTHON) ./scripts/check.py
@$(RUN_WITH_GIT_BASH) ./scripts/serve.sh --dev
-# Start all services in production mode (with optimizations)
+# Start all services in production mode (with optimizations).
+# SKIP_FRONTEND_BUILD=1 reuses the existing frontend build instead of running
+# `next build`; see scripts/serve.sh --skip-frontend-build.
start:
@$(PYTHON) ./scripts/check.py
- @$(RUN_WITH_GIT_BASH) ./scripts/serve.sh --prod
+ @$(RUN_WITH_GIT_BASH) ./scripts/serve.sh --prod $(if $(filter 1,$(SKIP_FRONTEND_BUILD)),--skip-frontend-build)
# Start all services in daemon mode (background)
dev-daemon:
@@ -150,7 +152,7 @@ dev-daemon:
# Start prod services in daemon mode (background)
start-daemon:
@$(PYTHON) ./scripts/check.py
- @$(RUN_WITH_GIT_BASH) ./scripts/serve.sh --prod --daemon
+ @$(RUN_WITH_GIT_BASH) ./scripts/serve.sh --prod --daemon $(if $(filter 1,$(SKIP_FRONTEND_BUILD)),--skip-frontend-build)
# Start nginx alone in the foreground with the local dev config
nginx:
diff --git a/README.md b/README.md
index 00dee1aeb..d688eff7d 100644
--- a/README.md
+++ b/README.md
@@ -392,6 +392,11 @@ DeerFlow runs the agent runtime inside the Gateway API. Development mode enables
| **Stop** | `./scripts/serve.sh --stop`
`make stop` | `./scripts/docker.sh stop`
`make docker-stop` | `./scripts/deploy.sh down`
`make down` |
| **Restart** | `./scripts/serve.sh --restart [flags]` | `./scripts/docker.sh restart` | — |
+`make start` and `make start-daemon` rebuild the frontend with `next build` on
+every run. To reuse the last build instead, pass `SKIP_FRONTEND_BUILD=1` (or add
+`--skip-frontend-build` when calling `./scripts/serve.sh --prod` directly). This
+is opt-in: it fails fast when `frontend/.next` has no completed build.
+
Gateway owns `/api/langgraph/*` and translates those public LangGraph-compatible paths to its native `/api/*` routers behind nginx.
#### LangGraph Studio (Optional)
diff --git a/backend/tests/test_serve_frontend_skip_build.py b/backend/tests/test_serve_frontend_skip_build.py
new file mode 100644
index 000000000..876ae63b2
--- /dev/null
+++ b/backend/tests/test_serve_frontend_skip_build.py
@@ -0,0 +1,55 @@
+"""Regression coverage for the opt-in --skip-frontend-build startup flag."""
+
+from __future__ import annotations
+
+import re
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+SERVE_SH = REPO_ROOT / "scripts" / "serve.sh"
+ROOT_MAKEFILE = REPO_ROOT / "Makefile"
+
+
+def _make_recipe(target: str) -> str:
+ makefile = ROOT_MAKEFILE.read_text(encoding="utf-8")
+ match = re.search(rf"^{target}:\n((?:\t.*\n?)+)", makefile, re.M)
+ assert match, f"target {target!r} not found in root Makefile"
+ return match.group(1)
+
+
+def test_serve_script_parses_skip_frontend_build_flag() -> None:
+ serve = SERVE_SH.read_text(encoding="utf-8")
+
+ assert "SKIP_FRONTEND_BUILD=false" in serve
+ assert "--skip-frontend-build) SKIP_FRONTEND_BUILD=true ;;" in serve
+
+
+def test_prod_default_still_builds_via_preview() -> None:
+ serve = SERVE_SH.read_text(encoding="utf-8")
+
+ # Verify the control-flow: skip flag -> `run start`, otherwise -> `run preview`.
+ assert re.search(
+ r"elif\s+\$SKIP_FRONTEND_BUILD;\s+then.*?FRONTEND_CMD=.*?run start.*?\nelse\n\s*FRONTEND_CMD=.*?run preview",
+ serve,
+ re.S,
+ ), "expected prod default to use `run preview` and --skip-frontend-build to use `run start`"
+
+
+def test_skip_build_reuses_existing_build_and_requires_build_id() -> None:
+ serve = SERVE_SH.read_text(encoding="utf-8")
+
+ assert 'if [ ! -f "$REPO_ROOT/frontend/.next/BUILD_ID" ]; then' in serve
+ assert "Run 'make start' once (full build)" in serve
+
+
+def test_skip_build_preflight_runs_before_stop_all() -> None:
+ serve = SERVE_SH.read_text(encoding="utf-8")
+
+ assert serve.index("frontend/.next/BUILD_ID") < serve.index('if [ "$ACTION" = "restart" ]; then')
+
+
+def test_make_start_exposes_flag_as_opt_in() -> None:
+ for target in ("start", "start-daemon"):
+ recipe = _make_recipe(target)
+ assert "--skip-frontend-build" in recipe
+ assert "$(if $(filter 1,$(SKIP_FRONTEND_BUILD)),--skip-frontend-build)" in recipe
diff --git a/scripts/serve.sh b/scripts/serve.sh
index 1eb76aaa3..aa72d14b5 100755
--- a/scripts/serve.sh
+++ b/scripts/serve.sh
@@ -11,9 +11,11 @@
# --daemon Run all services in background (nohup), exit after startup
#
# Actions:
-# --skip-install Skip dependency installation (faster restart)
-# --stop Stop all running services and exit
-# --restart Stop all services, then start with the given mode flags
+# --skip-install Skip dependency installation (faster restart)
+# --skip-frontend-build With --prod, reuse the existing .next build via `next start`
+# instead of `next build` (opt-in; fails if no build exists)
+# --stop Stop all running services and exit
+# --restart Stop all services, then start with the given mode flags
#
# Examples:
# ./scripts/serve.sh --dev # Gateway dev, hot reload
@@ -53,6 +55,7 @@ _pick_python() {
DEV_MODE=true
DAEMON_MODE=false
SKIP_INSTALL=false
+SKIP_FRONTEND_BUILD=false
ACTION="start" # start | stop | restart
for arg in "$@"; do
@@ -61,11 +64,12 @@ for arg in "$@"; do
--prod) DEV_MODE=false ;;
--daemon) DAEMON_MODE=true ;;
--skip-install) SKIP_INSTALL=true ;;
+ --skip-frontend-build) SKIP_FRONTEND_BUILD=true ;;
--stop) ACTION="stop" ;;
--restart) ACTION="restart" ;;
*)
echo "Unknown argument: $arg"
- echo "Usage: $0 [--dev|--prod] [--daemon] [--skip-install] [--stop|--restart]"
+ echo "Usage: $0 [--dev|--prod] [--daemon] [--skip-install] [--skip-frontend-build] [--stop|--restart]"
exit 1
;;
esac
@@ -268,6 +272,16 @@ stop_all() {
echo "✓ All services stopped"
}
+# Validate the reusable frontend build before any stop_all runs, so start and
+# restart never tear down a healthy stack only to fail here. --stop is exempt.
+if [ "$ACTION" != "stop" ] && ! $DEV_MODE && $SKIP_FRONTEND_BUILD; then
+ if [ ! -f "$REPO_ROOT/frontend/.next/BUILD_ID" ]; then
+ echo "✗ --skip-frontend-build requires an existing frontend build."
+ echo " Run 'make start' once (full build), or: cd frontend && pnpm run build"
+ exit 1
+ fi
+fi
+
# ── Action routing ───────────────────────────────────────────────────────────
if [ "$ACTION" = "stop" ]; then
@@ -305,6 +319,12 @@ 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'
+ 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"
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"
fi
@@ -405,6 +425,9 @@ echo " Starting DeerFlow"
echo "=========================================="
echo ""
echo " Mode: $MODE_LABEL"
+if ! $DEV_MODE && $SKIP_FRONTEND_BUILD; then
+ echo " (frontend: reusing existing build)"
+fi
echo ""
echo " Services:"
echo " Gateway → localhost:8001 (REST API + agent runtime)"