diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index 07a71c8b9..4e56bb8cc 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -273,6 +273,7 @@ def _build( pull: bool, tag: str, passthrough: Sequence[str] = (), + base_docker_tag: Optional[str] = None, ): # pull latest images if pull: @@ -280,10 +281,13 @@ def _build( subp_exec( "docker", "pull", - langgraph_cli.config.docker_tag(config_json, base_image), + langgraph_cli.config.docker_tag( + config_json, base_image, base_docker_tag + ), verbose=True, ) ) + set("Building...") # apply options args = [ @@ -294,7 +298,7 @@ def _build( ] # apply config stdin, additional_contexts = langgraph_cli.config.config_to_docker( - config, config_json, base_image + config, config_json, base_image, base_docker_tag ) # add additional_contexts if additional_contexts: @@ -355,7 +359,15 @@ def build( raise click.UsageError("Docker not installed") from None config_json = langgraph_cli.config.validate_config_file(config) _build( - runner, set, config, config_json, base_image, pull, tag, docker_build_args + runner, + set, + config, + config_json, + base_image, + pull, + tag, + docker_build_args, + config_json.get("_INTERNAL_docker_tag"), ) diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index f11850016..7b18b4a4d 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -326,6 +326,10 @@ class Config(TypedDict, total=False): Must be >= 20 if provided. """ + _INTERNAL_docker_tag: Optional[str] + """Optional. Internal use only. + """ + pip_config_file: Optional[str] """Optional. Path to a pip config file (e.g., "/etc/pip.conf" or "pip.ini") for controlling package installation (custom indices, credentials, etc.). @@ -480,6 +484,7 @@ def validate_config(config: Config) -> Config: "node_version": node_version, "python_version": python_version, "pip_config_file": config.get("pip_config_file"), + "_INTERNAL_docker_tag": config.get("_INTERNAL_docker_tag"), "dependencies": config.get("dependencies", []), "dockerfile_lines": config.get("dockerfile_lines", []), "graphs": config.get("graphs", {}), @@ -1025,7 +1030,10 @@ def _get_node_pm_install_cmd(config_path: pathlib.Path, config: Config) -> str: def python_config_to_docker( - config_path: pathlib.Path, config: Config, base_image: str + config_path: pathlib.Path, + config: Config, + base_image: str, + base_docker_tag: Optional[str] = None, ) -> tuple[str, dict[str, str]]: """Generate a Dockerfile from the configuration.""" # configure pip @@ -1160,7 +1168,7 @@ ADD {relpath} /deps/{name} ) docker_file_contents = [ - f"FROM {base_image}:{config['python_version']}", + f"FROM {base_image}:{base_docker_tag or config['python_version']}", "", os.linesep.join(config["dockerfile_lines"]), "", @@ -1192,7 +1200,10 @@ ADD {relpath} /deps/{name} def node_config_to_docker( - config_path: pathlib.Path, config: Config, base_image: str + config_path: pathlib.Path, + config: Config, + base_image: str, + base_docker_tag: Optional[str] = None, ) -> tuple[str, dict[str, str]]: faux_path = f"/deps/{config_path.parent.name}" install_cmd = _get_node_pm_install_cmd(config_path, config) @@ -1222,7 +1233,7 @@ def node_config_to_docker( env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'") docker_file_contents = [ - f"FROM {base_image}:{config['node_version']}", + f"FROM {base_image}:{base_docker_tag or config['node_version']}", "", os.linesep.join(config["dockerfile_lines"]), "", @@ -1246,8 +1257,14 @@ def default_base_image(config: Config) -> str: return "langchain/langgraph-api" -def docker_tag(config: Config, base_image: Optional[str] = None) -> str: +def docker_tag( + config: Config, + base_image: Optional[str] = None, + base_docker_tag: Optional[str] = None, +) -> str: base_image = base_image or default_base_image(config) + if base_docker_tag: + return f"{base_image}:{base_docker_tag}" if config.get("node_version") and not config.get("python_version"): return f"{base_image}:{config['node_version']}" @@ -1255,14 +1272,17 @@ def docker_tag(config: Config, base_image: Optional[str] = None) -> str: def config_to_docker( - config_path: pathlib.Path, config: Config, base_image: Optional[str] = None + config_path: pathlib.Path, + config: Config, + base_image: Optional[str] = None, + base_docker_tag: Optional[str] = None, ) -> tuple[str, dict[str, str]]: base_image = base_image or default_base_image(config) if config.get("node_version") and not config.get("python_version"): - return node_config_to_docker(config_path, config, base_image) + return node_config_to_docker(config_path, config, base_image, base_docker_tag) - return python_config_to_docker(config_path, config, base_image) + return python_config_to_docker(config_path, config, base_image, base_docker_tag) def config_to_compose(