mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 02:14:49 +00:00
Merge pull request #819 from oraios/dashboard_port_fix
Changed dashboard default listening address to 127.0.0.1 and made it configurable Also some improvements to the configuration in docker setup, no longer use serena_config.docker.yml
This commit is contained in:
9 files changed
+113
-135
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Docker Setup for Serena (Experimental)
|
||||
|
||||
⚠️ **EXPERIMENTAL FEATURE**: The Docker setup for Serena is currently experimental and has several limitations. Please read this entire document before using Docker with Serena.
|
||||
⚠️ **EXPERIMENTAL FEATURE**: The Docker setup for Serena is still experimental and has some limitations. Please read this entire document before using Docker with Serena.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -12,27 +12,40 @@ Docker support allows you to run Serena in an isolated container environment, wh
|
||||
- **Consistent dependencies**: No need to manage language servers and dependencies on your host system
|
||||
- **Cross-platform support**: Works consistently across Windows, macOS, and Linux
|
||||
|
||||
## Important Limitations and Caveats
|
||||
## Important Usage Pointers
|
||||
|
||||
### 1. Configuration File Conflicts
|
||||
### Configuration
|
||||
|
||||
⚠️ **Critical**: Docker uses a separate configuration file (`serena_config.docker.yml`) to avoid path conflicts. When running in Docker:
|
||||
- Container paths will be stored in the configuration (e.g., `/workspaces/serena/...`)
|
||||
- These paths are incompatible with non-Docker usage
|
||||
- After using Docker, you cannot directly switch back to non-Docker usage without manual configuration adjustment
|
||||
Serena's configuration and log files are stored in the container in `/workspaces/serena/config/`.
|
||||
Any local configuration you may have for Serena will not apply; the container uses its own separate configuration.
|
||||
|
||||
### 2. Project Activation Limitations
|
||||
You can mount a local configuration/data directory to persist settings across container restarts
|
||||
(which will also contain session log files).
|
||||
Simply mount your local directory to `/workspaces/serena/config` in the container.
|
||||
Initially, be sure to add a `serena_config.yml` file to the mounted directory which applies the following
|
||||
special settings for Docker usage:
|
||||
```
|
||||
# Disable the GUI log window since it's not supported in Docker
|
||||
gui_log_window: False
|
||||
# Disable opening the web dashboard on launch (not possible within the container)
|
||||
web_dashboard_open_on_launch: False
|
||||
```
|
||||
Set other configuration options as needed.
|
||||
|
||||
### Project Activation Limitations
|
||||
|
||||
- **Only mounted directories work**: Projects must be mounted as volumes to be accessible
|
||||
- Projects outside the mounted directories cannot be activated or accessed
|
||||
- Default setup only mounts the current directory
|
||||
- Since projects are not remembered across container restarts (unless you mount a local configuration as described above),
|
||||
activate them using the full path (e.g. `/workspaces/projects/my-project`) when using dynamic project activation
|
||||
|
||||
### 3. GUI Window Disabled
|
||||
### Language Support Limitations
|
||||
|
||||
- The GUI log window option is automatically disabled in Docker environments
|
||||
- Use the web dashboard instead (see below)
|
||||
The default Docker image does not include dependencies for languages that
|
||||
require explicit system-level installations.
|
||||
Only languages that install their requirements on the fly will work out of the box.
|
||||
|
||||
### 4. Dashboard Port Configuration
|
||||
### Dashboard Port Configuration
|
||||
|
||||
The web dashboard runs on port 24282 (0x5EDA) by default. You can configure this using environment variables:
|
||||
|
||||
@@ -46,7 +59,7 @@ SERENA_DASHBOARD_PORT=8080 docker-compose up serena
|
||||
|
||||
⚠️ **Note**: If the local port is occupied, you'll need to specify a different port using the environment variable.
|
||||
|
||||
### 5. Line Ending Issues on Windows
|
||||
### Line Ending Issues on Windows
|
||||
|
||||
⚠️ **Windows Users**: Be aware of potential line ending inconsistencies:
|
||||
- Files edited within the Docker container may use Unix line endings (LF)
|
||||
@@ -70,7 +83,7 @@ SERENA_DASHBOARD_PORT=8080 docker-compose up serena
|
||||
|
||||
Note: Edit the `compose.yaml` file to customize volume mounts for your projects.
|
||||
|
||||
### Using Docker directly
|
||||
### Building the Docker Image Manually
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
@@ -157,24 +170,3 @@ Ensure projects are properly mounted:
|
||||
- Check volume mounts in `docker-compose.yaml`
|
||||
- Use absolute paths for external projects
|
||||
- Verify permissions on mounted directories
|
||||
|
||||
## Migration Path
|
||||
|
||||
To switch between Docker and non-Docker usage:
|
||||
|
||||
1. **Docker to Non-Docker**:
|
||||
- Manually edit project paths in `serena_config.yml`
|
||||
- Change container paths to host paths
|
||||
- Or use separate config files for each environment
|
||||
|
||||
2. **Non-Docker to Docker**:
|
||||
- Projects will be re-registered with container paths
|
||||
- Original config remains unchanged
|
||||
|
||||
## Future Improvements
|
||||
|
||||
We're working on:
|
||||
- Automatic config migration between environments
|
||||
- Better project path handling
|
||||
- Dynamic port allocation
|
||||
- Windows line-ending handling.
|
||||
+9
-18
@@ -53,28 +53,20 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
|
||||
# Set the working directory
|
||||
WORKDIR /workspaces/serena
|
||||
|
||||
# Development target
|
||||
FROM base AS development
|
||||
# Copy all files for development
|
||||
COPY . /workspaces/serena/
|
||||
|
||||
# Create virtual environment and install dependencies with dev extras
|
||||
RUN uv venv
|
||||
RUN . .venv/bin/activate
|
||||
RUN uv pip install --all-extras -r pyproject.toml -e .
|
||||
ENV PATH="/workspaces/serena/.venv/bin:${PATH}"
|
||||
# Install sed
|
||||
RUN apt-get update && apt-get install -y sed
|
||||
|
||||
# Entrypoint to ensure environment is activated
|
||||
ENTRYPOINT ["/bin/bash", "-c", "source .venv/bin/activate && $0 $@"]
|
||||
# Create Serena configuration
|
||||
ENV SERENA_HOME=/workspaces/serena/config
|
||||
RUN mkdir -p $SERENA_HOME
|
||||
RUN cp src/serena/resources/serena_config.template.yml $SERENA_HOME/serena_config.yml
|
||||
RUN sed -i 's/^gui_log_window: .*/gui_log_window: False/' $SERENA_HOME/serena_config.yml
|
||||
RUN sed -i 's/^web_dashboard_open_on_launch: .*/web_dashboard_open_on_launch: False/' $SERENA_HOME/serena_config.yml
|
||||
|
||||
# Production target
|
||||
FROM base AS production
|
||||
# Copy only necessary files for production
|
||||
COPY pyproject.toml /workspaces/serena/
|
||||
COPY README.md /workspaces/serena/
|
||||
COPY src/ /workspaces/serena/src/
|
||||
|
||||
# Create virtual environment and install dependencies (production only)
|
||||
# Create virtual environment and install dependencies
|
||||
RUN uv venv
|
||||
RUN . .venv/bin/activate
|
||||
RUN uv pip install -r pyproject.toml -e .
|
||||
@@ -82,4 +74,3 @@ ENV PATH="/workspaces/serena/.venv/bin:${PATH}"
|
||||
|
||||
# Entrypoint to ensure environment is activated
|
||||
ENTRYPOINT ["/bin/bash", "-c", "source .venv/bin/activate && $0 $@"]
|
||||
|
||||
+3
-20
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
serena:
|
||||
image: serena:latest
|
||||
# To work with projects, you must mount them as volumes:
|
||||
# To work with projects, you must mount them into /workspace/ in the container:
|
||||
# volumes:
|
||||
# - ./my-project:/workspace/my-project
|
||||
# - /path/to/another/project:/workspace/another-project
|
||||
@@ -16,23 +16,6 @@ services:
|
||||
- SERENA_DOCKER=1
|
||||
command:
|
||||
- "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0"
|
||||
# Add the context for the IDE assistant
|
||||
# - "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0 --context ide-assistant"
|
||||
# Alternatively add further arguments, e.g. a context
|
||||
# - "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0 --context ide"
|
||||
|
||||
serena-dev:
|
||||
image: serena:dev
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: Dockerfile
|
||||
target: development
|
||||
tty: true
|
||||
stdin_open: true
|
||||
environment:
|
||||
- SERENA_DOCKER=1
|
||||
volumes:
|
||||
- .:/workspaces/serena
|
||||
ports:
|
||||
- "${SERENA_PORT:-9121}:9121" # MCP server port
|
||||
- "${SERENA_DASHBOARD_PORT:-24282}:24282" # Dashboard port
|
||||
command:
|
||||
- "uv run --directory . serena-mcp-server"
|
||||
@@ -47,28 +47,30 @@ As a consequence, you will need to specify paths when using CLI commands that wo
|
||||
:::
|
||||
|
||||
(docker)=
|
||||
### Using Docker (Experimental)
|
||||
### Using Docker
|
||||
|
||||
:::{warning}
|
||||
Docker support is currently experimental with several limitations.
|
||||
:::
|
||||
The Docker approach offers several advantages:
|
||||
|
||||
You can run the Serena MCP server directly via docker as follows,
|
||||
* better security isolation for shell command execution
|
||||
* no need to install language servers and dependencies locally
|
||||
* consistent environment across different systems
|
||||
|
||||
You can run the Serena MCP server directly via Docker as follows,
|
||||
assuming that the projects you want to work on are all located in `/path/to/your/projects`:
|
||||
|
||||
```shell
|
||||
docker run --rm -i --network host -v /path/to/your/projects:/workspaces/projects ghcr.io/oraios/serena:latest serena
|
||||
```
|
||||
|
||||
Replace `/path/to/your/projects` with the absolute path to your projects directory. The Docker approach provides:
|
||||
This command mounts your projects into the container under `/workspaces/projects`, so when working with projects,
|
||||
you need to refer to them using the respective path (e.g. `/workspaces/projects/my-project`).
|
||||
|
||||
* Better security isolation for shell command execution
|
||||
* No need to install language servers and dependencies locally
|
||||
* Consistent environment across different systems
|
||||
Alternatively, you may use Docker compose with the `compose.yml` file provided in the repository.
|
||||
See our [advanced Docker usage](https://github.com/oraios/serena/blob/main/DOCKER.md) documentation for more detailed instructions, configuration options, and limitations.
|
||||
|
||||
Alternatively, use docker compose with the `compose.yml` file provided in the repository.
|
||||
|
||||
See our [Docker Setup](https://github.com/oraios/serena/blob/main/DOCKER.md) documentation for more detailed setup instructions, configuration options, and known limitations.
|
||||
:::{note}
|
||||
Docker usage is subject to limitations; see the [advanced Docker usage](https://github.com/oraios/serena/blob/main/DOCKER.md) documentation for details.
|
||||
:::
|
||||
|
||||
### Using Nix
|
||||
|
||||
|
||||
+5
-2
@@ -271,8 +271,11 @@ class SerenaAgent:
|
||||
if self.serena_config.web_dashboard:
|
||||
self._dashboard_thread, port = SerenaDashboardAPI(
|
||||
get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats
|
||||
).run_in_thread()
|
||||
dashboard_url = f"http://127.0.0.1:{port}/dashboard/index.html"
|
||||
).run_in_thread(host=self.serena_config.web_dashboard_listen_address)
|
||||
dashboard_host = self.serena_config.web_dashboard_listen_address
|
||||
if dashboard_host == "0.0.0.0":
|
||||
dashboard_host = "localhost"
|
||||
dashboard_url = f"http://{dashboard_host}:{port}/dashboard/index.html"
|
||||
log.info("Serena web dashboard started at %s", dashboard_url)
|
||||
if self.serena_config.web_dashboard_open_on_launch:
|
||||
# open the dashboard URL in the default web browser (using a separate process to control
|
||||
|
||||
@@ -27,7 +27,7 @@ from serena.constants import (
|
||||
SERENA_FILE_ENCODING,
|
||||
SERENA_MANAGED_DIR_NAME,
|
||||
)
|
||||
from serena.util.general import load_yaml, save_yaml
|
||||
from serena.util.general import get_dataclass_default, load_yaml, save_yaml
|
||||
from serena.util.inspection import determine_programming_language_composition
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
@@ -106,19 +106,6 @@ def get_serena_managed_in_project_dir(project_root: str | Path) -> str:
|
||||
return os.path.join(project_root, SERENA_MANAGED_DIR_NAME)
|
||||
|
||||
|
||||
def is_running_in_docker() -> bool:
|
||||
"""Check if we're running inside a Docker container."""
|
||||
# Check for Docker-specific files
|
||||
if os.path.exists("/.dockerenv"):
|
||||
return True
|
||||
# Check cgroup for docker references
|
||||
try:
|
||||
with open("/proc/self/cgroup") as f:
|
||||
return "docker" in f.read()
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
|
||||
project_name: str
|
||||
@@ -367,6 +354,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
|
||||
trace_lsp_communication: bool = False
|
||||
web_dashboard: bool = True
|
||||
web_dashboard_open_on_launch: bool = True
|
||||
web_dashboard_listen_address: str = "127.0.0.1"
|
||||
tool_timeout: float = DEFAULT_TOOL_TIMEOUT
|
||||
loaded_commented_yaml: CommentedMap | None = None
|
||||
config_file_path: str | None = None
|
||||
@@ -396,7 +384,6 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
|
||||
"""Advanced configuration option allowing to configure language server implementation specific options, see SolidLSPSettings for more info."""
|
||||
|
||||
CONFIG_FILE = "serena_config.yml"
|
||||
CONFIG_FILE_DOCKER = "serena_config.docker.yml" # Docker-specific config file; auto-generated if missing, mounted via docker-compose for user customization
|
||||
|
||||
def _tostring_includes(self) -> list[str]:
|
||||
return ["config_file_path"]
|
||||
@@ -417,20 +404,17 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
|
||||
"""
|
||||
:return: the location where the Serena configuration file is stored/should be stored
|
||||
"""
|
||||
if is_running_in_docker():
|
||||
return os.path.join(REPO_ROOT, cls.CONFIG_FILE_DOCKER)
|
||||
else:
|
||||
config_path = os.path.join(SerenaPaths().serena_user_home_dir, cls.CONFIG_FILE)
|
||||
config_path = os.path.join(SerenaPaths().serena_user_home_dir, cls.CONFIG_FILE)
|
||||
|
||||
# if the config file does not exist, check if we can migrate it from the old location
|
||||
if not os.path.exists(config_path):
|
||||
old_config_path = os.path.join(REPO_ROOT, cls.CONFIG_FILE)
|
||||
if os.path.exists(old_config_path):
|
||||
log.info(f"Moving Serena configuration file from {old_config_path} to {config_path}")
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
shutil.move(old_config_path, config_path)
|
||||
# if the config file does not exist, check if we can migrate it from the old location
|
||||
if not os.path.exists(config_path):
|
||||
old_config_path = os.path.join(REPO_ROOT, cls.CONFIG_FILE)
|
||||
if os.path.exists(old_config_path):
|
||||
log.info(f"Moving Serena configuration file from {old_config_path} to {config_path}")
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
shutil.move(old_config_path, config_path)
|
||||
|
||||
return config_path
|
||||
return config_path
|
||||
|
||||
@classmethod
|
||||
def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig":
|
||||
@@ -481,23 +465,22 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
|
||||
instance.projects.append(project)
|
||||
|
||||
# set other configuration parameters
|
||||
if is_running_in_docker():
|
||||
instance.gui_log_window_enabled = False # not supported in Docker
|
||||
else:
|
||||
instance.gui_log_window_enabled = loaded_commented_yaml.get("gui_log_window", False)
|
||||
def get_value_or_default(field_name: str) -> Any:
|
||||
return loaded_commented_yaml.get(field_name, get_dataclass_default(SerenaConfig, field_name))
|
||||
|
||||
instance.gui_log_window_enabled = get_value_or_default("gui_log_window_enabled")
|
||||
instance.web_dashboard_listen_address = get_value_or_default("web_dashboard_listen_address")
|
||||
instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO))
|
||||
instance.web_dashboard = loaded_commented_yaml.get("web_dashboard", True)
|
||||
instance.web_dashboard_open_on_launch = loaded_commented_yaml.get("web_dashboard_open_on_launch", True)
|
||||
instance.tool_timeout = loaded_commented_yaml.get("tool_timeout", DEFAULT_TOOL_TIMEOUT)
|
||||
instance.trace_lsp_communication = loaded_commented_yaml.get("trace_lsp_communication", False)
|
||||
instance.excluded_tools = loaded_commented_yaml.get("excluded_tools", [])
|
||||
instance.included_optional_tools = loaded_commented_yaml.get("included_optional_tools", [])
|
||||
instance.jetbrains = loaded_commented_yaml.get("jetbrains", False)
|
||||
instance.token_count_estimator = loaded_commented_yaml.get(
|
||||
"token_count_estimator", RegisteredTokenCountEstimator.TIKTOKEN_GPT4O.name
|
||||
)
|
||||
instance.default_max_tool_answer_chars = loaded_commented_yaml.get("default_max_tool_answer_chars", 150_000)
|
||||
instance.ls_specific_settings = loaded_commented_yaml.get("ls_specific_settings", {})
|
||||
instance.web_dashboard = get_value_or_default("web_dashboard")
|
||||
instance.web_dashboard_open_on_launch = get_value_or_default("web_dashboard_open_on_launch")
|
||||
instance.tool_timeout = get_value_or_default("tool_timeout")
|
||||
instance.trace_lsp_communication = get_value_or_default("trace_lsp_communication")
|
||||
instance.excluded_tools = get_value_or_default("excluded_tools")
|
||||
instance.included_optional_tools = get_value_or_default("included_optional_tools")
|
||||
instance.jetbrains = get_value_or_default("jetbrains")
|
||||
instance.token_count_estimator = get_value_or_default("token_count_estimator")
|
||||
instance.default_max_tool_answer_chars = get_value_or_default("default_max_tool_answer_chars")
|
||||
instance.ls_specific_settings = get_value_or_default("ls_specific_settings")
|
||||
|
||||
# re-save the configuration file if any migrations were performed
|
||||
if num_project_migrations > 0:
|
||||
|
||||
@@ -560,7 +560,7 @@ class SerenaDashboardAPI:
|
||||
|
||||
raise RuntimeError(f"No free ports found starting from {start_port}")
|
||||
|
||||
def run(self, host: str = "0.0.0.0", port: int = 0x5EDA) -> int:
|
||||
def run(self, host: str, port: int) -> int:
|
||||
"""
|
||||
Runs the dashboard on the given host and port and returns the port number.
|
||||
"""
|
||||
@@ -572,8 +572,9 @@ class SerenaDashboardAPI:
|
||||
self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True)
|
||||
return port
|
||||
|
||||
def run_in_thread(self) -> tuple[threading.Thread, int]:
|
||||
def run_in_thread(self, host: str) -> tuple[threading.Thread, int]:
|
||||
port = self._find_first_free_port(0x5EDA)
|
||||
thread = threading.Thread(target=lambda: self.run(port=port), daemon=True)
|
||||
log.info("Starting dashboard (listen_address=%s, port=%d)", host, port)
|
||||
thread = threading.Thread(target=lambda: self.run(host=host, port=port), daemon=True)
|
||||
thread.start()
|
||||
return thread, port
|
||||
@@ -20,6 +20,9 @@ web_dashboard: True
|
||||
# shows Serena's current session logs - as an alternative to the GUI log window which
|
||||
# is supported on all platforms.
|
||||
|
||||
web_dashboard_listen_address: 127.0.0.1
|
||||
# The address where the web dashboard will listen on
|
||||
|
||||
web_dashboard_open_on_launch: True
|
||||
# whether to open a browser window with the web dashboard when Serena starts (provided that web_dashboard
|
||||
# is enabled). If set to False, you can still open the dashboard manually by navigating to
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
from typing import Literal, overload
|
||||
from dataclasses import MISSING, Field
|
||||
from typing import Any, Literal, cast, overload
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml.comments import CommentedMap
|
||||
@@ -32,3 +33,22 @@ def save_yaml(path: str, data: dict | CommentedMap, preserve_comments: bool = Fa
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding=SERENA_FILE_ENCODING) as f:
|
||||
yaml.dump(data, f)
|
||||
|
||||
|
||||
def get_dataclass_default(cls: type, field_name: str) -> Any:
|
||||
"""
|
||||
Gets the default value of a dataclass field.
|
||||
|
||||
:param cls: The dataclass type.
|
||||
:param field_name: The name of the field.
|
||||
:return: The default value of the field (either from default or default_factory).
|
||||
"""
|
||||
field = cast(Field, cls.__dataclass_fields__[field_name]) # type: ignore[attr-defined]
|
||||
|
||||
if field.default is not MISSING:
|
||||
return field.default
|
||||
|
||||
if field.default_factory is not MISSING: # default_factory is a function
|
||||
return field.default_factory()
|
||||
|
||||
raise AttributeError(f"{field_name} has no default")
|
||||
Reference in new issue
Block a user