Docker Container Terminal#
Dokploy's container terminal feature provides an interactive shell session inside a running Docker container, backed by a WebSocket server and rendered with xterm.js.
Key Files#
| File | Role |
|---|---|
docker-terminal.tsx | React client component (xterm + WebSocket) |
docker-container-terminal.ts | Server-side WebSocket handler (/docker-container-terminal) |
docker-terminal-modal.tsx | Modal wrapper with container selection dropdown |
server/api/routers/docker.ts | getContainersByAppNameMatch tRPC route |
packages/server/src/services/docker.ts | Container discovery logic |
server/wss/utils.ts | Input validation helpers (isValidContainerId, isValidShell) |
WebSocket Lifecycle#
The client builds a URL of the form:
ws(s)://<host>/docker-container-terminal?containerId=<id>&activeWay=<shell>[&serverId=<id>][&serviceId=<id>]
On the server, setupDockerContainerTerminalWebSocketServer attaches to the HTTP server with noServer: true and intercepts WebSocket upgrades on that path. At connection time it:
- Validates
containerIdviaisValidContainerId— closes with code 4000 if absent or malformed . - Validates the requested shell via
isValidShell— closes with code 4000 on an unsupported value . - Authenticates the user with
validateRequestand checkscanAccessDockerOverWss. - Forks into local (
node-pty) or remote (SSH2) execution.
Local path — spawns docker exec -it -w / <containerId> <shell> via node-pty; not available in the cloud version . PTY data streams directly to the WebSocket; ws.close triggers ptyProcess.kill() .
Remote path — opens an SSH2 Client connection using the server's stored ipAddress, port, username, and sshKey.privateKey, then runs the same command via conn.exec(..., { pty: true }, ...) . Both stdout and stderr are forwarded to the WebSocket ; ws.close calls stream.end() and conn.end() .
No keep-alive ping — unlike the log-streaming WebSocket, the terminal WebSocket does not send periodic pings. Long idle sessions may be dropped by proxies with short idle timeouts.
Shell Selection#
The UI renders two tabs — Bash and /bin/sh . The selected value (activeWay) is sent as a query parameter; it defaults to "bash" in component state . The server defaults to "sh" if the parameter is absent .
There is no auto-detection fallback. If the user selects bash but the container image doesn't include it, the docker exec call fails and the terminal shows an error. The user must manually switch to /bin/sh.
The server enforces a strict allowlist via isValidShell: only sh, bash, zsh, ash, and their /bin/ prefixed variants are accepted.
Docker Compose: Container Selection Flow#
Before the terminal opens, the user selects a container from a dropdown in DockerTerminalModal. This dropdown is populated by the getContainersByAppNameMatch tRPC query , which accepts appName, appType, and optional serverId.
The service layer handles appType asymmetrically:
"docker-compose"— filters by Docker label:--filter='label=com.docker.compose.project=<appName>'"stack"or undefined — greps container names:| grep '^.*Name: <appName>'
The first container in the result is auto-selected on load . Pressing Escape is suppressed in favor of an explicit confirmation dialog before closing an active session .
Historical Bug: Empty Container Dropdown for Docker Compose (PR #2684)#
Symptom: Opening the terminal modal for a Docker Compose application showed an empty container dropdown, making it impossible to connect.
Root cause: The appType prop was not passed from actions.tsx down to DockerTerminalModal, so the query always used the grep-based fallback. Docker Compose containers use project labels rather than name prefixes, so the grep path returned zero results.
Fix (merged 2025-10-05): actions.tsx now passes appType={data?.composeType || "docker-compose"} to DockerTerminalModal; the modal's props were extended to forward appType to the query. containerId in DockerTerminal was also made optional to prevent hard failures before a container is selected.
Security Notes#
containerIdis validated against a hex-or-name regex to prevent command injection via the URL parameter.- Shell is validated against a fixed allowlist ; anything else closes the connection with code 4000.
- Organization ownership of remote servers is checked before opening SSH connections .
- The local terminal path is blocked in
IS_CLOUDdeployments .