openEuler HPC 节点从零打通 Rootless Podman、Compose 与 FRP 开发容器
这篇文章不是照着某一段命令记录顺写的,而是根据一整段真实 shell 历史、远端节点排障记录和多轮智能体操作日志,反向整理出来的一条主线。
场景很具体:一台共享的
openEuler 22.03 (LTS-SP4)aarch64HPC 节点,没有 root 权限,系统预装工具很少,但我需要尽快在上面拉起一个能 SSH 进去继续干活的 Ubuntu 开发容器,并通过 FRP 暴露到公网。
背景
这次的目标其实分成了三层:
- 先证明这台共享 HPC 节点允许普通用户跑 rootless 容器。
- 再把
Podman、podman compose和一套开发容器栈真正跑起来。 - 最后用一个
frpcsidecar 把容器内的 SSH 暴露出去,做到公网可登录。
难点不在某一条命令,而在于这台机器同时具备几个典型限制:
- 没有 root。
- 没有预装
podman、apptainer、screen、htop、btop。 - 机器是
aarch64,很多“默认搜一个二进制就装”的直觉会失效。 - 还是共享 HPC 节点,rootless 网络、容器辅助二进制、镜像架构和出口网络限制都会一起冒出来。
先判断这事能不能做
在折腾之前,第一件事不是下载工具,而是先判断这台机器理论上能不能跑 rootless 容器。
环境核对里有几个关键信号:
- 系统是
openEuler 22.03 (LTS-SP4)。 - 内核是
5.10.0-216.0.0.115.oe2203sp4.aarch64。 /proc/sys/user/max_user_namespaces为3332813。/etc/subuid和/etc/subgid里已经给当前用户分配了区间。
这几个条件基本说明:用户命名空间是开的,rootless 容器这条路可以走。
真正一开始最烦人的反而不是容器,而是进机器本身。ssh hpc-vpn 前两次直接报:
1 | Ncat: Error reading proxy response Status-Line. |
后面再试才终于连上。这个细节很重要,因为后面远端 ssh 偶发断掉时,不能第一时间就把锅甩给 Podman 或 FRP,链路本身先天就有一点抖。
Rootless Podman 真正站起来
这一段是整件事里最容易“看起来快成了,实际上还没成”的部分。
一开始拿到的是 podman-remote-static-linux_arm64.tar.gz。它能输出一些信息,但本质上只有 remote client,不能在本机把 rootless 容器完整跑起来。后面换成完整的 podman-linux-arm64.tar.gz,再把里面真正需要的二进制补齐,事情才开始对路。
关键不只是 podman 主程序本身,还包括这一串 helper:
conmoncrun或runcfuse-overlayfsslirp4netnspastanetavarkaardvark-dns
真正可用的 rootless Podman,依赖的是“一整套辅助二进制 + 正确配置”,不是单个可执行文件。
我最后补的核心配置主要是这几块。
~/.config/containers/containers.conf:
1 | [engine] |
这里最坑的一点是:conmon_path 必须是数组,不是字符串。这个 TOML 细节一错,Podman 会表现得像“半活半死”。
~/.config/containers/storage.conf:
1 | [storage] |
再配一个可写的运行时目录:
1 | export XDG_RUNTIME_DIR="$HOME/.podman/run" |
中间踩过的坑包括:
~/.podman或相关目录不存在。helper_binaries_dir没配,导致网络和运行时组件找不到。conmon_path写成字符串。- 明明机器支持 rootless,但最后看起来像是“Podman 自己坏了”。
直到 podman --remote=false info 真正返回一套完整信息,确认:
rootless: trueoverlay生效netavark被识别- helper binaries 都在
这一步才算是“Podman 站起来了”,而不是“命令能运行了”。
info 通过后,pull 和 compose 还要继续补
很多人会在 podman info 成功后误以为结束了,但实际上后面还有两道坎:拉镜像和编排。
首先是镜像拉取。
一开始又踩到几个典型问题:
- 没有
policy.json。 - 没有像样的
registries.conf。 - 人的肌肉记忆还在敲
docker pull,但系统里根本没 Docker。 - 某些镜像标签不存在,比如
quay.io/libpod/ubuntu:22.04这种预期标签并不一定真有。
为了避免每次都忘记加参数,我后来直接设了一个别名:
1 | alias p='podman --remote=false' |
然后把缺的两个基础文件补上:
- 最小可用的
policy.json - 自己的
registries.conf
再往后一个非常现实的点是镜像源。直连海外仓库慢且不稳定,所以后面拉通的是国内镜像,比如华为云 SWR。这里还顺手吃了一个架构教训:有些镜像虽然能拉下来,但其实是 linux/amd64,在 arm64 机器上会伴随明显警告。不是“能拉”就等于“拉对了”。
然后是 podman compose。
系统最开始当然没有它。用默认 pip 源装,超时;切 USTC,又 403;最后换到华为云 PyPI 镜像才稳定装上:
1 | pip3 install --user -i https://repo.huaweicloud.com/repository/pypi/simple podman-compose |
能看到 podman-compose 1.5.0 以后,才有资格开始组织真正的服务栈。
组装开发容器和 FRP 栈
最后落地的方案不是单容器,而是一套双服务结构:
- 一个
devbox,跑纯 Ubuntu 开发环境。 - 一个
frpcsidecar,把devbox里的 SSH 暴露出去。
用户需求很明确:
- 开发容器要尽量保持“干净 Ubuntu”。
- 软件后装,不想一开始就把镜像做得很重。
- 容器里还要能控制宿主机的 Podman。
所以远端新建了一套类似这样的目录:
1 | ~/service/dev-reset-stack |
里面有这些核心文件:
compose.yaml.envfrpc.tomlstart.shstop.sh
devbox 里除了工作目录挂载外,还额外挂了宿主机的 Podman 和 socket,让容器内也能控制宿主机容器:
1 | CONTAINER_HOST=unix:///tmp/podman-hpc054.sock |
这部分设计本身不复杂,真正麻烦的是 rootless Podman 的网络。
Rootless 网络、FRP 和那些不像 FRP 的坑
这台节点上的 rootless Podman 默认网络表现并不稳定。桥接、DNS、aardvark-dns、pasta 这一串东西只要有一个状态不对,现象就会很飘。
最后收敛出来的稳定方案是:
- 自建 bridge 网络。
--disable-dns。- 给两个容器固定 IP。
大致是这种思路:
1 | devbox-reset 10.92.0.10 |
然后 frpc 不是回连宿主机 127.0.0.1,而是直接转发到容器内网目标:
1 | localIP = "10.92.0.10" |
中间还踩到过一个很脏的坑:某些 frpc 镜像在这台机器的 rootless Podman 下会直接 segfault,退出码 139。最后能稳定运行的做法反而是把 frpc 容器提到:
1 | privileged: true |
这不是优雅方案,但在共享 HPC 节点上,能稳定跑比理论优雅更重要。
为什么一开始是 7000 timeout,后来又变成 connection refused
这两个错误看起来像一回事,实际上是两个阶段。
第一阶段,frpc 连不上 frps:
1 | dial tcp <FRPS_HOST>:7000: i/o timeout |
这个问题后来确认不是配置拼错,而是出口限制。把 frps 监听端口从 7000 改到 80 以后,日志就变成了:
1 | login to server success |
也就是说,公网控制链路已经通了。
第二阶段,frpc 又开始报:
1 | connect to local service [10.92.0.10:22] error: connect: connection refused |
这时候很多人第一反应还是会继续查 FRP,但这次根因已经不是 FRP 了,而是本地服务本身不存在。
后续探测结果很直接:
- 容器里没有
sshd进程。 /etc/ssh都不存在。22、2222、2200、2022全都没监听。
所以这条报错的准确翻译是:
“FRP 没毛病,公网入口也没毛病,坏的是你转发目标里根本没有 SSH 服务。”
不折腾 build,改成启动后一次性注入 SSH
中间其实尝试过把 openssh-server 直接写进 Dockerfile,改成 compose build 流程。但这条路在这台 rootless Podman 机器上并不稳:
podman compose up -d --build会失败。- 单独
podman build也出现过异常中止。 - 某些构建路径在这台机器上就是不值得继续深挖。
容器保持纯 Ubuntu,SSH 这种基础设施可以在容器启动后一次性注入,不必强求 build 阶段完成。
于是最后改成运行时注入。
这里还有一个很关键的 ARM 细节。Ubuntu 在 arm64 上默认源不是常见的:
1 | archive.ubuntu.com |
而是:
1 | http://ports.ubuntu.com/ubuntu-ports |
前一版替换规则没覆盖到这个地址,所以明明“写了换源逻辑”,apt 实际还是在走原始 ports 源。修正后才真正切到华为镜像:
1 | http://mirrors.huaweicloud.com/ubuntu-ports |
最后一次性注入的主线就是:
1 | sed -i 's@http://ports.ubuntu.com/ubuntu-ports@http://mirrors.huaweicloud.com/ubuntu-ports@g' /etc/apt/sources.list.d/ubuntu.sources |
这次之后,公网探测终于拿到了明确 SSH banner:
1 | SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.15 |
到这里,这条链路才算彻底闭环。
最终架构
最后跑通的结构可以概括成这样:
1 | 公网 SSH 客户端 |
补充几点最终状态:
devbox保持纯 Ubuntu,没再强行改成自定义 build 镜像。frpc作为 sidecar 独立运行。- 远端
frps最终走的是可达端口,不再是最初超时的7000。 root和ubuntu都设置了密码并验证为有效,但具体密码当然不应该写进任何文章或仓库。
这次折腾里最值钱的结论
如果把这次经历压缩成几条真正有复用价值的经验,我会保留下面这些。
第一,rootless Podman 能不能跑,先看用户命名空间和 subuid/subgid,不要一上来就下载十几个二进制盲试。
第三,podman info 成功不等于环境可用。真正的完成线至少应该包括:
- 能
pull - 能
compose - 能跑通网络
- 能让目标服务真实监听
第五,在共享 HPC 节点上,最优雅的方案不一定最可靠。比如这次:
- build 路径不稳,就别死磕 Dockerfile。
- 直接运行时注入
openssh-server,反而更快闭环。 - 某些 rootless 网络或 sidecar 需要更“粗暴”的参数,优先保证可用。
第六,长 URL 一定加引号,尤其是带 & 的 GitHub release 链接。不然你甚至还没开始装工具,shell 就已经先把你炸了一遍。
可复用做法
如果下次我还要在类似环境里再来一次,我会直接按这个顺序:
- 先检查
max_user_namespaces、subuid、subgid。 - 部署完整的 rootless Podman 二进制和 helper binaries。
- 一次性写好
containers.conf、storage.conf和XDG_RUNTIME_DIR。 - 先让
podman --remote=false info真过。 - 再补
policy.json、registries.conf、podman-compose。 - 用双容器结构组织
devbox + frpc。 - 固定容器内网 IP,FRP 直接转发到容器内地址。
- 不在这类节点上死磕 build,容器起来后再注入
openssh-server。
这比“先写一堆 compose,然后边报错边猜哪里没配”快得多。
收尾
这次最大的收获不是“把容器跑起来了”,而是把一条在共享 openEuler aarch64 HPC 节点上可复用的路径收敛清楚了:
- rootless Podman 可以手搓起来。
podman compose可以补齐。- 纯 Ubuntu 开发容器可以落地。
- FRP sidecar 可以把容器内 SSH 暴露出去。
最后那条可登录的 SSH,不是某一条命令的胜利,而是把架构、网络、镜像、运行时和系统细节一层层拆开的结果。