[GH-ISSUE #55] Bug: Docker deployment conflates container-local and Docker-host paths for TERMINALS_DOCKER_DATA_DIR #25

Open
opened 2026-10-18 21:13:32 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @i0ntempest on GitHub (Sep 1, 2026).
Original GitHub issue: https://github.com/open-webui/terminals/issues/55

Description

When the Terminals orchestrator runs inside Docker and manages sibling Open Terminal containers through /var/run/docker.sock, it conflates two filesystem namespaces:

  1. The orchestrator container's filesystem.
  2. 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/docs/blob/main/docs/features/open-terminal/terminals/tab-deployment/Docker.md

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.

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

  1. Create a Docker network:

    docker network create open-webui-terminals
    
  2. 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
    
  3. Configure an orchestrator connection and named policy in Open WebUI.

  4. Activate it so that a user terminal is provisioned.

  5. Inspect the spawned terminal:

    docker ps \
      --filter label=app.kubernetes.io/managed-by=terminals \
      --format '{{.Names}}'
    
    docker inspect <spawned-container> \
      --format '{{json .HostConfig.Binds}}'
    
  6. Observe that the spawned container uses a source resembling:

    /app/data/terminals/<user-id>:/home/user
    

    This is interpreted on the Docker daemon host, not through the
    terminals-data:/app/data mount attached to the orchestrator.

  7. Inspect the effective database path:

    docker exec terminals python -c \
      'from terminals.config import settings; print(settings.database_url)'
    
  8. Stop and recreate the orchestrator.

  9. 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/user workspaces.
  • 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/data cannot 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.

Originally created by @i0ntempest on GitHub (Sep 1, 2026). Original GitHub issue: https://github.com/open-webui/terminals/issues/55 ## Description When the Terminals orchestrator runs inside Docker and manages sibling Open Terminal containers through `/var/run/docker.sock`, it conflates two filesystem namespaces: 1. The orchestrator container's filesystem. 2. 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: ```yaml 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/docs/blob/main/docs/features/open-terminal/terminals/tab-deployment/Docker.md 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`: ```python @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: ```python 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: ```python 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: ```bash 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: ```text /app/data/terminals -> /host/terminals-data/terminals ``` But when the orchestrator asks the Docker daemon to create a sibling container with: ```text /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: ```python 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: ```python _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: ```text /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: ```bash 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: ```text 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: ```python 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 1. Create a Docker network: ```bash docker network create open-webui-terminals ``` 2. Start Terminals using the documented storage layout: ```bash 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 ``` 3. Configure an orchestrator connection and named policy in Open WebUI. 4. Activate it so that a user terminal is provisioned. 5. Inspect the spawned terminal: ```bash docker ps \ --filter label=app.kubernetes.io/managed-by=terminals \ --format '{{.Names}}' docker inspect <spawned-container> \ --format '{{json .HostConfig.Binds}}' ``` 6. Observe that the spawned container uses a source resembling: ```text /app/data/terminals/<user-id>:/home/user ``` This is interpreted on the Docker daemon host, not through the `terminals-data:/app/data` mount attached to the orchestrator. 7. Inspect the effective database path: ```bash docker exec terminals python -c \ 'from terminals.config import settings; print(settings.database_url)' ``` 8. Stop and recreate the orchestrator. 9. 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/user` workspaces. - 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/data` cannot 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: ```bash 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: ```text TERMINALS_DOCKER_DATA_DIR=/app/data/terminals TERMINALS_DOCKER_HOST_DATA_DIR=/host/terminals-data ``` Then calculate corresponding paths: ```python 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: ```python 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: ```text 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.
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
open-webui/terminals#25
No description provided.