You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NodeJS+WebSocket连接Docker容器Web终端无法输入命令求助

解决Web终端无法向Docker容器输入命令及支持交互式命令的问题

问题核心分析

当前代码存在三个关键问题导致输入无响应、交互式命令失效:

  • container.modem.demuxStream直接将Docker输出流导向Socket.IO的socket,但Socket.IO的socket并非标准Node.js可写流,数据传递逻辑不匹配
  • 未设置StdinOnce: false,导致容器stdin在首次输入后自动关闭,无法持续接收输入
  • 缺少终端尺寸同步机制,vim、top等交互式命令依赖终端尺寸信息才能正常渲染

修改后端代码

const express = require('express');
const http = require('http');
const { Server } = require("socket.io");
const Docker = require('dockerode');
const docker = new Docker({ socketPath: '/var/run/docker.sock' });

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  transports: ['websocket'],
  allowEIO3: true
});

app.use(express.static('public'));

io.on('connection', async (socket) => {
  console.log('A user connected');
  let container = null;
  let stream = null;

  try {
    // 创建容器:添加StdinOnce保持stdin持续打开
    container = await docker.createContainer({
      Image: 'ubuntu',
      Cmd: ['/bin/bash'],
      AttachStdin: true,
      AttachStdout: true,
      AttachStderr: true,
      Tty: true,
      OpenStdin: true,
      StdinOnce: false // 关键参数:禁用stdin自动关闭
    });

    await container.start();

    // 附加流:启用hijack和raw模式适配交互式终端
    stream = await container.attach({
      stream: true,
      stdin: true,
      stdout: true,
      stderr: true,
      hijack: true,
      raw: true
    });

    // 转发Docker输出到前端
    stream.on('data', (data) => {
      socket.emit('output', data);
    });

    // 接收前端输入并写入Docker流
    socket.on('input', (data) => {
      if (stream) {
        stream.write(Buffer.from(data));
      }
    });

    // 处理终端尺寸调整
    socket.on('resize', (size) => {
      if (container) {
        container.resize({
          h: size.rows,
          w: size.cols
        }).catch(err => console.error('Resize error:', err));
      }
    });

    // 流结束时清理容器
    stream.on('end', async () => {
      if (container) {
        try {
          await container.stop();
          await container.remove();
        } catch (err) {
          console.error('Cleanup error:', err);
        }
      }
      socket.disconnect();
    });

    // 用户断开时清理容器资源
    socket.on('disconnect', async () => {
      console.log('User disconnected');
      if (container) {
        try {
          await container.stop();
          await container.remove();
        } catch (err) {
          console.error('Disconnect cleanup error:', err);
        }
      }
      stream?.destroy();
    });

  } catch (err) {
    console.error('Initialization error:', err);
    socket.emit('error', err.message);
    socket.disconnect();
  }
});

const port = 3000;
server.listen(port, () => console.log(`Server running on port ${port}`));

前端配套代码(public/index.html)

<!DOCTYPE html>
<html>
<head>
  <title>Web Terminal</title>
  <style>
    #terminal {
      width: 800px;
      height: 600px;
      border: 1px solid #ccc;
      padding: 10px;
      font-family: monospace;
      white-space: pre;
      overflow-y: auto;
    }
  </style>
</head>
<body>
  <div id="terminal"></div>
  <script src="/socket.io/socket.io.js"></script>
  <script>
    const socket = io();
    const terminal = document.getElementById('terminal');
    
    // 解析后端二进制输出并渲染
    socket.on('output', (data) => {
      terminal.textContent += new TextDecoder('utf-8').decode(data);
      terminal.scrollTop = terminal.scrollHeight;
    });

    // 捕获键盘输入并发送到后端
    document.addEventListener('keydown', (e) => {
      let input;
      switch(e.key) {
        case 'Enter':
          input = '\r\n';
          e.preventDefault();
          break;
        case 'Backspace':
          input = '\x7f';
          break;
        case 'Tab':
          input = '\t';
          e.preventDefault();
          break;
        default:
          input = e.key;
      }
      socket.emit('input', input);
    });

    // 发送终端尺寸信息
    function sendTerminalSize() {
      const cols = Math.floor(terminal.offsetWidth / 8);
      const rows = Math.floor(terminal.offsetHeight / 16);
      socket.emit('resize', { cols, rows });
    }

    window.addEventListener('resize', sendTerminalSize);
    sendTerminalSize();

    // 处理错误提示
    socket.on('error', (msg) => {
      terminal.textContent += `\nError: ${msg}\n`;
    });
  </script>
</body>
</html>

关键修改说明

  1. 容器参数优化:添加StdinOnce: false确保stdin持续打开,支持多次输入
  2. 流模式适配:启用hijack: true和raw: true,匹配Docker交互式终端的流协议
  3. 数据传递逻辑:通过Socket.IO事件手动转发输入输出,适配非标准流的Socket.IO通信
  4. 终端尺寸同步:前端监听窗口变化,实时发送终端行列数,后端调用container.resize适配交互式命令渲染
  5. 资源清理:在流结束、用户断开时强制停止并移除容器,避免资源泄漏

内容的提问来源于stack exchange,提问作者dennis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 06:22:51