﻿---
title: 用 Docker 与 SSH 绕过旧版 Ubuntu 的开发环境兼容问题
date: 2025-04-25
excerpt: Ubuntu 18.04 的 glibc 版本太低时，新版 VS Code Remote SSH Server、Node.js 预编译包等工具可能无法运行。可以把开发环境放进 Docker 容器，再把容器的 22 端口映射出来，用 SSH 像连接普通主机一样进入容器。
tags:
  - Docker
  - SSH
  - VSCode
  - Linux
updated: 2026-06-04 19:50:13
lang: zh-CN
i18n:
  en: /en/docker-ssh-forward
  translation: 2
---

## 问题

最近拿到的一台远程开发机还停在 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 起一个新环境，侵入性更低，也更容易复制给下一台机器。

> [!INFO] 命令提示符说明
> `>` - LocalHost
> `$` - Remote Host（Ubuntu 18）
> `#` - Docker Container

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

### 方案一：升级远程主机

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

```shell
# 更新软件包
$ 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-upgrades
Prompt=lts
# 执行升级，执行后会询问重启，或自行重启
$ sudo do-release-upgrade
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.6 LTS
Release:        20.04
Codename:       focal
```

### 方案二：SSH 到 Docker 容器

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

```mermaid
graph LR
    A[Windows 主机] -->|SSH 直接连接| B[Ubuntu 服务器]

    AA[Windows 主机] -->|SSH 连接| BB[Ubuntu 服务器]
    BB -->|启动容器，将容器22端口映射为Ubuntu 8033端口| CC[Docker 容器]
    AA -->|SSH ubuntu:8033端口| CC
```

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

## 构建可 SSH 登录的镜像

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

![SSH Forwarding](https://assets.vluv.space/ssh_forward_1.webp)

这个镜像需要做几件事：

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

> [!NOTE] 安全边界
> 下面的 `root:root` 和 `PermitRootLogin yes` 适合临时内网开发环境。长期使用时，最好改成单独用户、禁用密码登录，并只保留 key 登录。

```dockerfile
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/config

RUN sed -i 's/^#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config

RUN echo 'root:root' | chpasswd
RUN chmod 600 /root/.ssh/id_rsa
RUN mkdir /var/run/sshd

EXPOSE 22

# 启动 SSH daemon service
ENTRYPOINT service ssh start && tail -f /dev/null
```

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

```shell
# 创建容器
$ 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 准备好后，先构建镜像：

```shell
$ docker build -t dev-image:latest .
```

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

```shell
$ 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 端口。

> [!NOTE] 挂载数据卷
> 如果需要持久化数据，可以使用 `-v` 参数挂载数据卷，例如：`-v /path/on/host:/path/in/container`。

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

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

## 配置本地 SSH

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

编辑 `~/.ssh/config` 文件：

```shell
# Read more about SSH config files: https://linux.die.net/man/5/ssh_config
Host ubuntu
    HostName 10.21.163.77
    User root
    IdentityFile C:/Users/gjx/.ssh/id_rsa
    StrictHostKeyChecking no

Host 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 端口。

```mermaid
sequenceDiagram
    participant W as Windows
    participant U as Ubuntu 主机
    participant D as Docker 容器

    W->>U: SSH 连接到端口 8033
    U->>D: 端口映射 8033->22
    D-->>W: SSH 会话建立

    Note over W,D: 直接连接的效果
```

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

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

```shell
Host docker1
    HostName 10.21.163.77
    User root
    Port 8033

Host docker2
    HostName 10.21.163.77
    User root
    Port 8034
```

```mermaid
graph TB
    subgraph Windows
        ssh[SSH 客户端]
    end

    subgraph Ubuntu主机["Ubuntu 主机 (10.21.163.77)"]
        port1[端口 8033]
        port2[端口 8034]
    end

    subgraph Docker容器
        container1[容器1 - 端口22]
        container2[容器2 - 端口22]
    end

    ssh -->|ssh docker1| port1
    ssh -->|ssh docker2| port2
    port1 --> container1
    port2 --> container2

```

## 小结

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

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