用 Docker 与 SSH 绕过旧版 Ubuntu 的开发环境兼容问题

Ubuntu 18.04 的 glibc 版本太低时,新版 VS Code Remote SSH Server、Node.js 预编译包等工具可能无法运行。可以把开发环境放进 Docker 容器,再把容器的 22 端口映射出来,用 SSH 像连接普通主机一样进入容器。

问题

最近拿到的一台远程开发机还停在 Ubuntu 18.04 LTS,glibc 版本是 2.27。系统本身能跑,但新版开发工具已经开始绕不过这条系统库边界:

  • VS Code 新版本通过 Remote SSH 连接时,需要在远端启动 VS Code Server。这个 server 对 glibc 版本有要求,Ubuntu 18.04 的 glibc 2.27 太旧,容易在远端安装或启动阶段失败。
  • Node.js 的新版 Linux x64 预编译包也会依赖更新的 glibc。在 Ubuntu 18.04 上用 nvm 或二进制包安装时,可能会遇到 GLIBC_2.28 not found 这类错误;要么降级 Node.js,要么自己编译。
  • fastfetch 在 Ubuntu 18.04 下也不太省心,需要自行编译可执行文件。

所以问题不只是某个 package 装不上,而是旧系统的底层运行库已经跟不上新工具链的默认假设。

如果这台机器完全由自己控制,升级系统最干净。但实习、实验室或公司分配的开发机经常有额外约束:不能随便动系统版本,也不想把原环境改到不可回滚。于是就有了第二条路:宿主机保持不变,把开发环境放进 Docker 容器,再用 SSH 直接连进容器。

两种方案

这里有两个选择:

  1. 直接在远程主机上升级系统。
  2. 在远程主机上运行 Docker 容器,再用 SSH 直接连进这个容器。

第一种方案改的是整台机器,后续维护最简单,但前提是你有权限,也能接受升级带来的影响。第二种方案只是在旧系统上借 Docker 起一个新环境,侵入性更低,也更容易复制给下一台机器。

命令提示符说明

> - LocalHost
$ - Remote Host(Ubuntu 18)
# - Docker Container

下面先放升级系统的路径,再重点记录 Docker SSH forwarding 的做法。

方案一:升级远程主机

能控制整台机器时,升级 Ubuntu 是最直接的办法。下面这组命令会把系统升级到下一个 LTS 版本,过程中会有比较详细的交互提示。

# 更新软件包$ sudo apt-get update$ sudo apt-get upgrade$ sudo apt-get dist-upgrade# 安装升级工具$ sudo apt install update-manager-core# 编辑升级配置文件,末行配置为 lts,保存退出$ sudo vim /etc/update-manager/release-upgradesPrompt=lts# 执行升级,执行后会询问重启,或自行重启$ sudo do-release-upgrade$ lsb_release -aNo LSB modules are available.Distributor ID: UbuntuDescription:    Ubuntu 20.04.6 LTSRelease:        20.04Codename:       focal

方案二:SSH 到 Docker 容器

Docker 方案的直觉很简单:旧系统只负责运行 Docker 和转发端口,真正的开发环境放在容器里。容器内可以使用更新的 Ubuntu、Debian 或项目指定的基础镜像,本地则仍然通过 SSH 连接。

它和“用 Docker 做开发环境”的收益类似:环境配置可以标准化,镜像可以复制,宿主机也更干净。区别在于这里额外把 SSH 服务放进容器,让 VS Code Remote SSH 这类工具可以像连接普通服务器一样连接容器。

构建可 SSH 登录的镜像

先创建用于构建 Docker 镜像的目录,把 .ssh.config.vscode-server 等目录拷进去。这些配置大多可以复用,尤其是 SSH key 和常用 shell 配置。

SSH Forwarding
SSH Forwarding

这个镜像需要做几件事:

  • 安装必要工具:vim、SSH 服务、fish shell 和 fzf。
  • 配置 SSH,允许 root 登录,并设置临时密码。
  • 把主机的 SSH 密钥复制进容器,这样登录时不用输密码。
  • 暴露 22 端口,留给宿主机做端口映射。
安全边界

下面的 root:rootPermitRootLogin yes 适合临时内网开发环境。长期使用时,最好改成单独用户、禁用密码登录,并只保留 key 登录。

FROM artifactory.momenta.works/docker/python:3.11# 安装 vim 和其他必要软件包RUN apt-get update && \    apt-get install -y vim openssh-server fish fzf# 拷贝 SSH  shell 配置COPY ./.ssh /root/.ssh/COPY ./.config /root/.config/# 跳过首次连接确认RUN echo "Host *\n\tHostKeyAlgorithms +ssh-rsa\n\tPubkeyAcceptedKeyTypes +ssh-rsa\n\tStrictHostKeyChecking no" > /root/.ssh/configRUN sed -i 's/^#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_configRUN echo 'root:root' | chpasswdRUN chmod 600 /root/.ssh/id_rsaRUN mkdir /var/run/sshdEXPOSE 22# 启动 SSH daemon serviceENTRYPOINT service ssh start && tail -f /dev/null

一次性在 Dockerfile 里写完镜像配置会比较麻烦。可以先用简单的 Dockerfile 创建容器,进入 container 的 shell,再在容器里安装需要的环境。配好基础开发环境后,用 docker commit 把 container 打包成新的 image。

# 创建容器$ docker run -it --name dev-image --hostname=dk --privileged=true --net=bridge -p 8033:22 debian:bookworm# 进入容器$ docker exec -it dev-image /bin/bash# 安装需要的环境$ apt-get update && \    apt update && \    apt install <xxx># 退出容器$ exit# 提交容器为新的镜像$ docker commit dev-image dev-image:latest

这种做法更像是在容器里手工打草稿:先把环境试出来,再把稳定版本固化成镜像。等依赖关系确定后,再回头整理 Dockerfile,会轻松很多。

构建并运行容器

Dockerfile 准备好后,先构建镜像:

$ docker build -t dev-image:latest .

再运行容器,把容器的 22 端口映射到 Ubuntu 主机的 8033 端口:

$ docker run -it --name ssh --hostname=dk --privileged=true --net=bridge -p 8033:22 dev-image:latest

参数含义如下:

  • --name ssh:设置容器名称为 ssh
  • --hostname=dk:设置容器主机名。
  • --privileged=true:赋予容器特权模式。
  • --net=bridge:使用桥接网络模式。
  • -p 8033:22:把容器 22 端口映射到宿主机的 8033 端口。
挂载数据卷

如果需要持久化数据,可以使用 -v 参数挂载数据卷,例如:-v /path/on/host:/path/in/container

容器启动后,也可以先从宿主机进入容器检查 SSH 服务:

$ docker exec -it <container-name> /bin/bash$ docker exec -it <container-name> /bin/fish

配置本地 SSH

容器端口映射完成后,本地只需要多写一个 SSH Host。下面的 ubuntu 指向宿主机,docker 指向同一台宿主机的 8033 端口,也就是容器的 22 端口。

编辑 ~/.ssh/config 文件:

# Read more about SSH config files: https://linux.die.net/man/5/ssh_configHost ubuntu    HostName 10.21.163.77    User root    IdentityFile C:/Users/gjx/.ssh/id_rsa    StrictHostKeyChecking noHost docker    HostName 10.21.163.77    User root    Port 8033

之后在本地执行 ssh docker,或者在 VS Code Remote SSH 里选择 docker,连接到的就是 Docker 容器。

工作原理

这套连接方式其实只有两层转发:

  1. Windows 先连接 Ubuntu 主机的 8033 端口。
  2. Ubuntu 主机把 8033 端口转发到 Docker 容器的 22 端口。

从本地看,docker 只是一个普通的 SSH Host;从宿主机看,它只是把端口流量转给了某个容器。这个模型记住之后,扩展到多个容器也很自然。

需要连接多个 Docker 容器时,给每个容器映射不同端口,再在 SSH 配置里加对应条目:

Host docker1    HostName 10.21.163.77    User root    Port 8033Host docker2    HostName 10.21.163.77    User root    Port 8034

小结

旧系统上的 glibc 兼容性问题,最后不一定要靠升级宿主机解决。只要宿主机还能稳定运行 Docker,就可以把开发环境移进容器,再通过端口映射和 SSH 配置把它接回本地工作流。

这不是最“纯”的方案:容器里跑 SSH、root 登录、端口映射,都需要自己控制好安全边界。但在不能随便升级远程主机、又想继续使用新版本开发工具时,它是一个足够直接、也足够容易回滚的折中办法。

CompactRelaxed
Normal1.70