open-webui / open-webui/terminals

Bug: Docker deployment conflates container-local and Docker-host paths for `TERMINALS_DOCKER_DATA_DIR`

Open
#55 0 comments 0 reactions 0 assignees View on GitHub

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:

  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.

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

  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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.