open-webui / open-webui/terminals
Bug: Docker deployment conflates container-local and Docker-host paths for `TERMINALS_DOCKER_DATA_DIR`
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 108
- Forks
- 23
- Avg merge
- 12h 8m
- Merged PRs (30d)
- 1
Description
Description
When the Terminals orchestrator runs inside Docker and manages sibling Open Terminal containers through /var/run/docker.sock, it conflates two filesystem namespaces:
- The orchestrator container's filesystem.
- The Docker daemon host's filesystem.
TERMINALS_DOCKER_DATA_DIR is used in both namespaces, even though the same pathname is not necessarily valid in both. As a result, the documented /app/data volume does not reliably back the spawned terminals' /home/user directories. The documented volume also does not reliably persist the orchestrator SQLite database.
This causes policy loss after an orchestrator restart, followed by 404 responses for policy-prefixed Open WebUI requests.
Environment
- Terminals image:
ghcr.io/open-webui/terminals:v0.2.3 - Backend: Docker
- Host: macOS with Docker Desktop or OrbStack
- Orchestrator started with
docker run - Open WebUI running directly on the host from a Python virtual environment
This should also affect Docker Compose deployments because the documented Compose configuration uses the same /app/data mount.
Relevant documented configuration
The Docker documentation mounts:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- terminals-data:/app/data
The repository quick start similarly uses -v "$(pwd)/data:/app/data". However, these mounts only make /app/data available inside the orchestrator container. They do not make /app/data refer to the same directory from the Docker daemon's host namespace.
Documentation:
https://github.com/open-webui/terminals/blob/main/README.md
Root cause: workspace path crosses filesystem namespaces
The Docker backend constructs a per-user path using
settings.docker_data_dir:
@staticmethod
def _home_dir(user_id: str, context_id: str = DEFAULT_CONTEXT_ID) -> Path:
...
return Path(settings.docker_data_dir) / user_id
It resolves that path inside the orchestrator container:
host_data_dir = str(self._home_dir(user_id, context_id).resolve())
It then passes the resulting string directly to the Docker daemon as a bind
source:
host_config = {
"Binds": [f"{host_data_dir}:/home/user"],
"PublishAllPorts": True,
}
Source:
https://github.com/open-webui/terminals/blob/main/terminals/backends/docker.py#L61-L92
For example, consider this deployment:
docker run \
-v /host/terminals-data:/app/data \
-e TERMINALS_DOCKER_DATA_DIR=/app/data/terminals \
-v /var/run/docker.sock:/var/run/docker.sock \
ghcr.io/open-webui/terminals:v0.2.2
Inside the orchestrator:
/app/data/terminals -> /host/terminals-data/terminals
But when the orchestrator asks the Docker daemon to create a sibling container
with:
/app/data/terminals/<user-id>:/home/user
the daemon interprets /app/data/terminals/<user-id> on the Docker host. It
does not know that /app/data inside the orchestrator maps to
/host/terminals-data.
On a Linux Docker host, this can create or use an unrelated host directory under
/app/data. With Docker Desktop, it may refer to a path inside the Docker VM.
It does not refer to the volume mounted into the orchestrator.
The code also accesses this same path locally during reset:
home_dir = self._home_dir(user_id, context_id).resolve()
home_dir.mkdir(parents=True, exist_ok=True)
...
Therefore TERMINALS_DOCKER_DATA_DIR must currently be valid both:
- Inside the orchestrator container, for local filesystem operations.
- On the Docker daemon host, as a sibling-container bind source.
One setting cannot correctly represent differently mapped host and container
paths.
Related issue: SQLite database path is not guaranteed to be /app/data
The default database path is derived from the installed Python package location:
_PACKAGE_DIR = Path(__file__).resolve().parent
_DEFAULT_DATA_DIR = str(_PACKAGE_DIR.parent / "data")
database_url: str = (
f"sqlite+aiosqlite:///{_DEFAULT_DATA_DIR}/terminals.db"
)
Source:
https://github.com/open-webui/terminals/blob/main/terminals/config.py#L7-L34
The image uses pip install . and runs the installed terminals entry point.
The package-relative default may therefore resolve to a path such as:
/usr/local/lib/python3.12/site-packages/data/terminals.db
That location is outside the documented /app/data volume.
The effective paths can be confirmed with:
docker exec terminals python -c \
'from terminals.config import settings; print(settings.database_url); print(settings.docker_data_dir)'
If the database is outside /app/data, policies and lifecycle configuration
are written into the orchestrator container's writable layer and disappear when
a --rm container is restarted.
Symptoms
After restarting the Terminals orchestrator:
- Previously configured policies are missing.
- Existing running terminal containers may still be discovered.
- Open WebUI continues routing through its configured named policy.
- The named policy no longer exists in
terminals.db. - The orchestrator returns 404 before proxying to the spawned terminal.
Example:
GET /p/terminals/files/list?directory=/ 404
POST /p/terminals/files/cwd 404
GET /p/terminals/ports 404
The policy proxy explicitly returns 404 when its database lookup finds no
matching policy:
policy = row.scalar_one_or_none()
if policy is None:
raise HTTPException(
status_code=404,
detail=f"Policy '{policy_id}' not found",
)
Editing and re-saving the terminal connection in Open WebUI temporarily fixes
the problem because Open WebUI performs a PUT that recreates the missing
policy. The policy disappears again on the next orchestrator restart.
Steps to reproduce
-
Create a Docker network:
docker network create open-webui-terminals -
Start Terminals using the documented storage layout:
docker run --rm \ --name terminals \ --network open-webui-terminals \ -p 127.0.0.1:3001:3000 \ -e TERMINALS_BACKEND=docker \ -e TERMINALS_API_KEY=test-key \ -e TERMINALS_DOCKER_NETWORK=open-webui-terminals \ -e TERMINALS_DOCKER_DATA_DIR=/app/data/terminals \ -v /var/run/docker.sock:/var/run/docker.sock \ -v terminals-data:/app/data \ ghcr.io/open-webui/terminals:v0.2.2 -
Configure an orchestrator connection and named policy in Open WebUI.
-
Activate it so that a user terminal is provisioned.
-
Inspect the spawned terminal:
docker ps \ --filter label=app.kubernetes.io/managed-by=terminals \ --format '{{.Names}}' docker inspect <spawned-container> \ --format '{{json .HostConfig.Binds}}' -
Observe that the spawned container uses a source resembling:
/app/data/terminals/<user-id>:/home/userThis is interpreted on the Docker daemon host, not through the
terminals-data:/app/datamount attached to the orchestrator. -
Inspect the effective database path:
docker exec terminals python -c \ 'from terminals.config import settings; print(settings.database_url)' -
Stop and recreate the orchestrator.
-
Observe that policies are gone and
/p/<policy-id>/...requests return 404.
Expected behavior
The documented volume configuration should persist:
- The orchestrator database and policies.
- Per-user
/home/userworkspaces. - Context-specific workspaces.
A user should not need to mount a host directory at the exact same absolute path
inside the orchestrator container.
Restarting the orchestrator should not remove policies or make existing Open
WebUI terminal connections return 404.
Actual behavior
- The default SQLite database may be outside
/app/data. - The workspace path is resolved inside the orchestrator and then reused as
though it were a Docker-host path. - A normal host-to-container mapping such as
/host/data:/app/datacannot work for sibling-container bind sources. - Policies can disappear on restart.
- User workspaces can be stored in an unintended host/VM path rather than the
configured volume.
Current workaround
Persist the database separately and mount the workspace directory at the same
absolute path in both namespaces:
docker run --rm \
--name terminals \
--network open-webui-terminals \
-p 127.0.0.1:3001:3000 \
-e TERMINALS_BACKEND=docker \
-e TERMINALS_API_KEY=test-key \
-e TERMINALS_DATABASE_URL=sqlite+aiosqlite:////app/data/terminals.db \
-e TERMINALS_DOCKER_NETWORK=open-webui-terminals \
-e TERMINALS_DOCKER_DATA_DIR=/Users/me/.local/share/openwebui-terminals/workspaces \
-v /var/run/docker.sock:/var/run/docker.sock \
-v terminals-data:/app/data \
-v /Users/me/.local/share/openwebui-terminals/workspaces:/Users/me/.local/share/openwebui-terminals/workspaces \
ghcr.io/open-webui/terminals:v0.2.2
The identical source and destination path are necessary only because the current
implementation passes one pathname through both filesystem namespaces.
Suggested fixes
Option 1: Separate host and container workspace paths
Add separate configuration values, for example:
TERMINALS_DOCKER_DATA_DIR=/app/data/terminals
TERMINALS_DOCKER_HOST_DATA_DIR=/host/terminals-data
Then calculate corresponding paths:
local_home_dir = Path(settings.docker_data_dir) / user_id
host_home_dir = Path(settings.docker_host_data_dir) / user_id
Use local_home_dir for orchestrator filesystem operations and
host_home_dir as the Docker bind source:
host_config["Binds"] = [
f"{host_home_dir}:/home/user"
]
The two roots would contain the same relative user/context path but could have
different absolute prefixes.
Option 2: Use Docker named volumes
Create and manage a named Docker volume for each user/context instead of passing
host bind paths. Reset operations could operate through the Docker volume API or
a short-lived helper container.
This avoids host-path translation entirely and works naturally with Docker
Desktop and remote Docker daemons.
Database fix
The official Docker image should explicitly default its database to the
documented volume:
TERMINALS_DATABASE_URL=sqlite+aiosqlite:////app/data/terminals.db
Alternatively, the documentation should explicitly require this setting rather
than implying that mounting /app/data is sufficient.
Additional storage-policy inconsistency
The Docker backend always bind-mounts /home/user, regardless of whether an
Open WebUI policy is shown as ephemeral or persistent.
The policy's storage value only adds StorageOpt to the container writable
layer. It does not control whether /home/user is persisted.
Consequently, selecting ephemeral does not avoid this path problem and does
not discard /home/user when the terminal container is removed. If this is
intentional, the UI/documentation should clarify that the persistent/ephemeral
selection has different semantics on Docker and Kubernetes.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with terminals/backends/docker.py and terminals/config.py, then inspect the Docker deployment documentation and README configuration. Reproduce the path and database behavior with the provided docker exec commands and review how reset and policy persistence use those paths. Done means the documented deployment persists the database, policies, and workspaces across orchestrator restarts without requiring identical paths in both filesystem namespaces.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, python, sqlite
- Domain
- backend, databases, devops, infrastructure
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100