最近,为了在别人的工具上跑实验,笔者需要在他们提供的 docker 镜像的容器中做一些轻度的开发。然而棘手的是,由于时间久远,这个容器的宿主 OS 是 ubuntu 14.04,其 glibc 版本过于陈旧,以至于 vscode 的 remote-ssh 插件无法连接到容器当中。
解决方案一是古法纯命令行开发,对于底力深厚的人来说自然不在话下,可对于用惯了现代 IDE 以及 AI 的我来说开发效率会大打折扣。并且,想在这么老的系统装上 Claude Code 等现代命令行 coding agents 也是很困难的,因为它几乎肯定也无法运行新版 Claude Code CLI 所需的 Node.js 运行时环境(v18+)。
方案二是用 vscode 官网提供的 workaround[1]强行让 vscode 连进去,但因为涉及交叉编译 sysroot 这种操作,笔者不太想去尝试。
笔者最终采用的方案三是,将容器内的开发目录(假设为 dev)先拷贝到 host 上(假设为 dev-host),然后再利用 docker 的绑定挂载机制将 dev-host 挂载回容器中。其中 host 的环境是现代化的,因此可以方便地用 vscode 等 IDE 直接打开 mnt,并正常地使用 host 上的各种现代化工具,例如 copilot。此外,即使容器不小心被删除,由于我们的文件本体保存在 host 上,因此不会造成数据丢失。
但实际上还需要做一步工作,即设置好文件的权限,让我们实现以下三个目标:
- 在容器内跑实验脚本,并且防止在 host 上误操作
- 在 host 上做一些辅助性工作,例如跑数据处理的 python 脚本
- container 和 host 两端都对文件拥有读/写权限,方便开发
以下是具体的 step-by-step 记录:
第 0 步,创建一个临时容器 tmp_container,将开发目录拷贝到 host 上:
docker run -d --name tmp_container <image_name>docker cp tmp_container:/path/to/dev /path/to/dev-hostdocker stop tmp_container && docker rm tmp_container
注意拷贝出来之后,在 host 上目录的 ownership 是 host user,不是 container user。
第 1 步,再创建一个新容器,设置 bind mount :
docker run -d \ # detached mode to run in background --name <container_name> \ # -p <port_on_host>:<port_inside_container> \ optional ssh tun -v /path/to/dev-host:/path/to/dev \ # bind mount, 必须写绝对路径! <image_name> \ # /usr/sbin/sshd -D # launch sshd process(optional)
(docker -v 背后的原理实际上就是 Linux 内核的绑定挂载 mount --bind)
这时,可以用 host 的 vscode 直接打开 mnt 开发了,并且在 host 的终端里运行 docker exec -it <container_name> bash 或者 ssh 连接到容器内运行。
但事情并没有那么简单,这是因为 mount 一个目录时,Linux 内核不仅直接映射了文件路径,还保留了文件系统层面的数字 ID(UID 和 GID)。假设 host user 的 UID 是 1019,而容器内预设的用户 UID 是 1000,我们第 0 步拷贝得到的 host 目录的 owner 都是 1019。然而在容器内用户的 uid 是 1000,它会发现这个文件的 owner 不是自己,如果文件的 other 权限位没有x,容器内的程序就会报错 Permission Denied。
第 2 步,在 host 上查看用户的 uid 和 gid:
id # 查看 host user 的 uid 和 gid# example output: uid=1019(username) gid=1019(username) groups=1019(username),27(sudo),998(docker)
第 3 步,在容器内创建一个具有相同 gid 的 usergroup,并将容器用户加进去
# Create the hostgroup with matching GIDdocker exec <container_name> groupadd -g 1019 hostgroup# Add the container user to this groupdocker exec <container_name> usermod -aG hostgroup <container_user># check that the container user is now in the hostgroupdocker exec <container_name> id <container_user># example output: uid=1000(username) gid=1000(username) groups=1000(username),27(sudo),1019(hostgroup)
第 4 步,更改目录所有权。我们假设容器 uid 为 1000,host uid 为 1019。为了达成前文所述的三个目标,我们需要将目录的 ownership 设置为 1000:1019。这样一来,container user 的身份是 owner,host user 的身份是 owner group。在 host 上:
# Set ownership (user:group)sudo chown -R 1000:1019 /path/to/dev-host
由于 chown 通常需要 root 权限,为了避免在 host 上使用 sudo,我们可以利用容器内的 root 权限来“隔空”修改。哪怕 host 用户是普通用户,只要能运行 docker,就能通过容器修改挂载卷的权限:
# Set ownership (user:group)docker exec -u 0 chown -R 1000:1019 /path/to/dev# -u 0 是确保以容器内的 root 运行
第 5 步,更改目录和文件的权限模式(Permission Mode)。我们的需求可以整理成一张表格:
| | 权限位 | 实际能力 |
|---|
| | rwx | 全能 |
| | rw- | 开发(VSCode 编辑、Copilot 辅助、Python处理),但在终端无法直接运行二进制 |
| | 2775 | 开启 SGID,确保新建文件自动属于 Group 1019,双方都能持续访问 |
这里的 SGID 是 Linux 中的一种特殊权限位,组标识位。其作用原理为:
- 对目录:在该目录下创建的新文件/子目录会继承目录的所属组
# Set directory permissions with setgid bit (2775).find /path/to/dev-host -type d -exec chmod 2775 {} \;# Set file permissions to find /path/to/dev-host -type f -exec chmod 0764 {} \;# 测试docker exec <container_name> ls -na /path/to/dev# for directory, should see "drwxrwsr-x ... 1000 1019 ..." # for files, should see "-rwxrw-r-- ... 1000 1019 ..."
最后,第 6 步,配置 umask 以确保持续可写。容器默认的 umask 是 022,那么它创建的新文件权限将是 755 (rwxr-xr-x) 或 644 (rw-r--r--),这意味着 group(host) 只有读权限,没有写权限。可以在容器的启动脚本或 .bashrc 中加入:
umask 002
这样容器创建的文件默认权限就是 rw-rw-r--,host 完全可编辑。
最后总结一下。完成上述配置后,我们达成了以下目标:
- 容器视角 (UID 1000):我是所有文件的 Owner,拥有
rwx,编译、运行畅通无阻。 - 宿主机视角 (GID 1019):我是所有文件的 Group 成员,且文件权限是
rw-,我可以随意通过 VSCode 修改代码、保存文件。 - 新建文件:得益于 SGID,无论谁新建文件,Group ID 都是 1019,双方都能持续读写,不再出现权限分裂。
References
- vscode 官网提供的 workaround — https://code.visualstudio.com/docs/remote/faq#_can-i-run-vs-code-server-on-older-linux-distributions ↩︎