CodeMote 会话放进远端容器 / 本地只保留输入和显示

文章导读
CodeMote 这类“会话跑在远端容器、本地只负责输入和显示”的用法,核心是把容器当成一次性的隔离运行环境:容器里要有能拉起会话的最小 shell 和工具链,项目目录通过挂载从宿主带进去,本地编辑的读写要能确认落在挂载目录而不是容器可写层。判断标准也很直接——容器删掉重建后,代码还在宿主上,会话没了可以重新开一次;如果代码跟着容器一起消失了,说明挂载或工作目录配置有问题。
📋 目录
  1. Ⅰ 容器内需要的 shell、工具链与目录结构
  2. Ⅱ 把工作目录挂进容器的配置骨架
  3. Ⅲ 本地接进来后确认读写落在挂载目录
  4. Ⅳ 容器重启或重建后会话与文件的去向
  5. Ⅴ 容器里做不到的操作
A A

CodeMote 这类“会话跑在远端容器、本地只负责输入和显示”的用法,核心是把容器当成一次性的隔离运行环境:容器里要有能拉起会话的最小 shell 和工具链,项目目录通过挂载从宿主带进去,本地编辑的读写要能确认落在挂载目录而不是容器可写层。判断标准也很直接——容器删掉重建后,代码还在宿主上,会话没了可以重新开一次;如果代码跟着容器一起消失了,说明挂载或工作目录配置有问题。

把远端容器当隔离运行环境:容器内只装 shell、工具链和必要目录,项目目录用挂载方式从宿主带进去,本地输入输出都走这条挂载路径。适用场景是希望环境可随时重建;操作动作是配置挂载并固定工作目录;验证方式是容器内外对照建文件、改文件,再重启或重建容器看哪些文件还在。边界是容器可写层不保证保留,需要宿主设备、图形界面或更高权限的操作要切回本地。

容器内需要的 shell、工具链与目录结构

基础镜像的选择思路是先保证交互可用,再考虑体积。通常选带包管理器的精简发行版,例如 debian slim 或 alpine;用 alpine 时要留意 musl 和 glibc 的差异,某些预编译二进制在容器里会直接报找不到动态库。如果项目本身对运行时版本敏感,让容器里的语言运行时版本和宿主保持一致,能减少“本地能跑、容器里报错”的排查成本。

镜像构建完成后,进容器做一次基础命令可用性检查,比只看启动日志更可靠:

docker run `--rm` -it your-base:tag sh -lc '
  echo "shell: $(readlink -f /bin/sh)"
  for c in git curl python3 node make; do
    command -v $c >/dev/null 2>&1 && echo "ok   $c" || echo "miss $c"
  done
  id
  echo "PATH=$PATH"
'

目录结构建议固定成三块,便于后面写挂载和排查:

  • /workspace:项目工作目录,由宿主挂载进来,会话启动后的 working_dir 指向这里。
  • /home/<user>:会话用户的家目录,可以放 shell 配置、工具缓存;这部分通常是容器层或独立数据卷,不要指望它保存代码。
  • /tmp、/var/log:临时文件和日志,重启后按容器策略处理,不要在这里放需要留存的内容。

容器内的用户 uid/gid 需要结合宿主确认。宿主目录属主是 1000 而容器内用户是别的 uid 时,容器里写文件会出现权限拒绝,这类问题用 id 对照宿主的 ls -ln 就能看出来。

把工作目录挂进容器的配置骨架

让本地代码在容器内可见可写,靠的是 bind mount 把宿主目录映射到容器的工作目录。下面是一个可替换的编排骨架,重点看 working_dir 和 volumes 两段:

services:
  codemote:
    image: your-base:tag
    working_dir: /workspace
    user: "1000:1000"
    volumes:
      - ${LOCAL_PROJECT_DIR:-./project}:/workspace
      - codemote-home:/home/codemote
    environment:
      - HOME=/home/codemote
    stdin_open: true
    tty: true
    command: ["sh", "-lc", "while :; do sleep 3600; done"]

volumes:
  codemote-home:

如果用 docker run 直接起,等价的关键参数是:

docker run -d `--name` codemote \
  -w /workspace \
  -u 1000:1000 \
  -v "$LOCAL_PROJECT_DIR:/workspace" \
  -v codemote-home:/home/codemote \
  -e HOME=/home/codemote \
  your-base:tag sh -lc 'while :; do sleep 3600; done'

替换项是镜像标签、宿主项目路径和 uid/gid。SELinux 开启的宿主上,bind mount 可能需要加 :z 或 :Z 后缀;加了之后权限行为会变,需要先在测试目录确认再用于正式项目。

挂载是否生效,不看启动日志,看容器实际挂载表:

docker inspect -f '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' codemote
docker exec codemote sh -lc 'mount | grep workspace; df -h /workspace; ls -la /workspace | head'

如果 ls /workspace 是空的而宿主目录里有文件,先怀疑宿主路径变量没展开、路径写错,或者被别的空目录覆盖。

CodeMote 会话放进远端容器 / 本地只保留输入和显示

本地接进来后确认读写落在挂载目录

本地终端只是输入和显示,真正要确认的是:容器里的写入到底落在宿主目录,还是落在容器的可写层。区分办法是容器内外对照建文件、改文件,再用 stat 看 inode 和修改时间是否一致。

# 容器内创建并写入
docker exec codemote sh -lc 'echo hello-from-container > /workspace/from-container.txt'
docker exec codemote sh -lc 'stat -c "%i %s %y %n" /workspace/from-container.txt'

# 宿主侧查看同一个文件
stat -c "%i %s %y %n" "$LOCAL_PROJECT_DIR/from-container.txt"
cat "$LOCAL_PROJECT_DIR/from-container.txt"

再做一次反向验证,宿主编译或编辑、容器里读取:

echo hello-from-host > "$LOCAL_PROJECT_DIR/from-host.txt"
docker exec codemote sh -lc 'cat /workspace/from-host.txt; stat -c "%i %y" /workspace/from-host.txt'

两个方向都能对上,说明读写确实经过挂载目录。反例也很容易构造:往 /home/codemote/note.txt 写内容,宿主对应位置找不到,说明那份改动在容器层或数据卷里,本地看不到。常用编辑器的文件监听在 bind mount 上有时会有延迟,判断是否落盘以 stat 和 cat 的结果为准,不要只看编辑器提示。

容器重启或重建后会话与文件的去向

这里容易被误解的地方是:容器“重启”和“重建”是两种结果。对同一个容器执行 restart,可写层通常还在,/tmp 里的文件可能保留;而 docker rm 后再 run,是一次全新容器,可写层的内容全部消失,只有 bind mount 和具名数据卷的内容留下。会话进程本身随容器停止而结束,重启后需要重新连一次。

对照检查可以一次做完:

# 分别在挂载层和容器层各建一个文件
docker exec codemote sh -lc 'date > /workspace/keep.txt; date > /tmp/temp.txt'

# 重启同一个容器
docker restart codemote
docker exec codemote sh -lc 'ls -l /workspace/keep.txt /tmp/temp.txt'

# 删除并重建容器(挂载参数与之前保持一致)
docker rm -f codemote
docker run -d `--name` codemote -w /workspace -u 1000:1000 \
  -v "$LOCAL_PROJECT_DIR:/workspace" \
  -v codemote-home:/home/codemote \
  your-base:tag sh -lc 'while :; do sleep 3600; done'

docker exec codemote sh -lc 'ls -l /workspace/keep.txt; ls -l /tmp/temp.txt 2>/dev/null || echo "temp.txt 已随旧容器消失"'

预期看到的行为是:挂载目录里的文件在重建后仍在,容器层里的文件没了。数据卷 codemote-home 属于中间情况,它会跨重建保留,但删卷就没了,所以不要把它当作代码的备份位置。会话状态如果只存在容器进程内存里,重启后需要重连或重新拉起,这一点在配置里预留启动命令会更省事。

容器里做不到的操作

容器共享的是宿主内核,不是一台独立机器,所以有一类操作在容器里天然做不到,需要切回宿主环境执行。

  • 需要宿主设备的操作:USB、串口、摄像头、部分 GPU 相关能力。设备节点不会自动出现在容器里,需要显式映射;不映射时 ls /dev 看不到目标设备。
  • 需要图形界面或本地显示的操作:依赖 X11、Wayland、剪贴板和桌面环境的工具,通常要额外做显示转发,否则只能报连接失败。
  • 需要更高权限或宿主级管理的操作:加载内核模块、管理宿主 systemd 服务、修改宿主网络与防火墙、挂载宿主文件系统、访问宿主 dbus。容器里 systemctl 一般不可用,modprobe 也会失败。
  • 需要面向宿主物理路径的操作:宿主别的目录、外接磁盘、宿主侧密钥代理,只有在配置了对应挂载或转发之后才可见。

切换回宿主环境的判断条件可以概括成一句话:操作对象属于宿主设备、宿主服务或宿主显示时,就不在容器职责范围内。先用 ls /dev、command -v systemctl、ls /tmp/.X11-unix 这类命令确认边界,再决定是补挂载参数,还是直接在宿主上动手。补挂载能解决一部分问题,但不能把容器变成宿主,配置时按“容器内只做隔离运行、宿主保留设备和权限”来划分,后面维护会清楚很多。