Terminals (Orchestrator)
Terminals is an enterprise orchestration layer for Open Terminal that provisions a fully isolated terminal container for every user. Instead of sharing a single container, each person gets their own, complete with separate files, processes, resource limits, and network isolation.
- Need different environments per team? → Policies guide
How it works
The orchestrator sits between Open WebUI and the Open Terminal instances:
- A user activates a terminal in Open WebUI.
- Open WebUI proxies the request to the orchestrator, a service that manages the lifecycle of terminal containers.
- The orchestrator provisions a personal Open Terminal container for that user (or reconnects to an existing one).
- All traffic is proxied through the orchestrator. The user never connects to their container directly.
- Idle containers are automatically cleaned up after a configurable timeout. Data optionally persists across sessions.
The orchestrator also exposes the same OpenAPI-based tool interface as Open Terminal, so the AI can execute commands, read files, and run code, all scoped to the requesting user's container.
Deployment
- Docker
- Kubernetes Operator
Prerequisites
- Docker Engine installed and running
- Open WebUI running (or ready to deploy alongside)
- Open WebUI Enterprise License (required for production use)
Quick start with Docker Compose
This Compose file deploys Open WebUI and the Terminals orchestrator together.
services:
open-webui:
image: ghcr.io/open-webui/open-webui:main
ports:
- "3000:8080"
environment:
- >-
TERMINAL_SERVER_CONNECTIONS=[{
"id": "terminals",
"name": "Terminals",
"enabled": true,
"url": "http://terminals:3000",
"key": "${TERMINALS_API_KEY}",
"auth_type": "bearer",
"config": {
"access_grants": [{
"principal_type": "user",
"principal_id": "*",
"permission": "read"
}]
}
}]
volumes:
- open-webui:/app/backend/data
networks:
- webui
depends_on:
- terminals
terminals:
image: ghcr.io/open-webui/terminals:latest
environment:
- TERMINALS_BACKEND=docker
- TERMINALS_API_KEY=${TERMINALS_API_KEY}
- TERMINALS_IMAGE=ghcr.io/open-webui/open-terminal:latest
- TERMINALS_NETWORK=open-webui-network
- TERMINALS_IDLE_TIMEOUT_MINUTES=30
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- terminals-data:/app/data
networks:
- webui
volumes:
open-webui:
terminals-data:
networks:
webui:
name: open-webui-networkSet the shared API key in a .env file next to your Compose file:
TERMINALS_API_KEY=change-me-to-a-strong-random-value
Then start everything:
docker compose up -dOpen WebUI will be available at http://localhost:3000. When any user activates a terminal, the orchestrator provisions their personal container automatically.
The orchestrator needs access to the Docker socket (/var/run/docker.sock) to manage containers. For production, use a Docker socket proxy like Tecnativa/docker-socket-proxy to restrict the API calls it can make.
Configuration reference
| Variable | Default | Description |
|---|---|---|
TERMINALS_BACKEND | docker | Backend type. Set to docker for this deployment mode. |
TERMINALS_API_KEY | (empty) | Shared secret for authenticating requests from Open WebUI. Required. |
TERMINALS_IMAGE | ghcr.io/open-webui/open-terminal:latest | Default container image for user terminals. |
TERMINALS_PORT | 3000 | Port the orchestrator listens on. |
TERMINALS_HOST | 0.0.0.0 | Address the orchestrator binds to. |
TERMINALS_NETWORK | (empty) | Docker network for user containers. When set, containers communicate by name. |
TERMINALS_DOCKER_HOST | 127.0.0.1 | Address for published container ports. Only relevant without TERMINALS_NETWORK. |
TERMINALS_DATA_DIR | data/terminals | Host directory for per-user workspace data. |
TERMINALS_IDLE_TIMEOUT_MINUTES | 0 (disabled) | Minutes of inactivity before a container is stopped. Set to 30 for typical usage. |
TERMINALS_MAX_CPU | (empty) | CPU limit for user containers (e.g., 2). |
TERMINALS_MAX_MEMORY | (empty) | Memory limit for user containers (e.g., 4Gi). |
TERMINALS_OPEN_WEBUI_URL | (empty) | If set, validates incoming JWTs against this Open WebUI instance instead of using TERMINALS_API_KEY. |
Container lifecycle details
Naming. Containers are named terminals-{policy_id}-{user_id}, making them easy to filter with docker ps --filter "label=managed-by=terminals".
Health checks. After creating a container, the orchestrator polls its /health endpoint until it returns HTTP 200 (up to 15 seconds). Only then does it start proxying traffic.
Reconciliation. If the orchestrator restarts, it rediscovers existing running containers by their labels and recovers their API keys from the container configuration. This prevents duplicate containers from being created.
Conflict handling. If a container with the same name already exists (e.g., from a previous failed cleanup), the orchestrator force-removes the old container and retries up to 3 times.
Limitations
- Single host. All user containers run on one Docker host. For high availability or larger teams, use the Kubernetes Operator backend.
- No built-in HA. If the orchestrator goes down, active terminal sessions are interrupted (though containers keep running and are reconciled on restart).
- Docker socket required. The orchestrator needs access to the Docker socket to manage containers.
Prerequisites
- A running Kubernetes cluster (v1.24+)
- Helm v3 installed
kubectlconfigured to access your cluster- Open WebUI Enterprise License (required for production use)
Deploy with Helm
The Open WebUI Helm chart includes Terminals as an optional subchart. Add a terminals section to your values file:
# values.yaml
terminals:
enabled: true
apiKey: "" # Auto-generated if left empty
crd:
install: true
operator:
image:
repository: ghcr.io/open-webui/terminals-operator
tag: latest
orchestrator:
image:
repository: ghcr.io/open-webui/terminals
tag: latest
backend: kubernetes-operator
terminalImage: "ghcr.io/open-webui/open-terminal:latest"
idleTimeoutMinutes: 30Then install or upgrade:
helm upgrade --install open-webui open-webui/open-webui \
-f values.yaml \
--namespace open-webui --create-namespaceWhen terminals.enabled is true, the chart automatically sets TERMINAL_SERVER_CONNECTIONS to point at the in-cluster orchestrator. No manual connection setup is needed.
Verify
# Check that all pods are running
kubectl get pods -n open-webui -l app.kubernetes.io/part-of=open-terminal
# Check that the CRD is installed
kubectl get crd terminals.openwebui.comWhat gets deployed
When terminals.enabled: true, the chart creates:
| Resource | Purpose |
|---|---|
CRD (terminals.openwebui.com) | Defines the Terminal custom resource |
| Operator Deployment | Kopf controller that watches Terminal CRs and provisions Pods, Services, PVCs, Secrets |
| Orchestrator Deployment + Service | FastAPI service that receives requests from Open WebUI and proxies to user Pods |
| Secret | Shared API key (auto-generated if not provided) |
For each user terminal, the operator creates a Pod, Service, Secret (API key), and optionally a PVC for persistent storage.
Lifecycle
When a user activates a terminal, the orchestrator creates a Terminal CR. The operator provisions a Pod with a Service, Secret, and optional PVC. Once the Pod passes readiness checks, the orchestrator proxies traffic to it.
When a terminal has been idle longer than idleTimeoutMinutes, the operator deletes the Pod but keeps the PVC and Secret. On the next request, a fresh Pod is created with the same PVC reattached, so user data persists across idle cycles.
# List all terminals
kubectl get terminals -n open-webui
# Inspect a specific terminal
kubectl describe terminal <name> -n open-webui
# Delete a terminal (child resources are garbage-collected automatically)
kubectl delete terminal <name> -n open-webuiMonitoring
# Operator logs
kubectl logs -n open-webui deployment/<release>-terminals-operator --tail=50
# Orchestrator logs
kubectl logs -n open-webui deployment/<release>-terminals-orchestrator --tail=50Terminal CRD reference
Spec fields
| Field | Type | Default | Description |
|---|---|---|---|
userId | string | (required) | Open WebUI user ID |
image | string | ghcr.io/open-webui/open-terminal:latest | Container image |
resources.requests.cpu | string | 100m | CPU request |
resources.requests.memory | string | 256Mi | Memory request |
resources.limits.cpu | string | 1 | CPU limit |
resources.limits.memory | string | 1Gi | Memory limit |
idleTimeoutMinutes | integer | 30 | Idle timeout before Pod is stopped |
packages | array | [] | Apt packages to pre-install |
pipPackages | array | [] | Pip packages to pre-install |
persistence.enabled | boolean | true | Enable persistent storage |
persistence.size | string | 1Gi | PVC size |
persistence.storageClass | string | (cluster default) | Storage class |
Status fields
| Field | Description |
|---|---|
phase | Pending, Provisioning, Running, Idle, or Error |
podName | Name of the terminal Pod |
serviceUrl | In-cluster URL for the terminal |
apiKeySecret | Secret holding the terminal's API key |
lastActivityAt | Timestamp of last proxied request |
Full Helm values reference
| Key | Default | Description |
|---|---|---|
terminals.enabled | false | Enable the Terminals subchart |
terminals.apiKey | (empty) | Shared API key (auto-generated if empty) |
terminals.existingSecret | (empty) | Pre-existing Secret name (key: api-key) |
terminals.crd.install | true | Install the Terminal CRD |
terminals.operator.image.repository | ghcr.io/open-webui/terminals-operator | Operator image |
terminals.operator.image.tag | latest | Operator image tag |
terminals.operator.replicaCount | 1 | Operator replicas |
terminals.orchestrator.image.repository | ghcr.io/open-webui/terminals | Orchestrator image |
terminals.orchestrator.image.tag | latest | Orchestrator image tag |
terminals.orchestrator.backend | kubernetes-operator | Backend type |
terminals.orchestrator.terminalImage | ghcr.io/open-webui/open-terminal:latest | Default image for user Pods |
terminals.orchestrator.idleTimeoutMinutes | 30 | Idle timeout (minutes) |
terminals.orchestrator.service.type | ClusterIP | Orchestrator Service type |
terminals.orchestrator.service.port | 8080 | Orchestrator Service port |
RBAC requirements (manual install only)
If not using the Helm chart, the operator's ServiceAccount needs a ClusterRole with:
| Resource | Verbs |
|---|---|
terminals.openwebui.com | get, list, watch, create, update, patch, delete |
pods, services, persistentvolumeclaims, secrets | get, list, watch, create, update, patch, delete |
events | create |
configmaps, leases | get, list, watch, create, update, patch |
Authentication
The orchestrator supports three authentication modes:
| Mode | When to use | How to configure |
|---|---|---|
| Open WebUI JWT | Production. The orchestrator validates tokens against your Open WebUI instance. | Set TERMINALS_OPEN_WEBUI_URL on the orchestrator to your Open WebUI URL. |
| Shared API key | Standard. Open WebUI includes a shared secret in every request. | Set TERMINALS_API_KEY to the same value on both Open WebUI and the orchestrator. |
| Open | Development only. No authentication. Do not use in production. | Leave both TERMINALS_OPEN_WEBUI_URL and TERMINALS_API_KEY unset. |
When deployed via Docker Compose or Helm, the shared API key is configured automatically between Open WebUI and the orchestrator.
Troubleshooting
Terminal won't start
- Check orchestrator logs. The orchestrator logs the full provisioning flow, including image pull and container creation. Look for errors related to image availability or resource limits.
- Verify the API key. Ensure
TERMINALS_API_KEYmatches between Open WebUI and the orchestrator. A mismatch causes silent auth failures. - Check image pull access. If using a private container registry, make sure the orchestrator (Docker) or cluster (Kubernetes) has pull credentials configured.
Authentication failures
- If using JWT mode, confirm
TERMINALS_OPEN_WEBUI_URLpoints to a reachable Open WebUI instance. - If using API key mode, confirm the key is set identically on both sides. Check for extra whitespace or newlines.
- Check the orchestrator logs for
401or403responses.
Container is reaped too quickly
Increase TERMINALS_IDLE_TIMEOUT_MINUTES (or idle_timeout_minutes in a policy). The default is 0 (disabled), but if set too low, containers may be cleaned up while users are still working. A value of 30 is typical.
Connection refused
- Docker: ensure
TERMINALS_NETWORKis set so containers can communicate by name. Without it, containers use published ports and theTERMINALS_DOCKER_HOSTaddress must be reachable. - Kubernetes: verify the orchestrator Service is accessible from the Open WebUI Pod. Run
kubectl get svc -n open-webuito confirm the service exists.
Further reading
- Multi-User Setup: comparison of isolation approaches
- Security best practices
- Configuration reference: all Open Terminal container settings
License
Terminals requires an Open WebUI Enterprise License for production use. See the Terminals repository for details.