从零到交付:通用 C/C++ 项目容器镜像搭建完全指南
本文基于 Fast DDS QoS 工程实践提炼而成,适用于任何 C/C++、Go、Rust 等编译型语言项目的容器化。核心思路:先理解产物 → 确认环境 → 分析依赖 → 编译验证 → 收集库 → 编写 Dockerfile → 构建检查 → 运行测试 → 打包部署。
一、整体思路:容器化的"八步走"战略
容器化不是简单地写个 Dockerfile,而是一条分层递进的工程流水线。每一层都依赖上一层正确完成,任何一层有缺口,后面都会出问题。
┌─────────────────────────────────────────────────┐
│ 第 8 层:打包 & 部署(docker save / push) │
├─────────────────────────────────────────────────┤
│ 第 7 层:容器内运行 & 验收 │
├─────────────────────────────────────────────────┤
│ 第 6 层:构建后检查(架构 / 依赖 / 启动) │
├─────────────────────────────────────────────────┤
│ 第 5 层:编写 Dockerfile │
├─────────────────────────────────────────────────┤
│ 第 4 层:收集运行时动态库(runtime-libs/) │
├─────────────────────────────────────────────────┤
│ 第 3 层:主机上编译 & 验证 │
├─────────────────────────────────────────────────┤
│ 第 2 层:分析依赖(file / ldd / 头文件) │
├─────────────────────────────────────────────────┤
│ 第 1 层:确认环境(架构 / Docker / 基础镜像) │
└─────────────────────────────────────────────────┘
核心原则:程序必须在目标架构上编译,或者使用与目标架构一致的交叉编译环境。 不能在 x86 机器上编译 ARM 程序,然后塞进 ARM 镜像。
二、第一步:确认环境 —— 一切的起点
2.1 确认目标机器架构
uname -m
| 输出 | 含义 |
|---|---|
aarch64 / arm64 |
ARM 64 位(如 Kylin ARM、飞腾、鲲鹏) |
x86_64 |
Intel/AMD 64 位 |
riscv64 |
RISC-V 64 位 |
⚠️ 关键: 后续编译、基础镜像、动态库全部必须与目标架构一致。
2.2 确认 Docker 可用
docker version
检查 Client 和 Server 都正常,重点关注 Server 端版本。
2.3 确认基础镜像
# 列出已有镜像
docker images --format '{{.Repository}}:{{.Tag}}'
# 检查特定镜像的架构
docker image inspect <镜像名> --format '{{.Os}}/{{.Architecture}}'
选择基础镜像的原则:
- 优先使用目标环境已有的内网镜像(避免构建时联网超时)
- 确认架构匹配(
linux/arm64、linux/amd64等) - 不要盲目写
FROM ubuntu:20.04,先看看本地有什么
# ✅ 好的做法:使用内网 ARM64 镜像
FROM 192.168.16.61:30443/library/eclipse-temurin:17.0.19_10-jre-noble
# ❌ 不好的做法:假设能访问 Docker Hub
FROM ubuntu:20.04
三、第二步:分析依赖 —— 搞清楚"需要什么"
C/C++ 程序的依赖分三类,缺一不可:
| 类型 | 说明 | 示例 |
|---|---|---|
| 编译依赖 | 头文件、CMake 配置、静态库 | fastddsConfig.cmake、*.h |
| 运行依赖 | 程序启动时动态加载的 .so |
libssl.so.1.1、libfastdds.so |
| 环境依赖 | 网络、共享内存、权限、配置文件 | --network host、/dev/shm |
3.1 确认可执行文件架构
编译完成后,第一件事就是检查架构:
file build/MyProgram
期望输出:
ELF 64-bit LSB pie executable, ARM aarch64
# 或
ELF 64-bit LSB pie executable, x86-64
如果架构不对(比如在 x86 上编译了程序却要放进 ARM 镜像),立即停止,重新选择构建环境。
3.2 查看动态库依赖(最重要的一步)
ldd build/MyProgram
输出示例:
libfastdds.so.3 => /home/user/opt/fastdds/lib/libfastdds.so.3
libtinyxml2.so.6 => not found ❌ 缺失!
libssl.so.1.1 => not found ❌ 缺失!
libstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6
重点看 not found! 每个 not found 都是一颗定时炸弹。
核心洞察: “目标主机上能运行” ≠ “基础镜像里也能运行”。主机可能已经装了这些库,但容器是独立的文件系统,看不到主机的
/lib。
3.3 确认第三方库安装位置
# 以 Fast DDS 为例
export MYLIB_HOME=/path/to/install/prefix
ls -l "$MYLIB_HOME/include" # 头文件
ls -l "$MYLIB_HOME/lib" # 库文件
3.4 确认 CMake 能找到依赖
export CMAKE_PREFIX_PATH="$MYLIB_HOME:${CMAKE_PREFIX_PATH}"
cmake -S . -B build
CMAKE_PREFIX_PATH:告诉 CMake 去哪里找xxxConfig.cmake、头文件和库-S .:源代码目录-B build:构建输出目录(不污染源代码)
四、第三步:编译 & 主机验证
4.1 设置编译环境
export MYLIB_HOME=/path/to/install/prefix
export CMAKE_PREFIX_PATH="$MYLIB_HOME:${CMAKE_PREFIX_PATH}"
export LD_LIBRARY_PATH="$MYLIB_HOME/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
LD_LIBRARY_PATH只影响当前 Shell,不会自动写入镜像,Dockerfile 里要单独设置。
4.2 编译
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
| 参数 | 含义 |
|---|---|
-DCMAKE_BUILD_TYPE=Release |
发布版本(优化编译) |
-j"$(nproc)" |
按 CPU 核心数并行编译 |
4.3 在主机上先验证(容器化之前必做!)
# 终端 1:启动服务端/订阅端
./build/MyServer --config ./config.xml
# 终端 2:启动客户端/发布端
./build/MyClient --config ./config.xml
验收原则:以接收端为准! 发送端显示"成功"只能说明本地调用成功,不代表对端收到。
经验法则: 如果主机上都跑不通,容器里一定也跑不通。先修好主机上的问题,再谈容器化。
五、第四步:收集运行时动态库
这是整个流程中最容易出错的环节。
5.1 创建干净的库目录
rm -rf runtime-libs && mkdir -p runtime-libs
5.2 复制第三方库
cp -a "$MYLIB_HOME/lib"/*.so* runtime-libs/
cp -a保留符号链接关系,动态库通常是一串软链接(libxxx.so→libxxx.so.3→libxxx.so.3.6.1),不能只复制一个。
5.3 补齐系统库
根据 ldd 的结果,逐个补齐 not found 的库:
# 示例:补齐 TinyXML2 和 OpenSSL
cp -a /lib/aarch64-linux-gnu/libtinyxml2.so.6* runtime-libs/
cp -a /lib/aarch64-linux-gnu/libssl.so.1.1* runtime-libs/
cp -a /lib/aarch64-linux-gnu/libcrypto.so.1.1* runtime-libs/
通用方法(换项目时照做):
- 对每个可执行文件运行
ldd - 找出所有
not found或基础镜像中没有的库 - 用
find/dpkg -S/rpm -qf定位真实文件 - 复制库文件及其所有符号链接
- 构建镜像后再次在容器内
ldd验证
# 辅助定位命令
find /lib /usr/lib "$MYLIB_HOME/lib" -name 'lib*.so*' 2>/dev/null | grep -E '关键词'
dpkg -S libxxx.so.6 # Debian/Ubuntu
rpm -qf /lib/libxxx.so.6 # CentOS/RHEL
六、第五步:编写 Dockerfile
6.1 Dockerfile 模板
# ========================================
# 多阶段构建(推荐)
# ========================================
# ---- 阶段 1:构建阶段(如果支持在容器内编译)----
# FROM <base> AS builder
# WORKDIR /build
# COPY . .
# RUN cmake -S . -B build && cmake --build build -j4
# ---- 阶段 2:运行阶段(精简)----
ARG BASE_IMAGE=<你的内网基础镜像>
FROM ${BASE_IMAGE}
WORKDIR /opt/myapp
# 复制编译好的可执行文件
COPY build/MyProgram /opt/myapp/bin/MyProgram
# 复制配置文件
COPY config/*.xml /opt/myapp/config/
# 复制运行时库
COPY runtime-libs/ /opt/myapp/lib/
# 确保可执行权限
RUN chmod 0755 /opt/myapp/bin/MyProgram
# 设置库搜索路径
ENV LD_LIBRARY_PATH=/opt/myapp/lib:/usr/local/lib
# 默认启动命令(按需修改)
CMD ["/bin/sh"]
6.2 逐项说明
| 指令 | 作用 |
|---|---|
ARG BASE_IMAGE |
允许构建时替换基础镜像 |
FROM |
指定基础镜像和架构 |
WORKDIR |
设置工作目录 |
COPY |
将文件从构建上下文复制到镜像内 |
chmod 0755 |
确保程序有执行权限 |
ENV LD_LIBRARY_PATH |
让程序找到动态库 |
CMD |
默认启动命令 |
6.3 检查清单
-
FROM的架构是否与目标机一致 -
COPY的源文件是否都真实存在 -
WORKDIR、程序路径、配置路径是否一致 -
chmod是否覆盖所有可执行文件 -
ENV LD_LIBRARY_PATH是否包含实际库目录 - 是否需要
--network host、--ipc=host等特殊权限 - 是否需要在 Dockerfile 中安装额外包(
apt-get/yum)
七、第六步:构建镜像
7.1 构建前检查上下文
ls -l Dockerfile build/MyProgram config/*.xml runtime-libs/
确保所有 COPY 引用的文件都存在。
7.2 执行构建
docker build --pull=false -t myapp:1.0.0 .
| 参数 | 含义 |
|---|---|
--pull=false |
不尝试从远程更新基础镜像(离线环境必备) |
-t myapp:1.0.0 |
设置镜像名和不可变版本号 |
. |
当前目录为构建上下文 |
7.3 常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
Dockerfile: no such file |
当前目录不对或文件名大小写错误 | pwd + ls -l Dockerfile |
Client.Timeout exceeded |
无法访问 Docker Hub | 改用内网基础镜像 + --pull=false |
COPY failed: no source |
源文件不存在 | 检查文件路径和构建上下文 |
八、第七步:构建后检查(逐层验证)
8.1 检查镜像架构
docker image inspect myapp:1.0.0 --format '{{.Os}}/{{.Architecture}}'
# 必须输出:linux/arm64(或对应架构)
8.2 检查文件是否到位
docker run --rm --entrypoint /bin/sh myapp:1.0.0 -c \
'ls -l /opt/myapp/bin /opt/myapp/config /opt/myapp/lib'
8.3 检查动态库解析
docker run --rm --entrypoint /bin/sh myapp:1.0.0 -c \
'ldd /opt/myapp/bin/MyProgram'
如果还有
not found,回到第 5 步补齐库,重新构建。
8.4 检查程序能否启动
docker run --rm myapp:1.0.0 /opt/myapp/bin/MyProgram --help
九、第八步:容器内运行 & 验收
9.1 同一主机上的多容器
# 终端 1:启动服务端
docker run --rm --network host --ipc=host myapp:1.0.0 \
/opt/myapp/bin/MyServer --config /opt/myapp/config/server.xml
# 终端 2:启动客户端
docker run --rm --network host --ipc=host myapp:1.0.0 \
/opt/myapp/bin/MyClient --config /opt/myapp/config/client.xml
| 参数 | 含义 |
|---|---|
--network host |
使用主机网络(UDP 发现、数据传输) |
--ipc=host |
共享主机 IPC(共享内存通信必需) |
--rm |
退出后自动清理容器 |
9.2 常见"假通过"陷阱
| 现象 | 真正原因 |
|---|---|
| Writer write() 成功,Reader 收到 0 条 | 缺少 --ipc=host,共享内存通道隔离 |
| 端点匹配但无数据 | 检查 Domain/Topic/数据类型是否一致 |
| 连接超时 | 检查防火墙、端口、网络策略 |
记住:验收必须以接收端实际数据为准!
9.3 跨主机部署要点
--ipc=host不能跨主机- 必须明确使用 UDP/TCP,不能依赖共享内存
- 防火墙放通发现端口和数据端口
- 在真实双机环境重新完整测试
十、第九步:打包 & 部署
10.1 导出镜像为 tar
docker save -o myapp-1.0.0.tar myapp:1.0.0
ls -lh myapp-1.0.0.tar
docker save导出的是完整镜像包(含所有层),可直接上传 Harbor 或离线传输。
10.2 直接推送到 Harbor(如果网络可达)
docker tag myapp:1.0.0 <harbor-host>/<project>/myapp:1.0.0
docker push <harbor-host>/<project>/myapp:1.0.0
10.3 版本管理原则
- ✅ 使用不可变版本号:
1.0.0、1.0.1 - ❌ 不要使用
latest(无法追溯) - 任何变更(代码、库、配置、基础镜像)都应递增版本号
十一、运行时配置文件(runtime.yaml)注意事项
如果你的平台使用 runtime.yaml(非 Kubernetes Pod YAML),注意:
apiVersion: runtime.jfounder.io/v1
kind: RuntimeConfig
ports:
- name: management
port: 8080
protocol: TCP
expose: true
management:
portName: management
env:
- name: LD_LIBRARY_PATH
value: "/opt/myapp/lib:/usr/local/lib"
不要写 Kubernetes 字段: metadata、spec、containers、image、command、args
重要: YAML 通过 Schema 校验 ≠ 平台能正确运行你的程序。如果程序不实现平台管理接口,需要额外包装。
十二、通用排错手册
12.1 快速定位流程图
问题出现
│
├─ 构建失败?
│ ├─ Dockerfile 找不到 → 检查 pwd + 文件名
│ ├─ 基础镜像拉不到 → 改用内网镜像 + --pull=false
│ └─ COPY 失败 → 检查源文件是否存在于构建上下文
│
├─ 启动报错?
│ ├─ 动态库缺失 → ldd 查缺 + 补齐 runtime-libs
│ ├─ 配置文件找不到 → 检查容器内路径 vs 宿主机路径
│ └─ 权限不足 → chmod + 检查用户/设备权限
│
├─ 网络通信异常?
│ ├─ 收不到数据 → 检查 --network host + --ipc=host
│ ├─ 端点不匹配 → 检查 Domain/Topic/数据类型
│ └─ 端口不通 → 检查防火墙 + 安全组
│
└─ 架构不匹配?
└─ uname -m + file + docker inspect 三连确认
12.2 错误速查表
| 错误信息 | 根因 | 解决方案 |
|---|---|---|
Dockerfile: no such file |
目录不对或大小写错误 | pwd + ls Dockerfile |
Client.Timeout exceeded |
无法访问公网仓库 | 内网镜像 + --pull=false |
libxxx.so: cannot open |
动态库缺失 | ldd → 找库 → 复制 → 重建 |
write() 成功但收到 0 条 |
缺 --ipc=host |
加上 --ipc=host |
| 架构不匹配 | x86 程序放进 ARM 镜像 | 在 ARM 机器重新编译 |
| XML 找不到 | 用了宿主机路径 | 改用容器内路径 /opt/... |
十三、换项目时的完整检查清单
编译前
- 目标机器架构已确认(
uname -m) - 基础镜像已存在且架构匹配
- 确认工程生成的可执行文件名称(看 CMakeLists.txt / Makefile)
- 确认配置文件清单
- 设置正确的 CMake 前缀路径
编译后
-
file确认架构正确 -
ldd没有not found - 程序在主机上运行通过
- 退出码和业务输出符合预期
构建镜像前
- Dockerfile 位于工程根目录
- 所有
COPY源文件真实存在 - 所有动态库已放入
runtime-libs/ -
ENV LD_LIBRARY_PATH包含库目录 - 不依赖不可用的公网仓库
构建镜像后
- 镜像架构正确(
docker inspect) - 容器内文件路径正确
- 容器内
ldd无not found - 程序可在容器内启动
- 网络、IPC、权限均已验证
- 以接收端实际结果为准
部署前
- 使用不可变版本号(非
latest) - 关键业务测试已重新执行
-
docker save导出 tar 或 Harbor 推送成功 - 镜像名和 Tag 与部署配置一致
- runtime.yaml 使用平台真实 Schema
十四、一条命令流走完全程
以下是换项目时的最短完整流程模板:
# ===== 1. 环境确认 =====
cd /path/to/project
uname -m
docker version
# ===== 2. 设置依赖环境 =====
export MYLIB_HOME=/path/to/install
export CMAKE_PREFIX_PATH="$MYLIB_HOME:${CMAKE_PREFIX_PATH}"
export LD_LIBRARY_PATH="$MYLIB_HOME/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
# ===== 3. 编译 =====
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
# ===== 4. 架构 & 依赖检查 =====
file build/MyProgram
ldd build/MyProgram
# ===== 5. 主机验证 =====
./build/MyProgram --test
# ===== 6. 收集动态库 =====
rm -rf runtime-libs && mkdir -p runtime-libs
cp -a "$MYLIB_HOME/lib"/*.so* runtime-libs/
# 根据 ldd 结果补齐系统库
cp -a /lib/aarch64-linux-gnu/libxxx.so* runtime-libs/
# ===== 7. 构建镜像 =====
docker build --pull=false -t myapp:1.0.0 .
# ===== 8. 构建后检查 =====
docker image inspect myapp:1.0.0 --format '{{.Os}}/{{.Architecture}}'
docker run --rm --entrypoint /bin/sh myapp:1.0.0 -c 'ldd /opt/myapp/bin/MyProgram'
docker run --rm myapp:1.0.0 /opt/myapp/bin/MyProgram --help
# ===== 9. 容器内运行 =====
docker run --rm --network host --ipc=host myapp:1.0.0 \
/opt/myapp/bin/MyProgram --config /opt/myapp/config/app.xml
# ===== 10. 打包 =====
docker save -o myapp-1.0.0.tar myapp:1.0.0
附录:核心命令速查卡
| 命令 | 用途 |
|---|---|
uname -m |
查看 CPU 架构 |
file <binary> |
查看可执行文件架构 |
ldd <binary> |
查看动态库依赖 |
docker image inspect <img> |
查看镜像元数据(架构等) |
docker build --pull=false -t name:tag . |
离线构建镜像 |
docker run --rm -it --network host --ipc=host |
运行容器(同机通信) |
docker save -o file.tar image:tag |
导出镜像为 tar |
docker tag + docker push |
推送到 Harbor |
find / -name 'lib*.so*' |
定位动态库文件 |
dpkg -S <file> / rpm -qf <file> |
查询文件属于哪个包 |
总结: 容器化的本质是依赖管理 + 环境复现。搞清楚"程序需要什么",然后"把所有需要的东西装进一个隔离的文件系统",最后"确保运行时环境和主机打通"。掌握这套思路,任何 C/C++ 工程的容器化都能举一反三。# 从零到交付:通用 C/C++ 项目容器镜像搭建完全指南
本文基于 Fast DDS QoS 工程实践提炼而成,适用于任何 C/C++、Go、Rust 等编译型语言项目的容器化。核心思路:先理解产物 → 确认环境 → 分析依赖 → 编译验证 → 收集库 → 编写 Dockerfile → 构建检查 → 运行测试 → 打包部署。
一、整体思路:容器化的"八步走"战略
容器化不是简单地写个 Dockerfile,而是一条分层递进的工程流水线。每一层都依赖上一层正确完成,任何一层有缺口,后面都会出问题。
┌─────────────────────────────────────────────────┐
│ 第 8 层:打包 & 部署(docker save / push) │
├─────────────────────────────────────────────────┤
│ 第 7 层:容器内运行 & 验收 │
├─────────────────────────────────────────────────┤
│ 第 6 层:构建后检查(架构 / 依赖 / 启动) │
├─────────────────────────────────────────────────┤
│ 第 5 层:编写 Dockerfile │
├─────────────────────────────────────────────────┤
│ 第 4 层:收集运行时动态库(runtime-libs/) │
├─────────────────────────────────────────────────┤
│ 第 3 层:主机上编译 & 验证 │
├─────────────────────────────────────────────────┤
│ 第 2 层:分析依赖(file / ldd / 头文件) │
├─────────────────────────────────────────────────┤
│ 第 1 层:确认环境(架构 / Docker / 基础镜像) │
└─────────────────────────────────────────────────┘
核心原则:程序必须在目标架构上编译,或者使用与目标架构一致的交叉编译环境。 不能在 x86 机器上编译 ARM 程序,然后塞进 ARM 镜像。
二、第一步:确认环境 —— 一切的起点
2.1 确认目标机器架构
uname -m
| 输出 | 含义 |
|---|---|
aarch64 / arm64 |
ARM 64 位(如 Kylin ARM、飞腾、鲲鹏) |
x86_64 |
Intel/AMD 64 位 |
riscv64 |
RISC-V 64 位 |
⚠️ 关键: 后续编译、基础镜像、动态库全部必须与目标架构一致。
2.2 确认 Docker 可用
docker version
检查 Client 和 Server 都正常,重点关注 Server 端版本。
2.3 确认基础镜像
# 列出已有镜像
docker images --format '{{.Repository}}:{{.Tag}}'
# 检查特定镜像的架构
docker image inspect <镜像名> --format '{{.Os}}/{{.Architecture}}'
选择基础镜像的原则:
- 优先使用目标环境已有的内网镜像(避免构建时联网超时)
- 确认架构匹配(
linux/arm64、linux/amd64等) - 不要盲目写
FROM ubuntu:20.04,先看看本地有什么
# ✅ 好的做法:使用内网 ARM64 镜像
FROM 192.168.16.61:30443/library/eclipse-temurin:17.0.19_10-jre-noble
# ❌ 不好的做法:假设能访问 Docker Hub
FROM ubuntu:20.04
三、第二步:分析依赖 —— 搞清楚"需要什么"
C/C++ 程序的依赖分三类,缺一不可:
| 类型 | 说明 | 示例 |
|---|---|---|
| 编译依赖 | 头文件、CMake 配置、静态库 | fastddsConfig.cmake、*.h |
| 运行依赖 | 程序启动时动态加载的 .so |
libssl.so.1.1、libfastdds.so |
| 环境依赖 | 网络、共享内存、权限、配置文件 | --network host、/dev/shm |
3.1 确认可执行文件架构
编译完成后,第一件事就是检查架构:
file build/MyProgram
期望输出:
ELF 64-bit LSB pie executable, ARM aarch64
# 或
ELF 64-bit LSB pie executable, x86-64
如果架构不对(比如在 x86 上编译了程序却要放进 ARM 镜像),立即停止,重新选择构建环境。
3.2 查看动态库依赖(最重要的一步)
ldd build/MyProgram
输出示例:
libfastdds.so.3 => /home/user/opt/fastdds/lib/libfastdds.so.3
libtinyxml2.so.6 => not found ❌ 缺失!
libssl.so.1.1 => not found ❌ 缺失!
libstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6
重点看 not found! 每个 not found 都是一颗定时炸弹。
核心洞察: “目标主机上能运行” ≠ “基础镜像里也能运行”。主机可能已经装了这些库,但容器是独立的文件系统,看不到主机的
/lib。
3.3 确认第三方库安装位置
# 以 Fast DDS 为例
export MYLIB_HOME=/path/to/install/prefix
ls -l "$MYLIB_HOME/include" # 头文件
ls -l "$MYLIB_HOME/lib" # 库文件
3.4 确认 CMake 能找到依赖
export CMAKE_PREFIX_PATH="$MYLIB_HOME:${CMAKE_PREFIX_PATH}"
cmake -S . -B build
CMAKE_PREFIX_PATH:告诉 CMake 去哪里找xxxConfig.cmake、头文件和库-S .:源代码目录-B build:构建输出目录(不污染源代码)
四、第三步:编译 & 主机验证
4.1 设置编译环境
export MYLIB_HOME=/path/to/install/prefix
export CMAKE_PREFIX_PATH="$MYLIB_HOME:${CMAKE_PREFIX_PATH}"
export LD_LIBRARY_PATH="$MYLIB_HOME/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
LD_LIBRARY_PATH只影响当前 Shell,不会自动写入镜像,Dockerfile 里要单独设置。
4.2 编译
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
| 参数 | 含义 |
|---|---|
-DCMAKE_BUILD_TYPE=Release |
发布版本(优化编译) |
-j"$(nproc)" |
按 CPU 核心数并行编译 |
4.3 在主机上先验证(容器化之前必做!)
# 终端 1:启动服务端/订阅端
./build/MyServer --config ./config.xml
# 终端 2:启动客户端/发布端
./build/MyClient --config ./config.xml
验收原则:以接收端为准! 发送端显示"成功"只能说明本地调用成功,不代表对端收到。
经验法则: 如果主机上都跑不通,容器里一定也跑不通。先修好主机上的问题,再谈容器化。
五、第四步:收集运行时动态库
这是整个流程中最容易出错的环节。
5.1 创建干净的库目录
rm -rf runtime-libs && mkdir -p runtime-libs
5.2 复制第三方库
cp -a "$MYLIB_HOME/lib"/*.so* runtime-libs/
cp -a保留符号链接关系,动态库通常是一串软链接(libxxx.so→libxxx.so.3→libxxx.so.3.6.1),不能只复制一个。
5.3 补齐系统库
根据 ldd 的结果,逐个补齐 not found 的库:
# 示例:补齐 TinyXML2 和 OpenSSL
cp -a /lib/aarch64-linux-gnu/libtinyxml2.so.6* runtime-libs/
cp -a /lib/aarch64-linux-gnu/libssl.so.1.1* runtime-libs/
cp -a /lib/aarch64-linux-gnu/libcrypto.so.1.1* runtime-libs/
通用方法(换项目时照做):
- 对每个可执行文件运行
ldd - 找出所有
not found或基础镜像中没有的库 - 用
find/dpkg -S/rpm -qf定位真实文件 - 复制库文件及其所有符号链接
- 构建镜像后再次在容器内
ldd验证
# 辅助定位命令
find /lib /usr/lib "$MYLIB_HOME/lib" -name 'lib*.so*' 2>/dev/null | grep -E '关键词'
dpkg -S libxxx.so.6 # Debian/Ubuntu
rpm -qf /lib/libxxx.so.6 # CentOS/RHEL
六、第五步:编写 Dockerfile
6.1 Dockerfile 模板
# ========================================
# 多阶段构建(推荐)
# ========================================
# ---- 阶段 1:构建阶段(如果支持在容器内编译)----
# FROM <base> AS builder
# WORKDIR /build
# COPY . .
# RUN cmake -S . -B build && cmake --build build -j4
# ---- 阶段 2:运行阶段(精简)----
ARG BASE_IMAGE=<你的内网基础镜像>
FROM ${BASE_IMAGE}
WORKDIR /opt/myapp
# 复制编译好的可执行文件
COPY build/MyProgram /opt/myapp/bin/MyProgram
# 复制配置文件
COPY config/*.xml /opt/myapp/config/
# 复制运行时库
COPY runtime-libs/ /opt/myapp/lib/
# 确保可执行权限
RUN chmod 0755 /opt/myapp/bin/MyProgram
# 设置库搜索路径
ENV LD_LIBRARY_PATH=/opt/myapp/lib:/usr/local/lib
# 默认启动命令(按需修改)
CMD ["/bin/sh"]
6.2 逐项说明
| 指令 | 作用 |
|---|---|
ARG BASE_IMAGE |
允许构建时替换基础镜像 |
FROM |
指定基础镜像和架构 |
WORKDIR |
设置工作目录 |
COPY |
将文件从构建上下文复制到镜像内 |
chmod 0755 |
确保程序有执行权限 |
ENV LD_LIBRARY_PATH |
让程序找到动态库 |
CMD |
默认启动命令 |
6.3 检查清单
-
FROM的架构是否与目标机一致 -
COPY的源文件是否都真实存在 -
WORKDIR、程序路径、配置路径是否一致 -
chmod是否覆盖所有可执行文件 -
ENV LD_LIBRARY_PATH是否包含实际库目录 - 是否需要
--network host、--ipc=host等特殊权限 - 是否需要在 Dockerfile 中安装额外包(
apt-get/yum)
七、第六步:构建镜像
7.1 构建前检查上下文
ls -l Dockerfile build/MyProgram config/*.xml runtime-libs/
确保所有 COPY 引用的文件都存在。
7.2 执行构建
docker build --pull=false -t myapp:1.0.0 .
| 参数 | 含义 |
|---|---|
--pull=false |
不尝试从远程更新基础镜像(离线环境必备) |
-t myapp:1.0.0 |
设置镜像名和不可变版本号 |
. |
当前目录为构建上下文 |
7.3 常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
Dockerfile: no such file |
当前目录不对或文件名大小写错误 | pwd + ls -l Dockerfile |
Client.Timeout exceeded |
无法访问 Docker Hub | 改用内网基础镜像 + --pull=false |
COPY failed: no source |
源文件不存在 | 检查文件路径和构建上下文 |
八、第七步:构建后检查(逐层验证)
8.1 检查镜像架构
docker image inspect myapp:1.0.0 --format '{{.Os}}/{{.Architecture}}'
# 必须输出:linux/arm64(或对应架构)
8.2 检查文件是否到位
docker run --rm --entrypoint /bin/sh myapp:1.0.0 -c \
'ls -l /opt/myapp/bin /opt/myapp/config /opt/myapp/lib'
8.3 检查动态库解析
docker run --rm --entrypoint /bin/sh myapp:1.0.0 -c \
'ldd /opt/myapp/bin/MyProgram'
如果还有
not found,回到第 5 步补齐库,重新构建。
8.4 检查程序能否启动
docker run --rm myapp:1.0.0 /opt/myapp/bin/MyProgram --help
九、第八步:容器内运行 & 验收
9.1 同一主机上的多容器
# 终端 1:启动服务端
docker run --rm --network host --ipc=host myapp:1.0.0 \
/opt/myapp/bin/MyServer --config /opt/myapp/config/server.xml
# 终端 2:启动客户端
docker run --rm --network host --ipc=host myapp:1.0.0 \
/opt/myapp/bin/MyClient --config /opt/myapp/config/client.xml
| 参数 | 含义 |
|---|---|
--network host |
使用主机网络(UDP 发现、数据传输) |
--ipc=host |
共享主机 IPC(共享内存通信必需) |
--rm |
退出后自动清理容器 |
9.2 常见"假通过"陷阱
| 现象 | 真正原因 |
|---|---|
| Writer write() 成功,Reader 收到 0 条 | 缺少 --ipc=host,共享内存通道隔离 |
| 端点匹配但无数据 | 检查 Domain/Topic/数据类型是否一致 |
| 连接超时 | 检查防火墙、端口、网络策略 |
记住:验收必须以接收端实际数据为准!
9.3 跨主机部署要点
--ipc=host不能跨主机- 必须明确使用 UDP/TCP,不能依赖共享内存
- 防火墙放通发现端口和数据端口
- 在真实双机环境重新完整测试
十、第九步:打包 & 部署
10.1 导出镜像为 tar
docker save -o myapp-1.0.0.tar myapp:1.0.0
ls -lh myapp-1.0.0.tar
docker save导出的是完整镜像包(含所有层),可直接上传 Harbor 或离线传输。
10.2 直接推送到 Harbor(如果网络可达)
docker tag myapp:1.0.0 <harbor-host>/<project>/myapp:1.0.0
docker push <harbor-host>/<project>/myapp:1.0.0
10.3 版本管理原则
- ✅ 使用不可变版本号:
1.0.0、1.0.1 - ❌ 不要使用
latest(无法追溯) - 任何变更(代码、库、配置、基础镜像)都应递增版本号
十一、运行时配置文件(runtime.yaml)注意事项
如果你的平台使用 runtime.yaml(非 Kubernetes Pod YAML),注意:
apiVersion: runtime.jfounder.io/v1
kind: RuntimeConfig
ports:
- name: management
port: 8080
protocol: TCP
expose: true
management:
portName: management
env:
- name: LD_LIBRARY_PATH
value: "/opt/myapp/lib:/usr/local/lib"
不要写 Kubernetes 字段: metadata、spec、containers、image、command、args
重要: YAML 通过 Schema 校验 ≠ 平台能正确运行你的程序。如果程序不实现平台管理接口,需要额外包装。
十二、通用排错手册
12.1 快速定位流程图
问题出现
│
├─ 构建失败?
│ ├─ Dockerfile 找不到 → 检查 pwd + 文件名
│ ├─ 基础镜像拉不到 → 改用内网镜像 + --pull=false
│ └─ COPY 失败 → 检查源文件是否存在于构建上下文
│
├─ 启动报错?
│ ├─ 动态库缺失 → ldd 查缺 + 补齐 runtime-libs
│ ├─ 配置文件找不到 → 检查容器内路径 vs 宿主机路径
│ └─ 权限不足 → chmod + 检查用户/设备权限
│
├─ 网络通信异常?
│ ├─ 收不到数据 → 检查 --network host + --ipc=host
│ ├─ 端点不匹配 → 检查 Domain/Topic/数据类型
│ └─ 端口不通 → 检查防火墙 + 安全组
│
└─ 架构不匹配?
└─ uname -m + file + docker inspect 三连确认
12.2 错误速查表
| 错误信息 | 根因 | 解决方案 |
|---|---|---|
Dockerfile: no such file |
目录不对或大小写错误 | pwd + ls Dockerfile |
Client.Timeout exceeded |
无法访问公网仓库 | 内网镜像 + --pull=false |
libxxx.so: cannot open |
动态库缺失 | ldd → 找库 → 复制 → 重建 |
write() 成功但收到 0 条 |
缺 --ipc=host |
加上 --ipc=host |
| 架构不匹配 | x86 程序放进 ARM 镜像 | 在 ARM 机器重新编译 |
| XML 找不到 | 用了宿主机路径 | 改用容器内路径 /opt/... |
十三、换项目时的完整检查清单
编译前
- 目标机器架构已确认(
uname -m) - 基础镜像已存在且架构匹配
- 确认工程生成的可执行文件名称(看 CMakeLists.txt / Makefile)
- 确认配置文件清单
- 设置正确的 CMake 前缀路径
编译后
-
file确认架构正确 -
ldd没有not found - 程序在主机上运行通过
- 退出码和业务输出符合预期
构建镜像前
- Dockerfile 位于工程根目录
- 所有
COPY源文件真实存在 - 所有动态库已放入
runtime-libs/ -
ENV LD_LIBRARY_PATH包含库目录 - 不依赖不可用的公网仓库
构建镜像后
- 镜像架构正确(
docker inspect) - 容器内文件路径正确
- 容器内
ldd无not found - 程序可在容器内启动
- 网络、IPC、权限均已验证
- 以接收端实际结果为准
部署前
- 使用不可变版本号(非
latest) - 关键业务测试已重新执行
-
docker save导出 tar 或 Harbor 推送成功 - 镜像名和 Tag 与部署配置一致
- runtime.yaml 使用平台真实 Schema
十四、一条命令流走完全程
以下是换项目时的最短完整流程模板:
# ===== 1. 环境确认 =====
cd /path/to/project
uname -m
docker version
# ===== 2. 设置依赖环境 =====
export MYLIB_HOME=/path/to/install
export CMAKE_PREFIX_PATH="$MYLIB_HOME:${CMAKE_PREFIX_PATH}"
export LD_LIBRARY_PATH="$MYLIB_HOME/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
# ===== 3. 编译 =====
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
# ===== 4. 架构 & 依赖检查 =====
file build/MyProgram
ldd build/MyProgram
# ===== 5. 主机验证 =====
./build/MyProgram --test
# ===== 6. 收集动态库 =====
rm -rf runtime-libs && mkdir -p runtime-libs
cp -a "$MYLIB_HOME/lib"/*.so* runtime-libs/
# 根据 ldd 结果补齐系统库
cp -a /lib/aarch64-linux-gnu/libxxx.so* runtime-libs/
# ===== 7. 构建镜像 =====
docker build --pull=false -t myapp:1.0.0 .
# ===== 8. 构建后检查 =====
docker image inspect myapp:1.0.0 --format '{{.Os}}/{{.Architecture}}'
docker run --rm --entrypoint /bin/sh myapp:1.0.0 -c 'ldd /opt/myapp/bin/MyProgram'
docker run --rm myapp:1.0.0 /opt/myapp/bin/MyProgram --help
# ===== 9. 容器内运行 =====
docker run --rm --network host --ipc=host myapp:1.0.0 \
/opt/myapp/bin/MyProgram --config /opt/myapp/config/app.xml
# ===== 10. 打包 =====
docker save -o myapp-1.0.0.tar myapp:1.0.0
附录:核心命令速查卡
| 命令 | 用途 |
|---|---|
uname -m |
查看 CPU 架构 |
file <binary> |
查看可执行文件架构 |
ldd <binary> |
查看动态库依赖 |
docker image inspect <img> |
查看镜像元数据(架构等) |
docker build --pull=false -t name:tag . |
离线构建镜像 |
docker run --rm -it --network host --ipc=host |
运行容器(同机通信) |
docker save -o file.tar image:tag |
导出镜像为 tar |
docker tag + docker push |
推送到 Harbor |
find / -name 'lib*.so*' |
定位动态库文件 |
dpkg -S <file> / rpm -qf <file> |
查询文件属于哪个包 |
总结: 容器化的本质是依赖管理 + 环境复现。搞清楚"程序需要什么",然后"把所有需要的东西装进一个隔离的文件系统",最后"确保运行时环境和主机打通"。掌握这套思路,任何 C/C++ 工程的容器化都能举一反三。# 从零到交付:通用 C/C++ 项目容器镜像搭建完全指南
本文基于 Fast DDS QoS 工程实践提炼而成,适用于任何 C/C++、Go、Rust 等编译型语言项目的容器化。核心思路:先理解产物 → 确认环境 → 分析依赖 → 编译验证 → 收集库 → 编写 Dockerfile → 构建检查 → 运行测试 → 打包部署。
一、整体思路:容器化的"八步走"战略
容器化不是简单地写个 Dockerfile,而是一条分层递进的工程流水线。每一层都依赖上一层正确完成,任何一层有缺口,后面都会出问题。
┌─────────────────────────────────────────────────┐
│ 第 8 层:打包 & 部署(docker save / push) │
├─────────────────────────────────────────────────┤
│ 第 7 层:容器内运行 & 验收 │
├─────────────────────────────────────────────────┤
│ 第 6 层:构建后检查(架构 / 依赖 / 启动) │
├─────────────────────────────────────────────────┤
│ 第 5 层:编写 Dockerfile │
├─────────────────────────────────────────────────┤
│ 第 4 层:收集运行时动态库(runtime-libs/) │
├─────────────────────────────────────────────────┤
│ 第 3 层:主机上编译 & 验证 │
├─────────────────────────────────────────────────┤
│ 第 2 层:分析依赖(file / ldd / 头文件) │
├─────────────────────────────────────────────────┤
│ 第 1 层:确认环境(架构 / Docker / 基础镜像) │
└─────────────────────────────────────────────────┘
核心原则:程序必须在目标架构上编译,或者使用与目标架构一致的交叉编译环境。 不能在 x86 机器上编译 ARM 程序,然后塞进 ARM 镜像。
二、第一步:确认环境 —— 一切的起点
2.1 确认目标机器架构
uname -m
| 输出 | 含义 |
|---|---|
aarch64 / arm64 |
ARM 64 位(如 Kylin ARM、飞腾、鲲鹏) |
x86_64 |
Intel/AMD 64 位 |
riscv64 |
RISC-V 64 位 |
⚠️ 关键: 后续编译、基础镜像、动态库全部必须与目标架构一致。
2.2 确认 Docker 可用
docker version
检查 Client 和 Server 都正常,重点关注 Server 端版本。
2.3 确认基础镜像
# 列出已有镜像
docker images --format '{{.Repository}}:{{.Tag}}'
# 检查特定镜像的架构
docker image inspect <镜像名> --format '{{.Os}}/{{.Architecture}}'
选择基础镜像的原则:
- 优先使用目标环境已有的内网镜像(避免构建时联网超时)
- 确认架构匹配(
linux/arm64、linux/amd64等) - 不要盲目写
FROM ubuntu:20.04,先看看本地有什么
# ✅ 好的做法:使用内网 ARM64 镜像
FROM 192.168.16.61:30443/library/eclipse-temurin:17.0.19_10-jre-noble
# ❌ 不好的做法:假设能访问 Docker Hub
FROM ubuntu:20.04
三、第二步:分析依赖 —— 搞清楚"需要什么"
C/C++ 程序的依赖分三类,缺一不可:
| 类型 | 说明 | 示例 |
|---|---|---|
| 编译依赖 | 头文件、CMake 配置、静态库 | fastddsConfig.cmake、*.h |
| 运行依赖 | 程序启动时动态加载的 .so |
libssl.so.1.1、libfastdds.so |
| 环境依赖 | 网络、共享内存、权限、配置文件 | --network host、/dev/shm |
3.1 确认可执行文件架构
编译完成后,第一件事就是检查架构:
file build/MyProgram
期望输出:
ELF 64-bit LSB pie executable, ARM aarch64
# 或
ELF 64-bit LSB pie executable, x86-64
如果架构不对(比如在 x86 上编译了程序却要放进 ARM 镜像),立即停止,重新选择构建环境。
3.2 查看动态库依赖(最重要的一步)
ldd build/MyProgram
输出示例:
libfastdds.so.3 => /home/user/opt/fastdds/lib/libfastdds.so.3
libtinyxml2.so.6 => not found ❌ 缺失!
libssl.so.1.1 => not found ❌ 缺失!
libstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6
重点看 not found! 每个 not found 都是一颗定时炸弹。
核心洞察: “目标主机上能运行” ≠ “基础镜像里也能运行”。主机可能已经装了这些库,但容器是独立的文件系统,看不到主机的
/lib。
3.3 确认第三方库安装位置
# 以 Fast DDS 为例
export MYLIB_HOME=/path/to/install/prefix
ls -l "$MYLIB_HOME/include" # 头文件
ls -l "$MYLIB_HOME/lib" # 库文件
3.4 确认 CMake 能找到依赖
export CMAKE_PREFIX_PATH="$MYLIB_HOME:${CMAKE_PREFIX_PATH}"
cmake -S . -B build
CMAKE_PREFIX_PATH:告诉 CMake 去哪里找xxxConfig.cmake、头文件和库-S .:源代码目录-B build:构建输出目录(不污染源代码)
四、第三步:编译 & 主机验证
4.1 设置编译环境
export MYLIB_HOME=/path/to/install/prefix
export CMAKE_PREFIX_PATH="$MYLIB_HOME:${CMAKE_PREFIX_PATH}"
export LD_LIBRARY_PATH="$MYLIB_HOME/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
LD_LIBRARY_PATH只影响当前 Shell,不会自动写入镜像,Dockerfile 里要单独设置。
4.2 编译
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
| 参数 | 含义 |
|---|---|
-DCMAKE_BUILD_TYPE=Release |
发布版本(优化编译) |
-j"$(nproc)" |
按 CPU 核心数并行编译 |
4.3 在主机上先验证(容器化之前必做!)
# 终端 1:启动服务端/订阅端
./build/MyServer --config ./config.xml
# 终端 2:启动客户端/发布端
./build/MyClient --config ./config.xml
验收原则:以接收端为准! 发送端显示"成功"只能说明本地调用成功,不代表对端收到。
经验法则: 如果主机上都跑不通,容器里一定也跑不通。先修好主机上的问题,再谈容器化。
五、第四步:收集运行时动态库
这是整个流程中最容易出错的环节。
5.1 创建干净的库目录
rm -rf runtime-libs && mkdir -p runtime-libs
5.2 复制第三方库
cp -a "$MYLIB_HOME/lib"/*.so* runtime-libs/
cp -a保留符号链接关系,动态库通常是一串软链接(libxxx.so→libxxx.so.3→libxxx.so.3.6.1),不能只复制一个。
5.3 补齐系统库
根据 ldd 的结果,逐个补齐 not found 的库:
# 示例:补齐 TinyXML2 和 OpenSSL
cp -a /lib/aarch64-linux-gnu/libtinyxml2.so.6* runtime-libs/
cp -a /lib/aarch64-linux-gnu/libssl.so.1.1* runtime-libs/
cp -a /lib/aarch64-linux-gnu/libcrypto.so.1.1* runtime-libs/
通用方法(换项目时照做):
- 对每个可执行文件运行
ldd - 找出所有
not found或基础镜像中没有的库 - 用
find/dpkg -S/rpm -qf定位真实文件 - 复制库文件及其所有符号链接
- 构建镜像后再次在容器内
ldd验证
# 辅助定位命令
find /lib /usr/lib "$MYLIB_HOME/lib" -name 'lib*.so*' 2>/dev/null | grep -E '关键词'
dpkg -S libxxx.so.6 # Debian/Ubuntu
rpm -qf /lib/libxxx.so.6 # CentOS/RHEL
六、第五步:编写 Dockerfile
6.1 Dockerfile 模板
# ========================================
# 多阶段构建(推荐)
# ========================================
# ---- 阶段 1:构建阶段(如果支持在容器内编译)----
# FROM <base> AS builder
# WORKDIR /build
# COPY . .
# RUN cmake -S . -B build && cmake --build build -j4
# ---- 阶段 2:运行阶段(精简)----
ARG BASE_IMAGE=<你的内网基础镜像>
FROM ${BASE_IMAGE}
WORKDIR /opt/myapp
# 复制编译好的可执行文件
COPY build/MyProgram /opt/myapp/bin/MyProgram
# 复制配置文件
COPY config/*.xml /opt/myapp/config/
# 复制运行时库
COPY runtime-libs/ /opt/myapp/lib/
# 确保可执行权限
RUN chmod 0755 /opt/myapp/bin/MyProgram
# 设置库搜索路径
ENV LD_LIBRARY_PATH=/opt/myapp/lib:/usr/local/lib
# 默认启动命令(按需修改)
CMD ["/bin/sh"]
6.2 逐项说明
| 指令 | 作用 |
|---|---|
ARG BASE_IMAGE |
允许构建时替换基础镜像 |
FROM |
指定基础镜像和架构 |
WORKDIR |
设置工作目录 |
COPY |
将文件从构建上下文复制到镜像内 |
chmod 0755 |
确保程序有执行权限 |
ENV LD_LIBRARY_PATH |
让程序找到动态库 |
CMD |
默认启动命令 |
6.3 检查清单
-
FROM的架构是否与目标机一致 -
COPY的源文件是否都真实存在 -
WORKDIR、程序路径、配置路径是否一致 -
chmod是否覆盖所有可执行文件 -
ENV LD_LIBRARY_PATH是否包含实际库目录 - 是否需要
--network host、--ipc=host等特殊权限 - 是否需要在 Dockerfile 中安装额外包(
apt-get/yum)
七、第六步:构建镜像
7.1 构建前检查上下文
ls -l Dockerfile build/MyProgram config/*.xml runtime-libs/
确保所有 COPY 引用的文件都存在。
7.2 执行构建
docker build --pull=false -t myapp:1.0.0 .
| 参数 | 含义 |
|---|---|
--pull=false |
不尝试从远程更新基础镜像(离线环境必备) |
-t myapp:1.0.0 |
设置镜像名和不可变版本号 |
. |
当前目录为构建上下文 |
7.3 常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
Dockerfile: no such file |
当前目录不对或文件名大小写错误 | pwd + ls -l Dockerfile |
Client.Timeout exceeded |
无法访问 Docker Hub | 改用内网基础镜像 + --pull=false |
COPY failed: no source |
源文件不存在 | 检查文件路径和构建上下文 |
八、第七步:构建后检查(逐层验证)
8.1 检查镜像架构
docker image inspect myapp:1.0.0 --format '{{.Os}}/{{.Architecture}}'
# 必须输出:linux/arm64(或对应架构)
8.2 检查文件是否到位
docker run --rm --entrypoint /bin/sh myapp:1.0.0 -c \
'ls -l /opt/myapp/bin /opt/myapp/config /opt/myapp/lib'
8.3 检查动态库解析
docker run --rm --entrypoint /bin/sh myapp:1.0.0 -c \
'ldd /opt/myapp/bin/MyProgram'
如果还有
not found,回到第 5 步补齐库,重新构建。
8.4 检查程序能否启动
docker run --rm myapp:1.0.0 /opt/myapp/bin/MyProgram --help
九、第八步:容器内运行 & 验收
9.1 同一主机上的多容器
# 终端 1:启动服务端
docker run --rm --network host --ipc=host myapp:1.0.0 \
/opt/myapp/bin/MyServer --config /opt/myapp/config/server.xml
# 终端 2:启动客户端
docker run --rm --network host --ipc=host myapp:1.0.0 \
/opt/myapp/bin/MyClient --config /opt/myapp/config/client.xml
| 参数 | 含义 |
|---|---|
--network host |
使用主机网络(UDP 发现、数据传输) |
--ipc=host |
共享主机 IPC(共享内存通信必需) |
--rm |
退出后自动清理容器 |
9.2 常见"假通过"陷阱
| 现象 | 真正原因 |
|---|---|
| Writer write() 成功,Reader 收到 0 条 | 缺少 --ipc=host,共享内存通道隔离 |
| 端点匹配但无数据 | 检查 Domain/Topic/数据类型是否一致 |
| 连接超时 | 检查防火墙、端口、网络策略 |
记住:验收必须以接收端实际数据为准!
9.3 跨主机部署要点
--ipc=host不能跨主机- 必须明确使用 UDP/TCP,不能依赖共享内存
- 防火墙放通发现端口和数据端口
- 在真实双机环境重新完整测试
十、第九步:打包 & 部署
10.1 导出镜像为 tar
docker save -o myapp-1.0.0.tar myapp:1.0.0
ls -lh myapp-1.0.0.tar
docker save导出的是完整镜像包(含所有层),可直接上传 Harbor 或离线传输。
10.2 直接推送到 Harbor(如果网络可达)
docker tag myapp:1.0.0 <harbor-host>/<project>/myapp:1.0.0
docker push <harbor-host>/<project>/myapp:1.0.0
10.3 版本管理原则
- ✅ 使用不可变版本号:
1.0.0、1.0.1 - ❌ 不要使用
latest(无法追溯) - 任何变更(代码、库、配置、基础镜像)都应递增版本号
十一、运行时配置文件(runtime.yaml)注意事项
如果你的平台使用 runtime.yaml(非 Kubernetes Pod YAML),注意:
apiVersion: runtime.jfounder.io/v1
kind: RuntimeConfig
ports:
- name: management
port: 8080
protocol: TCP
expose: true
management:
portName: management
env:
- name: LD_LIBRARY_PATH
value: "/opt/myapp/lib:/usr/local/lib"
不要写 Kubernetes 字段: metadata、spec、containers、image、command、args
重要: YAML 通过 Schema 校验 ≠ 平台能正确运行你的程序。如果程序不实现平台管理接口,需要额外包装。
十二、通用排错手册
12.1 快速定位流程图
问题出现
│
├─ 构建失败?
│ ├─ Dockerfile 找不到 → 检查 pwd + 文件名
│ ├─ 基础镜像拉不到 → 改用内网镜像 + --pull=false
│ └─ COPY 失败 → 检查源文件是否存在于构建上下文
│
├─ 启动报错?
│ ├─ 动态库缺失 → ldd 查缺 + 补齐 runtime-libs
│ ├─ 配置文件找不到 → 检查容器内路径 vs 宿主机路径
│ └─ 权限不足 → chmod + 检查用户/设备权限
│
├─ 网络通信异常?
│ ├─ 收不到数据 → 检查 --network host + --ipc=host
│ ├─ 端点不匹配 → 检查 Domain/Topic/数据类型
│ └─ 端口不通 → 检查防火墙 + 安全组
│
└─ 架构不匹配?
└─ uname -m + file + docker inspect 三连确认
12.2 错误速查表
| 错误信息 | 根因 | 解决方案 |
|---|---|---|
Dockerfile: no such file |
目录不对或大小写错误 | pwd + ls Dockerfile |
Client.Timeout exceeded |
无法访问公网仓库 | 内网镜像 + --pull=false |
libxxx.so: cannot open |
动态库缺失 | ldd → 找库 → 复制 → 重建 |
write() 成功但收到 0 条 |
缺 --ipc=host |
加上 --ipc=host |
| 架构不匹配 | x86 程序放进 ARM 镜像 | 在 ARM 机器重新编译 |
| XML 找不到 | 用了宿主机路径 | 改用容器内路径 /opt/... |
十三、换项目时的完整检查清单
编译前
- 目标机器架构已确认(
uname -m) - 基础镜像已存在且架构匹配
- 确认工程生成的可执行文件名称(看 CMakeLists.txt / Makefile)
- 确认配置文件清单
- 设置正确的 CMake 前缀路径
编译后
-
file确认架构正确 -
ldd没有not found - 程序在主机上运行通过
- 退出码和业务输出符合预期
构建镜像前
- Dockerfile 位于工程根目录
- 所有
COPY源文件真实存在 - 所有动态库已放入
runtime-libs/ -
ENV LD_LIBRARY_PATH包含库目录 - 不依赖不可用的公网仓库
构建镜像后
- 镜像架构正确(
docker inspect) - 容器内文件路径正确
- 容器内
ldd无not found - 程序可在容器内启动
- 网络、IPC、权限均已验证
- 以接收端实际结果为准
部署前
- 使用不可变版本号(非
latest) - 关键业务测试已重新执行
-
docker save导出 tar 或 Harbor 推送成功 - 镜像名和 Tag 与部署配置一致
- runtime.yaml 使用平台真实 Schema
十四、一条命令流走完全程
以下是换项目时的最短完整流程模板:
# ===== 1. 环境确认 =====
cd /path/to/project
uname -m
docker version
# ===== 2. 设置依赖环境 =====
export MYLIB_HOME=/path/to/install
export CMAKE_PREFIX_PATH="$MYLIB_HOME:${CMAKE_PREFIX_PATH}"
export LD_LIBRARY_PATH="$MYLIB_HOME/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
# ===== 3. 编译 =====
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
# ===== 4. 架构 & 依赖检查 =====
file build/MyProgram
ldd build/MyProgram
# ===== 5. 主机验证 =====
./build/MyProgram --test
# ===== 6. 收集动态库 =====
rm -rf runtime-libs && mkdir -p runtime-libs
cp -a "$MYLIB_HOME/lib"/*.so* runtime-libs/
# 根据 ldd 结果补齐系统库
cp -a /lib/aarch64-linux-gnu/libxxx.so* runtime-libs/
# ===== 7. 构建镜像 =====
docker build --pull=false -t myapp:1.0.0 .
# ===== 8. 构建后检查 =====
docker image inspect myapp:1.0.0 --format '{{.Os}}/{{.Architecture}}'
docker run --rm --entrypoint /bin/sh myapp:1.0.0 -c 'ldd /opt/myapp/bin/MyProgram'
docker run --rm myapp:1.0.0 /opt/myapp/bin/MyProgram --help
# ===== 9. 容器内运行 =====
docker run --rm --network host --ipc=host myapp:1.0.0 \
/opt/myapp/bin/MyProgram --config /opt/myapp/config/app.xml
# ===== 10. 打包 =====
docker save -o myapp-1.0.0.tar myapp:1.0.0
附录:核心命令速查卡
| 命令 | 用途 |
|---|---|
uname -m |
查看 CPU 架构 |
file <binary> |
查看可执行文件架构 |
ldd <binary> |
查看动态库依赖 |
docker image inspect <img> |
查看镜像元数据(架构等) |
docker build --pull=false -t name:tag . |
离线构建镜像 |
docker run --rm -it --network host --ipc=host |
运行容器(同机通信) |
docker save -o file.tar image:tag |
导出镜像为 tar |
docker tag + docker push |
推送到 Harbor |
find / -name 'lib*.so*' |
定位动态库文件 |
dpkg -S <file> / rpm -qf <file> |
查询文件属于哪个包 |
总结: 容器化的本质是依赖管理 + 环境复现。搞清楚"程序需要什么",然后"把所有需要的东西装进一个隔离的文件系统",最后"确保运行时环境和主机打通"。掌握这套思路,任何 C/C++ 工程的容器化都能举一反三。
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐

所有评论(0)