通过 SSH 连接 Docker 容器 — Codex App实践记录

1. 目标与约束

1.1 目标

  • 使用 Codex 桌面 App(非 VS Code)进行开发
  • 代码与命令运行在 Docker 容器内,而非宿主机 shell
  • 容器已在运行,不能随意重建

1.2 环境概览(脱敏)

组件 说明
本机 Windows,OpenSSH 客户端
远程宿主机 dgx-server-01(IP:192.168.206.xxx),用户 user_host
容器 名称 my-dev-container,镜像 custom:dev,网络模式 host
项目挂载 宿主机 /mnt/shared/project → 容器 /workspace
SSH 密钥 本机 ~/.ssh/id_rsa,注释 user@local-pc

2. 第一性原理:Codex 远程连接到底在连什么?

2.1 Codex App 的远程模型

Codex App 不支持「Attach to Running Container」这类 Docker 原生操作。它的远程链路是:

复制代码
本机 Codex App
    │  OpenSSH
    ▼
远程目标(必须是一个完整的 SSH 登录环境)
    │  启动 codex app-server(通过 login shell)
    ▼
在该环境中读写文件、执行命令

推论 1: 容器必须对 Codex 呈现为一个 可 SSH 登录的端点,而不是「宿主机 + docker exec」。

推论 2: 远程环境中必须存在 codex CLI ,且版本满足 App 最低要求;App 通过 login shell 的 PATH 找到它。

推论 3: Codex App 只读取本机 ~/.ssh/config 中的 具体 Host 别名 ,不会读取 VS Code 的 devcontainer.json

2.2 两种网络模式的分叉

网络模式 容器 IP 连接方式
bridge(默认) 有独立 IP(如 172.17.0.x 本机 → SSH 宿主机(ProxyJump)→ SSH 容器 IP
host 无独立 IP 容器与宿主机共享网络栈,sshd 监听端口即宿主机端口

本案例容器为 host 模式,因此:

  • 不能在 HostName 里填「容器 IP」(为空)
  • 应在宿主机 IP 上监听非 22 端口 (如 2222),避免与宿主机 sshd 冲突

2.3 与 VS Code Dev Containers 的区别

能力 VS Code Codex App
Attach 已运行容器 �7�3 �7�4
读取 devcontainer.json �7�3 �7�4
SSH 远程项目 �7�3(Remote-SSH) �7�3(Settings → Connections)

若坚持用 Codex 桌面 App ,必须走 SSH 进容器 路线。


3. 整体架构(本案例最终方案)

复制代码
Windows(Codex App + ~/.ssh/config)
    │
    │  ssh sd-jgy  →  192.168.206.xxx:2222  user: root
    ▼
宿主机(host 网络)
    │  端口 2222 由容器内 sshd 监听
    ▼
Docker 容器 my-dev-container
    ├── /workspace(项目目录)
    ├── /usr/sbin/sshd(Port 2222)
    └── codex CLI ≥ 0.141.0(~/.codex 存会话与配置)

4. 实施步骤

4.1 侦察:确认容器网络与挂载

在宿主机执行:

bash 复制代码
# 网络模式与 IP
docker inspect my-dev-container --format \
  'NetworkMode={{.HostConfig.NetworkMode}} IP={{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'

# 挂载路径
docker inspect my-dev-container --format \
  '{{range .Mounts}}{{.Destination}} <- {{.Source}}{{"\n"}}{{end}}'

本案例结果:

  • NetworkMode=hostIP=(空)
  • /workspace <- /mnt/shared/project

4.2 在运行中的容器内配置 sshd

原则: 不停止容器,通过 docker exec 在内部安装并配置 SSH。

bash 复制代码
# 安装 openssh-server
docker exec -u root my-dev-container bash -c '
  apt-get update && apt-get install -y openssh-server
  mkdir -p /var/run/sshd /root/.ssh
  chmod 700 /root/.ssh
'

# 清理 Port 行,只保留 Port 2222(避免 sed 重复替换产生 Port 222222)
docker exec -u root my-dev-container bash -c '
  sed -i "/^Port /d" /etc/ssh/sshd_config
  sed -i "/^#Port /d" /etc/ssh/sshd_config
  sed -i "1i Port 2222" /etc/ssh/sshd_config
  grep -q "^PermitRootLogin" /etc/ssh/sshd_config && \
    sed -i "s/^PermitRootLogin.*/PermitRootLogin prohibit-password/" /etc/ssh/sshd_config || \
    echo "PermitRootLogin prohibit-password" >> /etc/ssh/sshd_config
  grep -q "^PubkeyAuthentication" /etc/ssh/sshd_config || \
    echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config
'

# 验证配置语法
docker exec -u root my-dev-container sshd -t

# 创建 privilege separation 目录(否则 sshd 拒绝启动)
docker exec -u root my-dev-container bash -c '
  mkdir -p /run/sshd /var/run/sshd
  chmod 755 /run/sshd /var/run/sshd
'

# 写入本机公钥(整行替换为实际 id_rsa.pub 内容)
docker exec -u root my-dev-container bash -c 'cat >> /root/.ssh/authorized_keys << "EOF"
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQD****...**** user@local-pc
EOF
chmod 600 /root/.ssh/authorized_keys'

# 启动 sshd
docker exec -u root my-dev-container bash -c 'pkill sshd 2>/dev/null; sleep 1; /usr/sbin/sshd'

# 宿主机本地验证
ssh -p 2222 -o StrictHostKeyChecking=accept-new root@127.0.0.1 "echo OK && ls /workspace"

注意: 在宿主机上 ssh -p 2222 root@127.0.0.1 若提示密码,是因为使用的是宿主机本机密钥 ,未必在 authorized_keys 中。以 Windows 本机 ssh sd-jgy 测试为准。

4.3 本机 Windows SSH 配置

文件:C:\Users\<用户名>\.ssh\config

text 复制代码
Host dgx-server-01
  HostName 192.168.206.xxx
  User user_host
  IdentityFile ~/.ssh/id_rsa
  IdentitiesOnly yes

Host sd-jgy
  HostName 192.168.206.xxx
  Port 2222
  User root
  IdentityFile ~/.ssh/id_rsa
  IdentitiesOnly yes
  StrictHostKeyChecking accept-new

关键原则:

  1. Host sd-jgyHostName 必须是宿主机 IP ,不能写 dgx-server-01 别名去「引用」上一个 Host 块------SSH 不会自动继承。
  2. host 网络模式下,Port 2222 即容器 sshd 在宿主机上的监听端口。
  3. IdentitiesOnly yes 强制使用指定密钥,避免 Windows 试错其他密钥后回落到密码认证。

验证:

powershell 复制代码
ssh sd-jgy "echo OK && ls /workspace"

4.4 Codex App 连接

  1. 设置 → 连接 → 启用 SSH 主机 sd-jgy
  2. 项目 → 远程目录选择 /workspace(或子目录)
  3. 若列表中无该主机,保存 config 后完全重启 Codex App

4.5 docker端口持久化

  1. 在宿主机 dgx1-10 上查看入口
bash 复制代码
docker inspect SD_jgy --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}'
  1. 在容器里加启动钩子
bash 复制代码
docker exec -u root SD_jgy bash -c '
ENTRYPOINT="/opt/nvidia/nvidia_entrypoint.sh"
HOOK="/usr/local/bin/00-start-sshd.sh"

# 写 sshd 启动脚本
cat > "$HOOK" << "EOF"
#!/bin/bash
mkdir -p /run/sshd /var/run/sshd
if [ -x /usr/sbin/sshd ]; then
  if ! ss -tlnp 2>/dev/null | grep -q ":2222"; then
    /usr/sbin/sshd
  fi
fi
EOF
chmod +x "$HOOK"

# 在 nvidia entrypoint 开头注入调用(仅注入一次)
if [ -f "$ENTRYPOINT" ] && ! grep -q "00-start-sshd.sh" "$ENTRYPOINT"; then
  cp "$ENTRYPOINT" "${ENTRYPOINT}.bak"
  sed -i "2i /usr/local/bin/00-start-sshd.sh" "$ENTRYPOINT"
fi
'

5. 问题排查记录

5.1 sshd 配置 Port 格式错误

现象:

text 复制代码
/etc/ssh/sshd_config line 14: Badly formatted port number.
Connection refused

原因: 多次 sed 's/Port 22/Port 2222/' 把已有 2222 变成 222222,且存在多行 Port

解决: 删除所有 Port 行,只保留一行 Port 2222


5.2 sshd 无法启动:缺少 /run/sshd

现象:

text 复制代码
Missing privilege separation directory: /run/sshd

解决:

bash 复制代码
mkdir -p /run/sshd && chmod 755 /run/sshd
/usr/sbin/sshd

5.3 宿主机 SSH 要密码,Windows 是否正常?

测试位置 使用的密钥 预期
宿主机 → 127.0.0.1:2222 宿主机 ~/.ssh/ 可能需密码(未添加宿主机公钥时)
Windows → sd-jgy 本机 id_rsa 应免密成功

容器默认无 root 密码 ,提示密码说明公钥认证未匹配,不是「需要设置密码」。


5.4 Codex CLI 版本不满足

现象(Codex App):

text 复制代码
最低要求版本:0.141.0
当前安装版本:0.130.0

原理: App 通过 SSH 启动远程 codex app-server,CLI 与 App 存在最低版本契约

升级注意:

  • 重装 CLI 不会 删除 ~/.codex/(聊天记录、登录态、配置保留)
  • 不要 执行 rm -rf ~/.codex(除非刻意清空)
5.4.1 npm 升级失败:ENOTEMPTY

现象:

text 复制代码
npm error ENOTEMPTY: directory not empty, rename '.../@openai/codex' -> '.../.codex-xxxxx'

原因: 上次升级中断,npm 无法重命名旧目录,不是 npm 版本过低的典型表现。

解决:

bash 复制代码
npm uninstall -g @openai/codex
rm -rf /usr/local/nvm/versions/node/v18.x.x/lib/node_modules/@openai/codex
rm -rf /usr/local/nvm/versions/node/v18.x.x/lib/node_modules/@openai/.codex-*
npm cache clean --force
npm install -g @openai/codex@0.141.0
bash -lc 'codex --version'
5.4.2 npm 包损坏:缺少平台二进制

现象:

text 复制代码
Error: Missing optional dependency @openai/codex-linux-x64.
Reinstall Codex: npm install -g @openai/codex@latest

原因: 部分安装导致 JS 包装存在,但 @openai/codex-linux-x64 未下载完整。

备选方案:使用独立二进制(推荐,绕过 npm)

bash 复制代码
cd /tmp
curl -fsSL -o codex.tar.gz \
  "https://github.com/openai/codex/releases/latest/download/codex-x86_64-unknown-linux-musl.tar.gz"
tar -xzf codex.tar.gz
install -m 0755 codex-x86_64-unknown-linux-musl /usr/local/bin/codex

export PATH="/usr/local/bin:$PATH"
grep -q '/usr/local/bin' ~/.bashrc || echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.bashrc
bash -lc 'which codex && codex --version'

确保 which codex 指向 /usr/local/bin/codex,而非 nvm 下损坏的 npm 版。


6. 数据与隐私边界

路径 内容 重装 CLI 是否影响
~/.codex/ 登录态、配置、会话/聊天记录 �7�4 不影响(不手动删除时)
node_modules/@openai/codex CLI 程序本体 �7�3 会被卸载/覆盖
~/.ssh/authorized_keys SSH 公钥 �7�4 不影响
Codex App 本机数据 本地项目与线程 �7�4 与容器 CLI 重装无关

7. 运维:容器重启后恢复 SSH

host 模式下,容器内 sshd 配置与 authorized_keys 通常持久保留 (未重建容器时),但 sshd 进程可能停止

bash 复制代码
docker exec -u root my-dev-container bash -c '
  mkdir -p /run/sshd
  pkill sshd 2>/dev/null
  sleep 1
  /usr/sbin/sshd
'
docker exec -u root my-dev-container ss -tlnp | grep 2222

Windows 验证:

powershell 复制代码
ssh sd-jgy "echo OK"

8. 决策树(速查)

复制代码
要在 Codex App 里用容器环境?
├─ 否 → VS Code Dev Containers: Attach to Running Container
└─ 是 → 容器能否配置 sshd?
    ├─ 否 → 暂不可行(或换可 SSH 的容器镜像)
    └─ 是 → 查网络模式
        ├─ bridge → HostName=容器IP, ProxyJump=宿主机
        └─ host   → HostName=宿主机IP, Port=2222(容器 sshd)
            → 本机 config + Codex App 启用该 Host
            → 容器 codex CLI ≥ App 要求版本
            → 项目路径 /workspace

9. 最终检查清单

  • docker inspect 确认网络模式(host / bridge)
  • 容器内 sshd -t 配置合法,Port 仅一行
  • /run/sshd 存在,2222 端口 LISTEN
  • authorized_keys 含本机公钥,权限 600
  • Windows ssh sd-jgy "echo OK" 免密成功
  • bash -lc 'codex --version' ≥ 0.141.0
  • Codex App 设置 → 连接 启用 sd-jgy
  • 项目目录 /workspace 可访问

10. 参考


文档生成日期:2026-06-26 · 隐私信息已脱敏,部署时请替换占位符为实际值。