Compare commits

...
Author SHA1 Message Date
William FHandClaude Opus 4.6 51561f117b fix: keep LANGSERVE_GRAPHS, LANGGRAPH_UI, LANGGRAPH_UI_CONFIG as individual ENV lines
JS build/runtime scripts (build.mts, client.mts, preload.mjs) read these
env vars directly, so they must remain as individual Dockerfile ENV lines
alongside AGENT_SERVER_CONFIG.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 04:30:26 +00:00
William FHandClaude Opus 4.6 c7760f1d4f feat(cli): emit unified AGENT_SERVER_CONFIG env var in Dockerfiles
Replace 8+ individual ENV lines (LANGGRAPH_STORE, LANGGRAPH_AUTH,
LANGGRAPH_HTTP, LANGSERVE_GRAPHS, etc.) with a single
AGENT_SERVER_CONFIG env var in generated Dockerfiles.

Companion server PR: langchain-ai/langgraph-api#2826

Release Notes:

- CLI now emits a single `AGENT_SERVER_CONFIG` environment variable in
  generated Dockerfiles instead of 8+ individual `LANGGRAPH_*` env vars.
  Requires langgraph-api >= 0.7.56.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 04:16:09 +00:00
4 changed files with 74 additions and 69 deletions
+24 -52
View File
@@ -22,6 +22,24 @@ DEFAULT_IMAGE_DISTRO = "debian"
_BUILD_TOOLS = ("pip", "setuptools", "wheel")
def _build_server_config_env(config: Config) -> str:
"""Build the AGENT_SERVER_CONFIG env var line from a validated config."""
server_config: dict = {"graphs": config["graphs"]}
for key in (
"store",
"auth",
"encryption",
"http",
"webhooks",
"checkpointer",
"ui",
"ui_config",
):
if (value := config.get(key)) is not None:
server_config[key] = value
return f"ENV AGENT_SERVER_CONFIG='{json.dumps(server_config)}'"
def _get_pip_cleanup_lines(
install_cmd: str,
to_uninstall: tuple[str] | None,
@@ -1006,37 +1024,14 @@ ADD {relpath} /deps/{name}
)
)
env_vars = []
if (store_config := config.get("store")) is not None:
env_vars.append(f"ENV LANGGRAPH_STORE='{json.dumps(store_config)}'")
if (auth_config := config.get("auth")) is not None:
env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'")
if (encryption_config := config.get("encryption")) is not None:
env_vars.append(f"ENV LANGGRAPH_ENCRYPTION='{json.dumps(encryption_config)}'")
if (http_config := config.get("http")) is not None:
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
# Inject webhooks configuration if provided
if (webhooks_config := config.get("webhooks")) is not None:
env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
if (checkpointer_config := config.get("checkpointer")) is not None:
env_vars.append(
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
)
env_vars = [_build_server_config_env(config)]
# JS build/runtime scripts read these env vars directly
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'")
if (ui := config.get("ui")) is not None:
env_vars.append(f"ENV LANGGRAPH_UI='{json.dumps(ui)}'")
if (ui_config := config.get("ui_config")) is not None:
env_vars.append(f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(ui_config)}'")
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'")
js_inst_str: str = ""
if (config.get("ui") or config.get("node_version")) and local_deps.working_dir:
js_inst_str = os.linesep.join(
@@ -1137,37 +1132,14 @@ def node_config_to_docker(
image_str = docker_tag(config, base_image, api_version)
env_vars: list[str] = []
if (store_config := config.get("store")) is not None:
env_vars.append(f"ENV LANGGRAPH_STORE='{json.dumps(store_config)}'")
if (auth_config := config.get("auth")) is not None:
env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'")
if (encryption_config := config.get("encryption")) is not None:
env_vars.append(f"ENV LANGGRAPH_ENCRYPTION='{json.dumps(encryption_config)}'")
if (http_config := config.get("http")) is not None:
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
# Inject webhooks configuration if provided
if (webhooks_config := config.get("webhooks")) is not None:
env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
if (checkpointer_config := config.get("checkpointer")) is not None:
env_vars.append(
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
)
env_vars: list[str] = [_build_server_config_env(config)]
# JS build/runtime scripts read these env vars directly
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'")
if ui := config.get("ui"):
env_vars.append(f"ENV LANGGRAPH_UI='{json.dumps(ui)}'")
if ui_config := config.get("ui_config"):
env_vars.append(f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(ui_config)}'")
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'")
# For monorepo support, we need to handle install and build commands differently
if build_context:
# Monorepo case: install from root, build from config directory
+19
View File
@@ -713,9 +713,28 @@ class Config(TypedDict, total=False):
"""
class ServerConfig(TypedDict, total=False):
"""Unified server configuration emitted as ``AGENT_SERVER_CONFIG``.
Mirrors the server-side ``ServerConfig`` but only contains fields
relevant to the CLI's Dockerfile / docker-compose emission.
"""
graphs: dict[str, str]
store: StoreConfig | None
auth: AuthConfig | None
encryption: EncryptionConfig | None
http: HttpConfig | None
webhooks: WebhooksConfig | None
checkpointer: CheckpointerConfig | None
ui: dict[str, str] | None
ui_config: dict | None
__all__ = [
"Config",
"GraphDef",
"ServerConfig",
"StoreConfig",
"CheckpointerConfig",
"AuthConfig",
@@ -152,6 +152,7 @@ services:
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "agent.py:graph"}}}}'
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
WORKDIR /deps/cli
+30 -17
View File
@@ -474,7 +474,7 @@ RUN set -ex && \\
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV LANGGRAPH_HTTP='{{"app": "/deps/examples/my_app.py:app"}}'
ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}, "http": {{"app": "/deps/examples/my_app.py:app"}}}}'
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
{FORMATTED_CLEANUP_LINES}
WORKDIR /deps/outer-unit_tests/unit_tests\
@@ -531,6 +531,7 @@ RUN set -ex && \\
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{"graphs": {"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}'
"""
+ FORMATTED_CLEANUP_LINES
@@ -578,6 +579,7 @@ RUN set -ex && \\
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{"graphs": {"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}'
"""
+ FORMATTED_CLEANUP_LINES
@@ -640,6 +642,7 @@ RUN set -ex && \\
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-graphs/src/agent.py:graph"}}}}'
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-graphs/src/agent.py:graph"}}'
{FORMATTED_CLEANUP_LINES}\
"""
@@ -676,6 +679,7 @@ ADD . /deps/unit_tests
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{"graphs": {"agent": "/deps/unit_tests/graphs/agent.py:graph"}}'
ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}'
"""
+ FORMATTED_CLEANUP_LINES
@@ -724,6 +728,7 @@ RUN set -ex && \\
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-graphs/src/agent.py:graph"}}}}'
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-graphs/src/agent.py:graph"}}'
{FORMATTED_CLEANUP_LINES}"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
@@ -752,10 +757,10 @@ ARG meow
ARG foo
ADD . /deps/unit_tests
RUN cd /deps/unit_tests && npm i
ENV LANGGRAPH_AUTH='{"path": "./graphs/auth.mts:auth"}'
ENV AGENT_SERVER_CONFIG='{"graphs": {"agent": "./graphs/agent.js:graph"}, "auth": {"path": "./graphs/auth.mts:auth"}, "ui": {"agent": "./graphs/agent.ui.jsx"}, "ui_config": {"shared": ["nuqs"]}}'
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}'
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
WORKDIR /deps/unit_tests
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
@@ -809,8 +814,8 @@ def test_config_to_docker_python_encryption_formatted():
),
base_image="langchain/langgraph-api",
)
# Verify that LANGGRAPH_ENCRYPTION is in the docker output with the correct path
assert "LANGGRAPH_ENCRYPTION=" in actual_docker_stdin
# Verify that AGENT_SERVER_CONFIG contains encryption with the correct path
assert "AGENT_SERVER_CONFIG=" in actual_docker_stdin
assert (
"/deps/outer-unit_tests/unit_tests/agent.py:my_encryption"
in actual_docker_stdin
@@ -839,10 +844,10 @@ ARG meow
ARG foo
ADD . /deps/unit_tests
RUN cd /deps/unit_tests && npm i
ENV LANGGRAPH_AUTH='{"path": "./graphs/auth.mts:auth"}'
ENV AGENT_SERVER_CONFIG='{"graphs": {"agent": "./graphs/agent.js:graph"}, "auth": {"path": "./graphs/auth.mts:auth"}, "ui": {"agent": "./graphs/agent.ui.jsx"}, "ui_config": {"shared": ["nuqs"]}}'
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}'
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
WORKDIR /deps/unit_tests
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
@@ -850,14 +855,14 @@ RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not foun
assert additional_contexts == {}
def _extract_env_json(dockerfile: str, var_name: str) -> dict:
"""Helper to extract and parse a JSON value from an ENV line in a Dockerfile."""
line_prefix = f"ENV {var_name}='"
def _extract_server_config(dockerfile: str) -> dict:
"""Helper to extract and parse the AGENT_SERVER_CONFIG JSON from a Dockerfile."""
line_prefix = "ENV AGENT_SERVER_CONFIG='"
for line in dockerfile.splitlines():
if line.startswith(line_prefix) and line.endswith("'"):
json_str = line[len(line_prefix) : -1]
return json.loads(json_str)
raise AssertionError(f"{var_name} not found in Dockerfile env lines")
raise AssertionError("AGENT_SERVER_CONFIG not found in Dockerfile env lines")
def test_config_to_docker_webhooks_python():
@@ -890,8 +895,8 @@ def test_config_to_docker_webhooks_python():
)
# Ensure the ENV line is present and the payload round-trips via JSON
parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS")
assert parsed == webhooks
server_config = _extract_server_config(dockerfile)
assert server_config["webhooks"] == webhooks
def test_config_to_docker_webhooks_node():
@@ -914,8 +919,8 @@ def test_config_to_docker_webhooks_node():
base_image="langchain/langgraphjs-api",
)
parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS")
assert parsed == webhooks
server_config = _extract_server_config(dockerfile)
assert server_config["webhooks"] == webhooks
def test_config_to_docker_no_webhooks():
@@ -926,7 +931,8 @@ def test_config_to_docker_no_webhooks():
base_image="langchain/langgraph-api",
)
assert "ENV LANGGRAPH_WEBHOOKS=" not in dockerfile
server_config = _extract_server_config(dockerfile)
assert "webhooks" not in server_config
def test_config_to_docker_gen_ui_python():
@@ -963,9 +969,10 @@ RUN set -ex && \\
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}, "ui": {{"agent": "./graphs/agent.ui.jsx"}}, "ui_config": {{"shared": ["nuqs"]}}}}'
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
ENV LANGGRAPH_UI='{{"agent": "./graphs/agent.ui.jsx"}}'
ENV LANGGRAPH_UI_CONFIG='{{"shared": ["nuqs"]}}'
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
# -- Installing JS dependencies --
ENV NODE_VERSION=20
RUN cd /deps/outer-unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
@@ -1009,6 +1016,7 @@ RUN set -ex && \\
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{{"graphs": {{"python": "/deps/outer-unit_tests/unit_tests/multiplatform/python.py:graph", "js": "/deps/outer-unit_tests/unit_tests/multiplatform/js.mts:graph"}}}}'
ENV LANGSERVE_GRAPHS='{{"python": "/deps/outer-unit_tests/unit_tests/multiplatform/python.py:graph", "js": "/deps/outer-unit_tests/unit_tests/multiplatform/js.mts:graph"}}'
# -- Installing JS dependencies --
ENV NODE_VERSION=22
@@ -1150,6 +1158,7 @@ def test_config_to_compose_simple_config():
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}}}'
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
WORKDIR /deps/outer-unit_tests/unit_tests
@@ -1191,6 +1200,7 @@ def test_config_to_compose_env_vars():
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}}}'
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
WORKDIR /deps/outer-unit_tests/unit_tests
@@ -1236,6 +1246,7 @@ def test_config_to_compose_env_file():
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}}}'
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
WORKDIR /deps/outer-unit_tests/unit_tests
@@ -1274,6 +1285,7 @@ def test_config_to_compose_watch():
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}}}'
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
WORKDIR /deps/outer-unit_tests/unit_tests
@@ -1321,6 +1333,7 @@ def test_config_to_compose_end_to_end():
# -- Installing all local dependencies --
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
# -- End of local dependencies install --
ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}}}'
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
WORKDIR /deps/outer-unit_tests/unit_tests