Skip to content

openEuler 节点从零打通 Rootless Podman、Compose 与 FRP 开发容器

openEuler HPC 节点从零打通 Rootless Podman、Compose 与 FRP 开发容器

这篇文章不是照着某一段命令记录顺写的,而是根据一整段真实 shell 历史、远端节点排障记录和多轮智能体操作日志,反向整理出来的一条主线。

场景很具体:一台共享的 openEuler 22.03 (LTS-SP4) aarch64 HPC 节点,没有 root 权限,系统预装工具很少,但我需要尽快在上面拉起一个能 SSH 进去继续干活的 Ubuntu 开发容器,并通过 FRP 暴露到公网。

背景

这次的目标其实分成了三层:

  1. 先证明这台共享 HPC 节点允许普通用户跑 rootless 容器。
  2. 再把 Podmanpodman compose 和一套开发容器栈真正跑起来。
  3. 最后用一个 frpc sidecar 把容器内的 SSH 暴露出去,做到公网可登录。

难点不在某一条命令,而在于这台机器同时具备几个典型限制:

  • 没有 root。
  • 没有预装 podmanapptainerscreenhtopbtop
  • 机器是 aarch64,很多“默认搜一个二进制就装”的直觉会失效。
  • 还是共享 HPC 节点,rootless 网络、容器辅助二进制、镜像架构和出口网络限制都会一起冒出来。

先判断这事能不能做

在折腾之前,第一件事不是下载工具,而是先判断这台机器理论上能不能跑 rootless 容器。

环境核对里有几个关键信号:

  • 系统是 openEuler 22.03 (LTS-SP4)
  • 内核是 5.10.0-216.0.0.115.oe2203sp4.aarch64
  • /proc/sys/user/max_user_namespaces3332813
  • /etc/subuid/etc/subgid 里已经给当前用户分配了区间。

这几个条件基本说明:用户命名空间是开的,rootless 容器这条路可以走。

真正一开始最烦人的反而不是容器,而是进机器本身。ssh hpc-vpn 前两次直接报:

1
2
Ncat: Error reading proxy response Status-Line.
Ncat: Proxy returned status code 500.

后面再试才终于连上。这个细节很重要,因为后面远端 ssh 偶发断掉时,不能第一时间就把锅甩给 Podman 或 FRP,链路本身先天就有一点抖。

Rootless Podman 真正站起来

这一段是整件事里最容易“看起来快成了,实际上还没成”的部分。

一开始拿到的是 podman-remote-static-linux_arm64.tar.gz。它能输出一些信息,但本质上只有 remote client,不能在本机把 rootless 容器完整跑起来。后面换成完整的 podman-linux-arm64.tar.gz,再把里面真正需要的二进制补齐,事情才开始对路。

关键不只是 podman 主程序本身,还包括这一串 helper:

  • conmon
  • crunrunc
  • fuse-overlayfs
  • slirp4netns
  • pasta
  • netavark
  • aardvark-dns

真正可用的 rootless Podman,依赖的是“一整套辅助二进制 + 正确配置”,不是单个可执行文件。

我最后补的核心配置主要是这几块。

~/.config/containers/containers.conf

1
2
3
[engine]
conmon_path = ["/home/<USER>/bin/conmon"]
helper_binaries_dir = ["/home/<USER>/bin", "/home/<USER>/.local/lib/podman"]

这里最坑的一点是:conmon_path 必须是数组,不是字符串。这个 TOML 细节一错,Podman 会表现得像“半活半死”。

~/.config/containers/storage.conf

1
2
3
4
5
[storage]
driver = "overlay"

[storage.options]
mount_program = "/home/<USER>/bin/fuse-overlayfs"

再配一个可写的运行时目录:

1
2
3
export XDG_RUNTIME_DIR="$HOME/.podman/run"
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR"

中间踩过的坑包括:

  • ~/.podman 或相关目录不存在。
  • helper_binaries_dir 没配,导致网络和运行时组件找不到。
  • conmon_path 写成字符串。
  • 明明机器支持 rootless,但最后看起来像是“Podman 自己坏了”。

直到 podman --remote=false info 真正返回一套完整信息,确认:

  • rootless: true
  • overlay 生效
  • netavark 被识别
  • helper binaries 都在

这一步才算是“Podman 站起来了”,而不是“命令能运行了”。

info 通过后,pullcompose 还要继续补

很多人会在 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 开发环境。
  • 一个 frpc sidecar,把 devbox 里的 SSH 暴露出去。

用户需求很明确:

  • 开发容器要尽量保持“干净 Ubuntu”。
  • 软件后装,不想一开始就把镜像做得很重。
  • 容器里还要能控制宿主机的 Podman。

所以远端新建了一套类似这样的目录:

1
~/service/dev-reset-stack

里面有这些核心文件:

  • compose.yaml
  • .env
  • frpc.toml
  • start.sh
  • stop.sh

devbox 里除了工作目录挂载外,还额外挂了宿主机的 Podman 和 socket,让容器内也能控制宿主机容器:

1
2
3
CONTAINER_HOST=unix:///tmp/podman-hpc054.sock
PODMAN_HOST=unix:///tmp/podman-hpc054.sock
DOCKER_HOST=unix:///tmp/podman-hpc054.sock

这部分设计本身不复杂,真正麻烦的是 rootless Podman 的网络。

Rootless 网络、FRP 和那些不像 FRP 的坑

这台节点上的 rootless Podman 默认网络表现并不稳定。桥接、DNS、aardvark-dnspasta 这一串东西只要有一个状态不对,现象就会很飘。

最后收敛出来的稳定方案是:

  • 自建 bridge 网络。
  • --disable-dns
  • 给两个容器固定 IP。

大致是这种思路:

1
2
devbox-reset       10.92.0.10
frpc-devbox-reset 10.92.0.11

然后 frpc 不是回连宿主机 127.0.0.1,而是直接转发到容器内网目标:

1
2
3
localIP = "10.92.0.10"
localPort = 22
remotePort = <REMOTE_PORT>

中间还踩到过一个很脏的坑:某些 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
2
login to server success
start proxy success

也就是说,公网控制链路已经通了。

第二阶段,frpc 又开始报:

1
connect to local service [10.92.0.10:22] error: connect: connection refused

这时候很多人第一反应还是会继续查 FRP,但这次根因已经不是 FRP 了,而是本地服务本身不存在。

后续探测结果很直接:

  • 容器里没有 sshd 进程。
  • /etc/ssh 都不存在。
  • 22222222002022 全都没监听。

所以这条报错的准确翻译是:

“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
2
archive.ubuntu.com
security.ubuntu.com

而是:

1
http://ports.ubuntu.com/ubuntu-ports

前一版替换规则没覆盖到这个地址,所以明明“写了换源逻辑”,apt 实际还是在走原始 ports 源。修正后才真正切到华为镜像:

1
http://mirrors.huaweicloud.com/ubuntu-ports

最后一次性注入的主线就是:

1
2
3
4
5
6
7
8
sed -i 's@http://ports.ubuntu.com/ubuntu-ports@http://mirrors.huaweicloud.com/ubuntu-ports@g' /etc/apt/sources.list.d/ubuntu.sources
apt-get update
apt-get install -y --no-install-recommends openssh-server ca-certificates
mkdir -p /run/sshd
sed -ri 's/^#?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
sed -ri 's/^#?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
ssh-keygen -A
/usr/sbin/sshd

这次之后,公网探测终于拿到了明确 SSH banner:

1
SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.15

到这里,这条链路才算彻底闭环。

最终架构

最后跑通的结构可以概括成这样:

1
2
3
4
5
6
公网 SSH 客户端
-> <FRPS_HOST>:<REMOTE_PORT>
-> frps
-> frpc-devbox-reset
-> 10.92.0.10:22
-> devbox-reset (Ubuntu)

补充几点最终状态:

  • devbox 保持纯 Ubuntu,没再强行改成自定义 build 镜像。
  • frpc 作为 sidecar 独立运行。
  • 远端 frps 最终走的是可达端口,不再是最初超时的 7000
  • rootubuntu 都设置了密码并验证为有效,但具体密码当然不应该写进任何文章或仓库。

这次折腾里最值钱的结论

如果把这次经历压缩成几条真正有复用价值的经验,我会保留下面这些。

第一,rootless Podman 能不能跑,先看用户命名空间和 subuid/subgid,不要一上来就下载十几个二进制盲试。

第三,podman info 成功不等于环境可用。真正的完成线至少应该包括:

  • pull
  • compose
  • 能跑通网络
  • 能让目标服务真实监听

第五,在共享 HPC 节点上,最优雅的方案不一定最可靠。比如这次:

  • build 路径不稳,就别死磕 Dockerfile。
  • 直接运行时注入 openssh-server,反而更快闭环。
  • 某些 rootless 网络或 sidecar 需要更“粗暴”的参数,优先保证可用。

第六,长 URL 一定加引号,尤其是带 & 的 GitHub release 链接。不然你甚至还没开始装工具,shell 就已经先把你炸了一遍。

可复用做法

如果下次我还要在类似环境里再来一次,我会直接按这个顺序:

  1. 先检查 max_user_namespacessubuidsubgid
  2. 部署完整的 rootless Podman 二进制和 helper binaries。
  3. 一次性写好 containers.confstorage.confXDG_RUNTIME_DIR
  4. 先让 podman --remote=false info 真过。
  5. 再补 policy.jsonregistries.confpodman-compose
  6. 用双容器结构组织 devbox + frpc
  7. 固定容器内网 IP,FRP 直接转发到容器内地址。
  8. 不在这类节点上死磕 build,容器起来后再注入 openssh-server

这比“先写一堆 compose,然后边报错边猜哪里没配”快得多。

收尾

这次最大的收获不是“把容器跑起来了”,而是把一条在共享 openEuler aarch64 HPC 节点上可复用的路径收敛清楚了:

  • rootless Podman 可以手搓起来。
  • podman compose 可以补齐。
  • 纯 Ubuntu 开发容器可以落地。
  • FRP sidecar 可以把容器内 SSH 暴露出去。

最后那条可登录的 SSH,不是某一条命令的胜利,而是把架构、网络、镜像、运行时和系统细节一层层拆开的结果。

About this Post

This post is written by KaranocaVe.

#openEuler #Podman #Podman Compose #rootless #HPC #FRP #SSH #aarch64