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 是空的而宿主目录里有文件,先怀疑宿主路径变量没展开、路径写错,或者被别的空目录覆盖。
本地接进来后确认读写落在挂载目录
本地终端只是输入和显示,真正要确认的是:容器里的写入到底落在宿主目录,还是落在容器的可写层。区分办法是容器内外对照建文件、改文件,再用 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 这类命令确认边界,再决定是补挂载参数,还是直接在宿主上动手。补挂载能解决一部分问题,但不能把容器变成宿主,配置时按“容器内只做隔离运行、宿主保留设备和权限”来划分,后面维护会清楚很多。