linux下创建c++项目的docker镜像和容器
我们系统跑在linux下,有多个应用程序,我想用微服务架构来承载。好处之一是便于管理,可以通过portainer集中管理,省却了开机重启的设置,手动启停也方便,还容易观察到状态和日志,总之好处多多。
一、概述
构建c++项目的docker镜像,跟java项目一样,都是在项目中创建一个Dockerfile文件,然后就可以构建镜像,接着创建容器。就这么简单。
但这不是一个简单的任务。c++项目没有虚拟机的概念,许多依赖库都要在Dockerfile里申明,构建的过程中引入,所以Dockerfile的内容比java的要长得多。由于我的c++项目使用了CMake构建,所以Dockerfile也跟CMakeLists.txt紧密相关。
另外,按照AI的建议,构建镜像时采用多阶段构建的模式,即构建镜像过程中,分为构建阶段(编译阶段)和运行时阶段(发布打包阶段),在这些不同的阶段,可以指定不同的基础镜像,好处就是可以减少最终出来的镜像大小。这比较容易理解,编译时可能涉及到多方面,需要的内容就多,镜像就比较大;等编译好了,生成成品,尺寸就比较小。但是,我在构建的过程中,发现不同阶段,基础镜像需要是同一种,否则能构建,但运行时有问题,提示少了什么东西。
二、Dockerfile
1、直接贴出我完整的Dockerfile镇楼
# 第一阶段:构建阶段
FROM ubuntu:24.04 AS builder# 设置工作目录
WORKDIR /ackctrl# 更新包列表并安装必要的工具和依赖项
RUN apt-get update && apt-get install -y \gcc g++ \wget \make \cmake \libboost-system-dev \libboost-thread-dev \libssl-dev \zlib1g-dev \uuid-dev \libjsoncpp-dev \doxygen \graphviz \dia \pkg-config \libvisa-dev \&& rm -rf /var/lib/apt/lists/*# 将工作目录设为tmp
# 此命令有2个作用:1是之后的操作都从这里出发,而是如果没有这个目录的话则创建一个
WORKDIR /tmp# Hiredis
# 复制解压后的 Hiredis 源码到容器中
COPY ./docker/hiredis-1.2.0 /tmp/hiredis# 编译并安装 Hiredis
RUN cd /tmp/hiredis && \mkdir build && cd build && \cmake .. && \make -j$(nproc) && \make install && \ldconfig && \cd / && rm -rf /tmp/hiredis# 验证 Hiredis 安装
# RUN pkg-config --modversion hiredis && \
# find /usr/local -name "HiredisConfig.cmake"# MySQL
# 复制 MySQL Connector/C++ 的 DEB 包到容器中
COPY ./docker/libmysqlcppconn10_9.2.0-1ubuntu24.04_amd64.deb /tmp/libmysqlcppconn10.deb
COPY ./docker/libmysqlcppconn-dev_9.2.0-1ubuntu24.04_amd64.deb /tmp/libmysqlcppconn-dev.deb
COPY ./docker/libmysqlcppconnx2_9.2.0-1ubuntu24.04_amd64.deb /tmp/libmysqlcppconnx2.deb# 安装 MySQL Connector/C++ 的运行时和开发库
RUN dpkg -i /tmp/libmysqlcppconn10.deb /tmp/libmysqlcppconn-dev.deb /tmp/libmysqlcppconnx2.deb && \apt-get install -f -y && \rm -rf /tmp/*.deb# 验证安装
# RUN dpkg -l | grep mysqlcppconn && \
# find /usr -name "mysql" && \
# ls /usr/lib/x86_64-linux-gnu | grep mysqlcppconn# Drogon
# 将本地的 drogon 源码复制到容器中
COPY ./docker/drogon-master /tmp/drogon-src# 初始化子模块(如果需要)
WORKDIR /tmp/drogon-srcRUN mkdir -p build && cd build && \cmake .. -DBUILD_SHARED_LIBS=ON && \make -j$(nproc) && \make install && \cd / && rm -rf /tmp/drogon-src# 验证 Drogon 和 Trantor 是否安装成功
# RUN find /usr -name "libdrogon.so*" && \
# find /usr/local -name "libdrogon.so*"
# RUN find /usr -name "libtrantor.so*" && \
# find /usr/local -name "libtrantor.so*"# 回到工作目录
WORKDIR /ackctrl# 将 AckCtrl 源码复制到容器中
COPY . /ackctrl# 创建构建目录并编译应用代码
RUN mkdir -p build && cd build && \cmake -DCMAKE_PREFIX_PATH=/usr/local /ackctrl && \make # 第二阶段:运行时(多阶段构建)
#FROM debian:stable-slim
FROM ubuntu:24.04WORKDIR /ackctrl# 删除可能存在的旧版本动态库
RUN rm -f /usr/local/lib/libdrogon.so* && \rm -f /usr/lib/x86_64-linux-gnu/libdrogon.so*# 从构建阶段复制动态链接库
COPY --from=builder /usr/local/lib/libhiredis.so* /usr/local/lib/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libmysqlcppconnx.so* /usr/local/lib/
COPY --from=builder /usr/local/lib/libdrogon.so* /usr/local/lib/
COPY --from=builder /usr/local/lib/libtrantor.so* /usr/local/lib/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libssl.so.3 /usr/local/lib/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libcrypto.so.3 /usr/local/lib/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libjsoncpp.so.25 /usr/local/lib/# 从构建阶段复制可执行文件到运行时镜像
#COPY --from=builder /ackctrl/build/UnderwtConn .# 创建符号链接
RUN ln -sf libdrogon.so.1.9.10 /usr/local/lib/libdrogon.so.1 && \ln -sf libdrogon.so.1 /usr/local/lib/libdrogon.so && \ln -sf /usr/local/lib/libtrantor.so.1 /usr/local/lib/libtrantor.so# 更新动态链接库缓存
RUN ldconfig# 暴露默认端口
EXPOSE 10992# 运行程序
CMD ["./UnderwtConn"]
2、CMakeLists.txt
我的C++项目使用了CMake构建,所以Dockerfile与CMakeLists.txt强相关。下面是我的CMakeLists.txt的完整版本。有关CMakeLists.txt的理解,有兴趣也可以阅读拙作《C++程序从windows移植到linux后cmake脚本CMakeLists.txt的修改》
cmake_minimum_required (VERSION 3.25)cmake_policy(SET CMP0153 OLD)project ("UnderwtConn")# 将实现文件添加到此项目的可执行文件
add_executable (UnderwtConn
"UnderWtConn.cpp"
"controller/DeviceController.cpp"
"util/conf_util.cpp"
"util/redis_util.cpp"
"util/path_util.cpp"
"util/time_util.cpp"
"util/json_util.cpp"
"util/hex_util.cpp"
"util/mysqlconn.cpp"
"util/global_map_util.cpp"
"util/general.cpp"
"util/cbx_operate.cpp"
"entity/ConnBoxSIIM.cpp"
"entity/Gateway.cpp"
"entity/LIMClass.cpp"
"entity/HangYuPower.cpp"
"config/ConfigManager.cpp"
"lib/canClient.cpp")find_package(Drogon CONFIG REQUIRED)
find_package(Boost REQUIRED COMPONENTS system thread)#告诉 CMake 尝试找到版本至少为 1.1.0 的 Hiredis 库,并且要求必须找到这个包才能继续执行后续的构建过程
#CONFIG:指示 CMake 使用“配置”模式来查找包。这意味着 CMake 期望找到由该软件包提供的配置文件
#REQUIRED:表示这是一个必需的依赖项,如果找不到就抛出错误
find_package(Hiredis 1.1 CONFIG REQUIRED) # 指定 hiredis 版本为 1.1.0
# 如果不存在 hiredis::hiredis 目标,则创建一个
if(NOT TARGET hiredis::hiredis)add_library(hiredis::hiredis UNKNOWN IMPORTED)set_target_properties(hiredis::hiredis PROPERTIESIMPORTED_LOCATION "${HIREDIS_LIBRARIES}"INTERFACE_INCLUDE_DIRECTORIES "${HIREDIS_INCLUDE_DIRS}")
endif()# 指定额外的头文件搜索路径。
# 具体来说,它告诉编译器在编译过程中除了标准的头文件目录外,还需要检查 /usr/include/mysql 目录下的头文件
# 类似于 target_include_directories,但后者更精细。目前推荐使用 target_include_directories
include_directories(/usr/include/mysql)# 查找 MySQL Connector/C++ 头文件目录
find_path(MYSQL_CONNECTOR_CPP_INCLUDE_DIRNAMES mysqlx/xdevapi.hPATHS /usr/include /usr/local/include /usr/include/mysql-cppconn)
find_library(MYSQL_CONNECTOR_CPP_LIBRARYNAMES mysqlcppconnx # 直接指定具体版本PATHS /usr/lib/x86_64-linux-gnu)
if(NOT MYSQL_CONNECTOR_CPP_INCLUDE_DIR OR NOT MYSQL_CONNECTOR_CPP_LIBRARY)message(FATAL_ERROR "MySQL Connector/C++ not found! Please install libmysqlcppconn-dev.")
endif()#类似于include_directories
target_include_directories(${PROJECT_NAME} PRIVATE ${MYSQL_CONNECTOR_CPP_INCLUDE_DIR})
# 编译过程分为4个阶段,预处理、编译、汇编、链接。
# 所谓链接,是指将多个目标文件以及所需的库文件合并成最终的可执行文件或库
target_link_libraries(UnderwtConn
PUBLIC visa
Boost::system Boost::thread
Drogon::Drogon
hiredis::hiredis
${MYSQL_CONNECTOR_CPP_LIBRARY}
)if (CMAKE_VERSION VERSION_GREATER 3.12)set_property(TARGET UnderwtConn PROPERTY CXX_STANDARD 20)
endif()# 将配置文件复制到构建目录
add_custom_command(TARGET UnderwtConn POST_BUILDCOMMAND ${CMAKE_COMMAND} -E copy_if_different${CMAKE_CURRENT_SOURCE_DIR}/config/sys.conf${CMAKE_CURRENT_BINARY_DIR}/config/sys.confCOMMENT "Copying config file to build directory"VERBATIM)
3、Dockerfile与CMakeLists.txt
三、构建镜像和创建容器
1、构建镜像命令
构建镜像命令与其他项目没有两样,进入项目所在根目录,因为Dockerfile就在根目录。运行:
sudo docker build -t ackctrl .
2、构建镜像时展示详细信息
如果想观察构建过程中输出的详细信息,可加参数:
sudo docker build -t ackctrl . \
--progress=plain
3、构建镜像时强制重新编译
系统会有缓存,利于下次构建。有时需要强迫它重新编译:
sudo docker build -t ackctrl . \
--no-cache
4、创建容器
创建容器命令众所周知,
sudo docker run --name ackctrl \--restart=always -d -it \-v /home/leftfist/work/docker/ackctrl/config/sys.conf:/ackctrl/config/sys.conf \-p 10992:10992 \ackctrl:latest
5、创建容器并进入容器内部
如果创建的容器有问题,创建之后运行报错,这时它就处于挂起状态,想进入容器内部根本就不可能,完全不知道出了什么问题。可以用以下命令,创建容器后立即进入容器内部:
sudo docker run --name ackctrl \-v /home/leftfist/work/docker/ackctrl/config/sys.conf:/ackctrl/config/sys.conf \-p 10992:10992 \--restart=no -it ackctrl:latest /bin/bash
四、两种运行方式
一般构建镜像,可执行文件都在镜像中。如果程序有改动,只能重新构造镜像,更新稍嫌麻烦。我比较喜欢将可执行文件外挂到宿主机,镜像只是一个批处理命令,用于打开可执行文件。这样更新的话,直接覆盖宿主机上的可执行文件就好了。要实现这点,一是修改Dockerfile,二是将可执行文件外挂。
1、修改Dockerfile
在Dockerfile尾部,修改为:
# 运行程序
#CMD ["./UnderwtConn"]
CMD ["sh", "-c", "./UnderwtConn"]
2、外挂可执行文件
sudo docker run --name ackctrl \--restart=always -d -it \-v /home/leftfist/work/docker/ackctrl/config/sys.conf:/ackctrl/config/sys.conf \-v /home/leftfist/work/docker/ackctrl/UnderwtConn:/ackctrl/UnderwtConn \-p 10992:10992 \ackctrl:latest
五、Dockerfile中的一些命令
1、WORKDIR
# 将工作目录设为tmp
# 此命令有2个作用:1是之后的操作都从这里出发,而是如果没有这个目录的话则创建一个
WORKDIR /tmp
WORKDIR,只会影响容器内工作目录。
六、有关多阶段构建
1、多阶段构建的基本概念
多阶段构建允许你在同一个 Dockerfile 中使用多个 FROM 指令,每个 FROM 指令都会启动一个新的构建阶段。通常的做法是:
第一阶段:构建阶段
用于安装开发工具、编译代码等。
生成最终需要的可执行文件或其他资源。
第二阶段:运行时阶段
使用一个更小的基础镜像(如 debian:stable-slim 或 alpine)。
只复制第一阶段生成的必要文件,而不包含任何不必要的开发工具或中间文件。
通过这种方式,可以显著减小最终镜像的体积,并提高安全性。
2、为什么选择 ubuntu:24.04作为基础镜像?
我的开发环境,生产环境都是ubuntu:24.04。同时也因为:
(1) Ubuntu 提供了丰富的工具链
开发工具丰富:
Ubuntu 是一个功能齐全的 Linux 发行版,默认包含许多开发工具(如 gcc、g++、make、cmake 等),这些工具对于编译代码非常方便。
不需要额外配置复杂的依赖项即可快速开始构建。
包管理器强大:
Ubuntu 使用 apt 包管理器,拥有大量的软件包仓库,可以轻松安装各种开发依赖。
(2) 兼容性好
很多开源项目和框架的构建文档通常基于 Ubuntu 或 Debian 系统编写,因此使用 Ubuntu 可以减少兼容性问题。
ubuntu:24.04 是一个长期支持版本(LTS),稳定性和社区支持非常好。
(3) 开发环境友好
构建阶段通常需要调试、测试和安装大量中间依赖项,Ubuntu 提供了一个更完整的开发环境,适合复杂项目的构建。
七、一些处理
1、网络慢导致构建长问题
由于众所周知的原因,如果构建过程中要下载个什么东西简直就是噩梦,构建时间将长到无法忍受。所以我的做法是预先下载好,放在项目里,构建时现场编译。
# 将工作目录设为tmp
# 此命令有2个作用:1是之后的操作都从这里出发,而是如果没有这个目录的话则创建一个
WORKDIR /tmp# Hiredis
# 复制解压后的 Hiredis 源码到容器中
COPY ./docker/hiredis-1.2.0 /tmp/hiredis# 编译并安装 Hiredis
RUN cd /tmp/hiredis && \mkdir build && cd build && \cmake .. && \make -j$(nproc) && \make install && \ldconfig && \cd / && rm -rf /tmp/hiredis