Remote terminal leaks SSH/PTY sessions when WebSocket closes during SSH handshake
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 37.4k
- Forks
- 3k
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 73
Description
Summary
In self-hosted Dokploy, closing, refreshing, or reconnecting a remote terminal while the backend SSH connection is still being established can leave SSH sessions and interactive PTY shells running on the target server.
Repeated terminal operations accumulate sshd-session, bash, and PTY processes.
Environment
- Dokploy: v0.29.8
- Deployment: self-hosted Docker
- Terminal type: remote server/container terminal
- Target: remote Linux server
Observed behavior
The Dokploy container maintained approximately 200 established SSH connections to the same remote server.
On the remote server we observed:
- approximately 198 SSH sessions originating from Dokploy
- approximately 198 interactive PTYs
- approximately 200
bashprocesses - zero zombie processes
- sessions remaining alive for more than one hour
The SSH connections were owned by the Dokploy Node.js process using ssh2, not by separate SSH client processes.
Expected behavior
When the browser WebSocket closes, Dokploy should immediately terminate:
- the pending or established
ssh2client; - the SSH channel/PTY;
- the remote shell;
- all related event listeners and timers.
No remote SSH/PTY session should remain after the terminal WebSocket is closed.
Reproduction / trigger
- Run self-hosted Dokploy with a remote server configured.
- Open a remote server or remote container terminal.
- While the terminal is connecting or reconnecting, close the terminal, navigate away, refresh the page, or interrupt the network connection.
- Repeat the operation several times.
- Inspect the target server's SSH sessions and PTY processes.
The issue is more likely to occur when the remote SSH handshake is delayed or interrupted.
Suspected cause
The frontend cleanup only closes the WebSocket when:
webSocket.readyState === WebSocket.OPEN
This does not clean up a connection that is still connecting.
On the backend, the WebSocket close handler for the remote SSH path is registered inside the SSH ready/exec callback. If the WebSocket closes before SSH emits ready, the close event can be missed. The SSH connection may then complete and create a remote shell without a live browser terminal.
The same lifecycle pattern is present in the v0.30.2 source:
- https://github.com/Dokploy/dokploy/blob/v0.30.2/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx
- https://github.com/Dokploy/dokploy/blob/v0.30.2/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx
- https://github.com/Dokploy/dokploy/blob/v0.30.2/apps/dokploy/server/wss/docker-container-terminal.ts
- https://github.com/Dokploy/dokploy/blob/v0.30.2/apps/dokploy/server/wss/terminal.ts
Suggested fix
- Register WebSocket cleanup immediately after creating the SSH client, before calling
connect(). - Make cleanup idempotent and safe for pending, established, and PTY-created states.
- Destroy pending SSH clients when the WebSocket closes.
- Do not call
shell()orexec()if the WebSocket is already closing or closed. - Add an SSH handshake timeout.
- Add a regression test for WebSocket closure before SSH
ready. - Verify that normal terminal exit closes both the browser WebSocket and the remote PTY.
Related work
Contributor guide
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 the four referenced terminal files: docker-terminal.tsx, terminal.tsx, docker-container-terminal.ts, and terminal.ts. Trace cleanup when the WebSocket closes before SSH emits ready, then add a regression test for that handshake timing and verify normal terminal exit. Done means pending and established SSH, PTY, shell, listeners, and timers are cleaned up without creating a session after the WebSocket closes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, node.js, typescript
- Domain
- backend, devops
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100