镜像明明构建成功,容器为什么一启动就退出?
时间:2026/05/06
整理:2026/07/06
关键词:Docker、镜像、容器、Dockerfile、数据卷、网络、Compose、C++、ONNX Runtime、OpenCV
你把 C++ ONNX 程序写进 Dockerfile,docker build 顺利结束,镜像也出现在列表中。可执行下面的命令后,docker ps 里却什么都没有:
1 | docker run -d --name infer cpp-onnx:cpu |
继续查看才发现容器已经退出:有时退出码是 0,因为默认命令执行完就结束;有时是 127,因为缺少 libonnxruntime.so;还有时程序找不到宿主机上的模型路径。镜像“构建成功”只证明每个构建步骤完成了,并不证明容器具备正确的主进程、动态库、挂载、网络、权限和运行配置。
本文从这个失败现场展开,先解释镜像、容器与主进程的关系,再逐步加入多阶段构建、数据卷、网络、Compose 和 C++ ONNX Runtime。最终目标不是记住一页 Docker 命令,而是建立一条可验证的部署契约:镜像交付程序与依赖,运行参数注入环境差异,容器主进程以前台方式运行,日志和退出码能够解释失败。
文中运行的是 Linux 容器。Linux 主机直接共享宿主机内核;macOS 和 Windows 上的 Docker Desktop 通常通过 Linux 虚拟机承载这些容器,因此文件挂载、网络和性能表现可能不同。示例中的版本标签用于固定演示环境,不表示当前最新版;正式项目还应固定镜像 digest、依赖锁定方式并验证供应链来源。
1. 构建成功为什么还不能证明部署成功?
开发一个 Linux 服务器程序时,经常遇到这些问题:
- 本机能跑,换一台机器就缺库、缺配置、缺系统包
- 开发环境、测试环境、生产环境版本不一致
- 部署服务时要手动安装依赖、创建目录、配置端口
- 多个服务共用一台机器,依赖版本互相影响
- 想快速启动、停止、回滚某个服务,但手工操作很容易遗漏步骤
- C++ 程序依赖
.so动态库,拷贝到服务器后运行时报libxxx.so: cannot open shared object file
Docker 的核心思路是:
1 | 把程序 + 依赖库 + 运行配置 + 文件系统环境 |
可以粗略理解成:
1 | Dockerfile -> 描述如何制作镜像 |
对于 C++ / ONNX 部署来说,Docker 最直接的价值是:
1 | 固定 Linux 发行版 |
这样部署时不再依赖“服务器上刚好安装了正确的库”,而是把依赖一起交付。
2. 容器和虚拟机的区别
虚拟机通常包含完整操作系统:
1 | 硬件 |
Docker 容器共享宿主机内核:
1 | 硬件 |
常见差异:
| 对比项 | Docker 容器 | 虚拟机 |
|---|---|---|
| 启动速度 | 通常秒级 | 通常更慢 |
| 资源占用 | 较低 | 较高 |
| 内核 | 共享宿主机内核 | 每个虚拟机有自己的内核 |
| 隔离强度 | 进程级隔离 | 机器级隔离 |
| 镜像大小 | 通常较小 | 通常较大 |
| 适合场景 | 应用打包、部署、测试环境 | 强隔离、多系统内核环境 |
Docker 不是虚拟机。容器本质上还是宿主机上的进程,只是通过 Linux 内核能力做了隔离和资源限制。
Docker 常用的 Linux 内核机制:
namespace:隔离进程、网络、挂载点、用户、主机名等视图cgroups:限制 CPU、内存、I/O 等资源overlayfs:把镜像层叠加成容器文件系统
一个重要结论:
Docker 能隔离用户态环境,但不能改变宿主机内核。Linux 容器运行 Linux 程序,Windows 容器运行 Windows 程序,macOS 上的 Docker Desktop 本质上是在 Linux 虚拟机里运行 Linux 容器。
3. 镜像、容器、仓库和 Dockerfile
3.1 镜像 image
镜像是一个只读模板,里面包含:
- 基础系统文件,比如
ubuntu、debian、alpine - 程序运行依赖,比如
libstdc++、opencv、onnxruntime - 你的程序二进制文件
- 默认启动命令
查看本地镜像:
1 | docker images |
拉取镜像:
1 | docker pull ubuntu:24.04 |
删除镜像:
1 | docker rmi nginx:1.27 |
3.2 容器 container
容器是镜像运行起来之后的实例。一个镜像可以启动多个容器。
运行一个容器:
1 | docker run ubuntu:24.04 echo "hello docker" |
进入交互式 shell:
1 | docker run --rm -it ubuntu:24.04 bash |
查看正在运行的容器:
1 | docker ps |
查看所有容器:
1 | docker ps -a |
停止容器:
1 | docker stop <container_id_or_name> |
删除容器:
1 | docker rm <container_id_or_name> |
3.3 镜像仓库 registry
镜像仓库用来保存和分发镜像。常见仓库:
- Docker Hub
- GitHub Container Registry
- 公司内部镜像仓库
镜像名通常长这样:
1 | nginx:1.27 |
格式大致是:
1 | 仓库地址/命名空间/镜像名:标签 |
工程里尽量固定版本标签,例如:
1 | ubuntu:24.04 |
少用裸 latest,否则同一份 Dockerfile 在不同时间构建出来的环境可能不同。
3.4 Dockerfile
Dockerfile 是构建镜像的说明书。
最小示例:
1 | FROM ubuntu:24.04 |
构建镜像:
1 | docker build -t curl-demo:v1 . |
运行镜像:
1 | docker run --rm curl-demo:v1 |
4. 第一个容器:从 hello-world 到 nginx
运行官方测试镜像:
1 | docker run hello-world |
运行一个后台 nginx:
1 | docker run -d --name web -p 8080:80 nginx:1.27 |
参数含义:
| 参数 | 含义 |
|---|---|
-d |
后台运行 |
--name web |
容器命名为 web |
-p 8080:80 |
把宿主机 8080 端口映射到容器 80 端口 |
nginx:1.27 |
使用的镜像 |
访问:
1 | curl http://127.0.0.1:8080 |
查看日志:
1 | docker logs web |
进入容器:
1 | docker exec -it web sh |
停止并删除:
1 | docker stop web |
临时运行一个容器并自动删除:
1 | docker run --rm -it ubuntu:24.04 bash |
--rm 很适合练习和一次性任务,容器退出后不会留下停止状态的容器。
5. docker run 常用参数
常用形式:
1 | docker run [options] image [command] |
常见参数:
| 参数 | 作用 |
|---|---|
-d |
后台运行 |
-it |
交互式终端 |
--name |
指定容器名 |
--rm |
容器退出后自动删除 |
-p host:container |
端口映射 |
-v host:container |
挂载目录或数据卷 |
-e KEY=VALUE |
设置环境变量 |
--env-file |
从文件读取环境变量 |
--network |
指定网络 |
--restart |
设置重启策略 |
--memory |
限制内存 |
--cpus |
限制 CPU |
--entrypoint |
覆盖镜像入口命令 |
-w |
设置容器工作目录 |
服务例子:
1 | docker run -d \ |
含义:
- 后台运行
myserver:v1 - 容器名叫
myserver - 暴露服务端口
9000 - 设置环境变量
LOG_LEVEL=info - Docker 重启后自动拉起容器
开发环境例子:
1 | docker run --rm -it \ |
含义:
- 把当前目录挂载到容器
/work - 容器退出后自动删除
- 在容器内直接操作当前工程目录
ONNX 推理例子:
1 | docker run --rm -it \ |
这里的 :ro 表示只读挂载,适合模型和输入数据,避免容器意外改动原始文件。
6. 容器生命周期与常用管理命令
容器生命周期大致是:
1 | create -> start -> running -> stop -> removed |
常用命令:
1 | docker create --name app myserver:v1 |
查看容器状态:
1 | docker inspect app |
查看容器资源占用:
1 | docker stats |
查看容器内进程:
1 | docker top app |
注意:
容器的主进程退出后,容器就会停止。服务容器通常以前台方式运行服务进程,而不是在容器内部再把服务放到后台。
错误写法:
1 | ./myserver & |
正确思路:
1 | CMD ["/app/myserver"] |
这里使用 JSON 数组形式,也叫 exec form。程序会直接成为容器内的 PID 1,更容易收到 docker stop 发出的终止信号。下面的 shell form 会额外经过 /bin/sh -c,如果没有正确转发信号,服务可能等到超时后才被强制结束:
1 | # 长期服务不推荐这样写 |
C++ 服务本身仍要正确处理 SIGTERM:停止接收新请求、让正在处理的任务在有上限的时间内结束、刷新必要数据,然后返回明确退出码。如果程序还会创建并遗留子进程,可评估 docker run --init 或在镜像中使用专门 init;不要让 shell 脚本无意中变成不转发信号的 PID 1。
如果需要临时查看容器为什么退出:
1 | docker ps -a |
7. Dockerfile:构建自己的镜像
7.1 常用 Dockerfile 指令
| 指令 | 含义 |
|---|---|
FROM |
指定基础镜像 |
WORKDIR |
设置工作目录 |
COPY |
复制文件到镜像 |
RUN |
构建镜像时执行命令 |
ENV |
设置环境变量 |
ARG |
设置构建参数 |
EXPOSE |
声明容器服务端口 |
CMD |
默认启动命令 |
ENTRYPOINT |
固定入口命令 |
RUN 和 CMD 的区别:
RUN在构建镜像时执行,结果会写进镜像层CMD在容器启动时执行,是容器的默认主进程
EXPOSE 9000 也只是镜像元数据,用来声明程序预期监听的端口;它不会自动把端口发布到宿主机。外部访问仍需 docker run -p 9000:9000 ... 或 Compose ports。容器内程序还必须监听容器可访问的地址,不能只绑定 127.0.0.1 后又期待宿主机端口映射生效。
例子:
1 | FROM ubuntu:24.04 |
7.2 镜像分层和构建缓存
Dockerfile 每条指令通常会形成一层。构建时 Docker 会尽量复用缓存。
缓存命中思路:
1 | 变化少的步骤放前面 |
C++ 项目常见写法:
1 | WORKDIR /src |
如果项目依赖文件单独存在,也可以先复制依赖描述,再复制源码,以便依赖安装层复用缓存。
7.3 .dockerignore
.dockerignore 用来排除不该发送给 Docker 构建上下文的文件。
推荐示例:
1 | .git |
注意:
- 不要把大型数据集、模型权重、实验输出直接打进镜像
- 模型文件更常见的做法是运行时挂载到
/models - 输入数据挂载到
/data - 结果输出挂载到
/outputs
8. 案例一:打包一个 C++ TCP 服务端
假设项目结构:
1 | server/ |
可以写一个多阶段构建 Dockerfile:
1 | FROM ubuntu:24.04 AS builder |
构建:
1 | docker build -t myserver:v1 . |
运行:
1 | docker run -d --name myserver -p 9000:9000 myserver:v1 |
测试:
1 | nc 127.0.0.1 9000 |
查看日志:
1 | docker logs -f myserver |
多阶段构建的好处:
- 编译工具链只留在
builder阶段 - 最终镜像只包含运行服务所需文件
- 镜像更小,攻击面也更小
- C++ 编译依赖和运行依赖分离,部署更清楚
9. 数据卷:保存数据与挂载工程目录
容器文件系统默认是临时的。容器删除后,容器内部写入的数据也会丢失。
如果要保存数据,应使用数据卷或目录挂载。
9.1 命名卷
创建数据卷:
1 | docker volume create mysql-data |
使用数据卷:
1 | docker run -d \ |
查看数据卷:
1 | docker volume ls |
删除数据卷:
1 | docker volume rm mysql-data |
命名卷适合数据库、缓存持久化目录等不需要你直接手动编辑的内容。
9.2 宿主机目录挂载
1 | docker run --rm -it \ |
含义:
- 把当前目录挂载到容器
/work - 容器工作目录设置成
/work - 容器里修改
/work,宿主机当前目录也会变化
工程经验:
- 数据库数据更适合使用命名卷
- 开发调试更适合使用宿主机目录挂载
- 模型和输入数据通常用只读挂载
:ro - 输出目录用可写挂载,例如
-v "$PWD/outputs":/outputs - 不要把敏感目录随意挂进容器,比如
/、/etc、/var/run/docker.sock
9.3 C++ ONNX 项目的目录挂载约定
推荐把路径固定成:
1 | /app 程序目录 |
运行命令:
1 | docker run --rm -it \ |
这样做的好处:
- 镜像不绑定某个具体模型文件
- 换模型不需要重新构建镜像
- 输入和输出路径清晰
- 生产环境和本地测试可以使用同一套容器启动方式
10. Docker 网络:端口映射与容器互联
Docker 默认会创建一个 bridge 网络。容器之间如果在同一个自定义网络里,可以通过容器名互相访问。
创建网络:
1 | docker network create app-net |
运行 Redis:
1 | docker run -d --name redis --network app-net redis:7 |
运行应用:
1 | docker run -d \ |
此时 app 容器里可以用 redis:6379 访问 Redis。
常见网络模式:
| 模式 | 含义 |
|---|---|
bridge |
默认桥接网络,最常用 |
host |
直接使用宿主机网络命名空间,Linux 上常见 |
none |
不配置网络 |
| 自定义 bridge | 推荐给一组服务使用 |
注意:
-p 8080:80是把容器端口暴露给宿主机- 同一 Docker 网络内,容器之间通常不需要
-p - 容器互访时使用容器内部端口,不使用宿主机映射端口
- 生产环境应只暴露外部真正需要访问的端口
C++ 推理程序如果只是离线图片或视频处理,通常不需要暴露端口。
如果它是 HTTP / gRPC 推理服务,才需要 -p 映射服务端口。
11. Docker Compose:管理多容器项目
当一个项目包含多个服务,比如:
1 | web server + redis + mysql |
继续手写多个 docker run 命令会很麻烦。Docker Compose 用一个 compose.yaml 描述这些容器。
示例:
1 | services: |
普通短写法 depends_on: [redis] 只保证启动顺序,不保证 Redis 已经能处理请求。这里用 healthcheck 和 condition: service_healthy 等待依赖进入健康状态;应用自身仍应实现连接超时与有上限重试,因为运行期间 Redis 依旧可能重启或失联。
container_name 也不是服务发现所必需的。Compose 默认网络已经可以使用服务名 redis 解析依赖,省略固定容器名更利于项目隔离和水平扩展。
启动:
1 | docker compose up -d |
查看状态:
1 | docker compose ps |
查看日志:
1 | docker compose logs -f app |
停止并删除容器:
1 | docker compose down |
连数据卷一起删除:
1 | docker compose down -v |
11.1 C++ ONNX 离线推理 Compose 示例
1 | services: |
运行一次推理:
1 | docker compose run --rm onnx-infer |
如果程序是长期服务,可以改成:
1 | services: |
12. 调试容器:日志、进入容器、排查依赖
12.1 查看日志
1 | docker logs -f myserver |
Compose 项目:
1 | docker compose logs -f app |
12.2 进入容器
1 | docker exec -it myserver sh |
如果镜像里有 bash:
1 | docker exec -it myserver bash |
一次性调试入口:
1 | docker run --rm -it --entrypoint bash cpp-onnx:cpu |
12.3 查看配置和挂载
1 | docker inspect myserver |
重点看:
MountsEnvNetworkSettingsStateConfig.CmdConfig.Entrypoint
12.4 查看端口
1 | docker port myserver |
容器内看监听端口:
1 | docker exec -it myserver ss -lntp |
12.5 排查 C++ 动态库
C++ 容器化最常见问题之一是动态库缺失。
进入容器后执行:
1 | ldd /app/onnx_demo |
如果看到:
1 | libonnxruntime.so.1 => not found |
说明运行阶段没有找到 ONNX Runtime 动态库。解决方式通常是:
- 把
libonnxruntime.so*复制到/usr/local/lib - 执行
ldconfig - 或者设置
LD_LIBRARY_PATH - 或者在 CMake 中设置 RPATH
更推荐的运行阶段做法:
1 | COPY --from=builder /opt/onnxruntime/lib/libonnxruntime.so* /usr/local/lib/ |
12.6 临时启动一个网络排查容器
1 | docker run --rm -it --network app-net nicolaka/netshoot |
这个镜像常用于网络排查,里面带了 curl、dig、tcpdump、ss 等工具。
13. 案例二:用 Docker 搭建 C++ 编译环境
有时不急着把程序打成最终镜像,只是想要一个干净、可复制的 C++ 编译环境。
13.1 临时编译一个 C++ 文件
当前目录有 main.cpp:
1 |
|
进入编译环境:
1 | docker run --rm -it \ |
容器里安装编译器并编译:
1 | apt-get update |
这个适合临时验证,但不适合正式项目,因为每次进入容器都要重新安装工具链。
13.2 固定一个 C++ 开发镜像
Dockerfile.dev:
1 | FROM ubuntu:24.04 |
构建:
1 | docker build -f Dockerfile.dev -t cpp-dev:ubuntu24 . |
使用:
1 | docker run --rm -it \ |
容器里构建 CMake 项目:
1 | cmake -S . -B build -DCMAKE_BUILD_TYPE=Release |
这种方式适合:
- 学习 CMake
- 验证 Linux 编译
- 在 macOS 上模拟 Linux 构建环境
- 快速复现别人项目的依赖环境
14. 实战:部署 C++ ONNX Runtime 推理环境
目标:把一个 C++ ONNX Runtime 推理程序打进 Docker 镜像,在容器内加载 ONNX 模型,用 OpenCV 读图或读视频,输出推理结果。
典型链路:
1 | 宿主机准备模型和输入数据 |
14.1 推荐项目结构
1 | cpp-onnx-demo/ |
建议:
src/放 C++ 源码models/放模型,运行时挂载,不建议打进镜像data/放测试图片或视频,运行时挂载outputs/放推理结果- 镜像里只放程序和运行依赖
14.2 CMakeLists.txt 示例
如果项目没有现成的 CMake,可以用下面这种最小结构。这里假设 ONNX Runtime 安装在 /opt/onnxruntime。
1 | cmake_minimum_required(VERSION 3.20) |
如果运行时报找不到 libonnxruntime.so,优先在 Dockerfile 运行阶段复制动态库并执行 ldconfig。
14.3 CPU 版本 Dockerfile
这个 Dockerfile 使用多阶段构建:
builder阶段安装编译器、CMake、OpenCV 开发包、ONNX Runtimeruntime阶段只保留运行程序需要的依赖
ORT_VERSION 要固定为你已经验证过的版本。下面以 2026 年 7 月可用的 1.26.0 为示例;以后升级时应重新核对 Release 资产名、opset/EP 兼容性和数值回归,不能只改版本字符串。
1 | FROM ubuntu:24.04 AS builder |
为了让示例跨 Ubuntu 小版本更容易阅读,runtime 阶段直接安装了 libopencv-dev,但它会带入头文件和不必要的开发内容。生产镜像应根据 ldd /app/onnx_demo 和实际编解码功能安装对应运行时包,再用集成测试确认没有漏库;不要只为追求镜像体积随意删除依赖。
BuildKit 会为目标平台提供 TARGETARCH。上面的 Dockerfile把 Docker 的 amd64/arm64 映射到 ONNX Runtime Release 资产使用的 x64/aarch64,遇到其他架构则直接失败,避免悄悄下载错误二进制。构建前仍应到对应 Release 页面确认该版本确实发布了目标架构资产,并在供应链要求较高的环境中校验下载文件的摘要。
Apple Silicon 上可以显式构建 --platform=linux/amd64 并通过虚拟化/仿真运行 x86_64 镜像,但这更适合兼容性验证,不能用于代表真实 x86_64 服务器的性能 benchmark。生产镜像应尽量在目标架构上构建和测试。
构建:
1 | docker build \ |
运行:
1 | mkdir -p outputs |
进入容器排查:
1 | docker run --rm -it \ |
容器内检查:
1 | ls -lh /models |
14.4 .dockerignore 示例
1 | .git |
这里排除 models、data、outputs 是为了避免把大文件打进镜像。运行时用 -v 挂载。
14.5 Compose 版本
compose.yaml:
1 | services: |
构建并运行:
1 | docker compose build onnx-infer |
14.6 C++ 程序应支持的最小配置
为了让容器启动命令简单,C++ 程序建议从环境变量读取路径:
1 |
|
实际推理代码里至少要把这些错误打到 stdout / stderr:
- 模型文件不存在
- 图片或视频打开失败
- ONNX Runtime 创建 Session 失败
- 输入 shape 和程序预期不一致
- 输出 tensor shape 不符合后处理逻辑
- 输出目录不可写
容器里日志最好直接输出到终端,不要只写到容器内部文件,否则 docker logs 看不到。
14.7 ONNX Runtime C++ 初始化要点
核心配置通常包括:
1 | Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "onnx_demo"); |
建议:
- 先跑通
CPUExecutionProvider - 先建立 FP32 正确性基线
- 再测试图优化、线程数、INT8、CUDA 或 TensorRT
- 每次只改一个变量,记录推理耗时和端到端耗时
14.8 预处理和后处理不要藏在 Docker 里
Docker 只负责环境一致,不能替你保证模型结果正确。
C++ ONNX 推理程序仍然要确认:
1 | BGR / RGB 是否一致 |
容器化后的验证顺序:
1 | 同一张图片 |
如果 Docker 内结果不一致,优先排查:
- OpenCV 版本差异
- 模型文件是否同一个
- 输入图片是否同一个
- 工作目录和相对路径
- 动态库版本
- 线程数和非确定性后处理
14.9 CPU 性能配置
运行时限制资源:
1 | docker run --rm -it \ |
程序内部也要避免线程过度竞争:
1 | 推理 worker 数 × ONNX Runtime intra_op 线程数 |
不要明显大于机器实际物理核心数。
性能统计建议至少拆成:
1 | decode_ms |
只看 FPS 不够,因为 FPS 不能告诉你瓶颈在模型推理、预处理、后处理还是视频读写。
14.10 可选:CUDA / GPU 版本
如果部署目标是 NVIDIA GPU,通常需要:
- 宿主机有 NVIDIA 驱动
- 宿主机已配置 NVIDIA Container Toolkit
- 镜像使用 CUDA / cuDNN 运行时基础镜像
- ONNX Runtime 使用 GPU 包
- 启动容器时加
--gpus all
注意:
macOS Docker Desktop 不能直接提供 NVIDIA CUDA GPU 给 Linux 容器。CUDA 容器部署通常针对 Linux + NVIDIA GPU 服务器。
GPU 版 Dockerfile 只展示关键差异:
1 | FROM nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04 AS builder |
关键点:
devel镜像用于编译,runtime镜像用于运行builder和runtime尽量使用同一个 Ubuntu 版本- GPU 版 ONNX Runtime 包名通常带
gpu - 程序内部还要显式注册 CUDA Execution Provider
运行:
1 | docker run --rm -it \ |
GPU 版本不要只看容器能不能启动,还要确认程序确实启用了 CUDA Execution Provider。否则可能仍然在 CPU 上跑。
这个 GPU 示例使用 linux-x64-gpu 资产,目标是常见的 x86_64 Linux + NVIDIA GPU 服务器。Jetson 等 ARM64 NVIDIA 平台的 CUDA、TensorRT、JetPack 与 ONNX Runtime 组合不同,不能只把 x64 替换成 aarch64 就假定可用,必须依据目标设备版本矩阵选择官方包或自行构建。
14.11 部署检查脚本思路
正式部署前可以准备一个最小自检命令:
1 | docker run --rm \ |
自检应确认:
1 | 模型文件存在 |
15. 常见问题和排查思路
15.1 容器启动后立刻退出
原因通常是主进程执行完了。
例如:
1 | docker run ubuntu:24.04 |
这条命令没有前台任务,容器会马上退出。
可以这样进入 shell:
1 | docker run -it ubuntu:24.04 bash |
服务类容器应该以前台方式运行:
1 | CMD ["/app/myserver"] |
不要在容器启动命令里把服务放到后台:
1 | ./myserver & |
15.2 宿主机访问不到容器服务
检查:
- 程序是否监听
0.0.0.0,而不是只监听127.0.0.1 - 是否用了
-p host_port:container_port - 容器内服务端口是否正确
- 防火墙是否阻止连接
查看监听:
1 | docker exec -it myserver ss -lntp |
15.3 容器之间访问不到
检查:
- 两个容器是否在同一个 Docker 网络
- 是否使用容器名作为主机名
- 服务端口是否是容器内部端口,而不是宿主机映射端口
15.4 修改代码后镜像没有变化
重新构建:
1 | docker build -t myserver:v1 . |
如果缓存导致结果不符合预期:
1 | docker build --no-cache -t myserver:v1 . |
--no-cache 适合诊断缓存相关假设,不应成为每次构建的默认修复。先确认构建上下文是否正确、源码是否被 .dockerignore 排除、COPY 是否覆盖目标文件,以及运行的容器是否真的来自刚构建的 tag。否则禁用缓存只会让构建更慢,并不能修复错误的上下文或镜像标签。
15.5 磁盘空间被占满
查看空间:
1 | docker system df |
清理未使用资源:
1 | docker system prune |
清理未使用镜像、容器、网络和构建缓存:
1 | docker system prune -a |
注意:清理命令会删除未使用资源,执行前要确认没有需要保留的镜像或容器。
15.6 C++ 程序找不到动态库
典型错误:
1 | error while loading shared libraries: libonnxruntime.so.1: cannot open shared object file |
排查:
1 | ldd /app/onnx_demo |
解决:
1 | COPY --from=builder /opt/onnxruntime/lib/libonnxruntime.so* /usr/local/lib/ |
或者运行时设置:
1 | ENV LD_LIBRARY_PATH=/usr/local/lib:${LD_LIBRARY_PATH} |
更工程化的方式是在 CMake 安装阶段设置 RPATH,不过对初学部署来说,先用 ldconfig 更直接。
15.7 模型路径在本机存在,容器里不存在
本机路径:
1 | /Users/me/project/models/model.onnx |
容器内不一定存在。必须显式挂载:
1 | docker run --rm -it \ |
程序里不要硬编码宿主机绝对路径。
15.8 OpenCV 读不到图片或视频
检查:
- 文件是否挂载进容器
- 容器内路径是否正确
- OpenCV 是否安装了需要的图片或视频编解码依赖
- 文件名大小写是否一致
- 程序是否用相对路径,工作目录是否符合预期
容器内检查:
1 | ls -lh /data |
15.9 ONNX Runtime 能加载模型,但结果不对
这通常不是 Docker 的问题,而是推理链路问题。
优先检查:
1 | 输入尺寸 |
对照 Python 推理结果做一张图片的最小复现。
15.10 容器内速度和宿主机不同
可能原因:
- Docker Desktop 在 macOS / Windows 上经过 Linux 虚拟机
- 挂载目录 I/O 比容器内部文件慢
- CPU 和内存限制不同
- ONNX Runtime 线程数不同
- OpenCV / ORT 动态库版本不同
- GPU 没有透传,实际在 CPU 上跑
建议记录:
1 | 镜像 tag |
16. 推荐实践
16.1 Dockerfile
- 使用明确的镜像标签,比如
ubuntu:24.04,少用裸latest - 使用多阶段构建,编译环境和运行环境分离
- 把变化少的步骤放在前面,提升构建缓存命中率
- 构建完成后清理包管理器缓存
.dockerignore排除build/、.git/、日志、数据集、模型和输出结果- 镜像中不要保存密码、私钥和 token
- 构建阶段需要凭据时使用 BuildKit secret/SSH mount,不要通过
ARG、ENV或COPY传入 - 运行阶段在条件允许时使用固定的非 root 用户,并提前验证 bind mount 的 UID/GID 和写权限
- 不要把大型模型和数据集直接打进镜像,优先运行时挂载
16.2 容器运行
- 用
--name给重要容器命名 - 用
--restart unless-stopped管理服务自动重启 - 用
docker logs观察服务日志 - 用数据卷保存持久化数据
- 用自定义网络连接一组服务
- 模型和输入数据用只读挂载
- 输出目录用单独可写挂载
- 给容器设置合理的 CPU 和内存限制
16.3 C++ 服务
- 服务进程以前台方式运行
- 日志输出到
stdout/stderr - 配置通过环境变量、配置文件或挂载注入
- 不把重要数据只写在容器临时文件系统里
- 只暴露必要端口
- 用
ldd检查动态库依赖 - 对外提供明确的退出码和错误日志
16.4 ONNX 部署
- 先确认模型输入输出契约,再写 Dockerfile
- 先跑通 CPU,再考虑 CUDA、TensorRT 或其他 EP
- 先建立 FP32 基线,再测试 INT8
- 模型文件和配置文件运行时挂载
- 推理程序打印 ORT 版本、模型路径、输入 shape 和线程配置
- 性能日志拆分预处理、推理、后处理和总耗时
- 不要把 Docker 环境问题和模型数值问题混在一起排查
17. 常用命令速查
| 命令 | 作用 |
|---|---|
docker version |
查看 Docker 客户端和服务端版本 |
docker info |
查看 Docker 系统信息 |
docker images |
查看本地镜像 |
docker pull image:tag |
拉取镜像 |
docker build -t name:tag . |
构建镜像 |
docker ps |
查看运行中的容器 |
docker ps -a |
查看所有容器 |
docker run image |
创建并启动容器 |
docker stop name |
停止容器 |
docker rm name |
删除容器 |
docker rmi image |
删除镜像 |
docker logs -f name |
跟踪容器日志 |
docker exec -it name sh |
进入容器 shell |
docker inspect name |
查看容器详细信息 |
docker stats |
查看资源占用 |
docker system df |
查看 Docker 磁盘占用 |
docker system prune |
清理未使用资源 |
docker compose up -d |
后台启动 Compose 项目 |
docker compose down |
停止并删除 Compose 容器 |
docker compose logs -f app |
查看 Compose 服务日志 |
docker compose run --rm app |
一次性运行 Compose 服务 |
调试 C++ 程序常用:
| 命令 | 作用 |
|---|---|
ldd /app/onnx_demo |
查看动态库是否缺失 |
printenv |
查看环境变量 |
pwd |
查看当前工作目录 |
ls -lh /models |
查看模型目录是否挂载成功 |
ls -lh /data |
查看输入目录是否挂载成功 |
ls -lh /outputs |
查看输出目录是否挂载成功 |
18. 回到开头:容器为什么会立即退出?
因为容器不是一台需要“保持开机”的虚拟机,它的生命周期跟随主进程。镜像构建成功后,如果默认命令很快执行完、动态库加载失败、模型路径不存在,或者程序因权限问题返回,PID 1 都会结束,容器也随之停止。正确排查顺序是 docker ps -a → docker logs → docker inspect → 检查挂载、动态库和启动命令,而不是先给容器加无限循环。
Docker 的主线可以压缩成一句话:
1 | Dockerfile 构建镜像,镜像启动容器,容器运行服务,Compose 编排多个服务。 |
学习顺序建议:
- 先掌握
docker run、docker ps、docker logs、docker exec - 再理解镜像、容器、端口映射、数据卷、网络
- 然后学会写 Dockerfile
- 再用多阶段构建打包 C++ 程序
- 最后用 Docker Compose 管理多服务项目
C++ ONNX 容器化部署检查:
1 | 1. C++ 程序本机能编译、能运行 |
对于 Linux 高性能服务器编程来说,Docker 是部署工具,不是性能模型本身。
它能让服务更容易交付、复制和回滚,但服务的并发能力仍然取决于程序本身的 I/O、线程、内存、协议设计,以及 ONNX 推理链路里的预处理、模型执行和后处理实现。
最后保留四条工程结论:
- 构建阶段验证如何生成文件,运行阶段才验证主进程和部署契约。
- 长期服务使用 exec form 前台运行,并正确处理
SIGTERM与退出码。 - Compose 的启动顺序不等于依赖就绪,健康检查也不能替代应用自身的超时和重试。
- 镜像标签、CPU 架构、动态库和模型 hash 都是可复现部署的一部分,不能只保存一份 Dockerfile。
参考资料
- Dockerfile reference:https://docs.docker.com/reference/dockerfile/
- Docker Compose startup order:https://docs.docker.com/compose/how-tos/startup-order/
- Docker build cache optimization:https://docs.docker.com/build/cache/optimize/
- Docker build secrets:https://docs.docker.com/build/building/secrets/
- ONNX Runtime releases:https://github.com/microsoft/onnxruntime/releases