使用 Docker 构建已编译的应用程序

Building a compiled application with Docker

我正在构建一个用 C++ 编写的服务器,并希望使用 Docker 和 docker-compose 来部署它。 "right way"是做什么的呢?我应该从 Docker 文件调用 make 还是手动构建,上传到某个服务器,然后从 Docker 文件调用 COPY 二进制文件?

更新

对于 2017 年之后访问此问题的任何人,请参阅 by fuglede about using multi-stage Docker builds,这确实是比我 2015 年的答案(下方)更好的解决方案,远早于该答案可用。


旧答案

我的做法是 运行 在容器外构建,只将构建的输出(二进制文件和任何必要的库)复制到容器中。然后您可以将您的容器上传到容器注册表(例如,使用托管的或 运行 您自己的),然后从该注册表拉到您的生产机器上。因此,流程可能如下所示:

  1. 构建二进制文件
  2. test / sanity-check binary 本身
  3. 使用二进制构建容器镜像
  4. 测试 / sanity-check 容器映像 与二进制
  5. 上传到容器注册表
  6. 部署到 staging/test/qa,从注册表中提取
  7. 部署到产品,从注册表中提取

由于在生产部署之前进行测试很重要,因此您希望测试与将在生产中部署的完全相同的东西,因此您不想以任何方式提取或修改 Docker 图像建成后。

我不会 运行 构建 inside 您计划在产品中部署的容器,因为那样您的容器将具有各种额外的工件(例如临时生成生产中不需要的输出、工具等),并使用不会用于部署的内容不必要地扩展容器映像。

我的建议是完全在容器本身上开发、构建和测试。这保证了Docker开发者环境与生产环境相同的理念,见The Modern Developer Workstation on MacOS with Docker.

尤其是 C++ 应用程序,通常与共享 libraries/object 文件存在依赖关系。

我认为 Docker 上还没有用于开发、测试和部署 C++ 应用程序的标准化开发流程。

为了回答您的问题,我们目前的做法是,将容器视为您的开发环境,并在团队中实施一系列实践,例如:

  1. 我们的代码库(配置文件除外)始终位于共享卷(在本地计算机上)(版本在 Git 上)
  2. Shared/dependent 库、二进制文件等。始终 存在于容器中
  3. 在容器中构建和测试,在提交映像之前,清除不需要的对象文件、库等,并确保 docker diff 更改符合预期
  4. Changes/updates 环境,包括共享库、依赖项,始终记录在案并与团队沟通。

我在使用 docker-compose 进行自动化构建时遇到了困难,最后我对所有内容都使用了 docker build

三层建筑

Run → develop → build

然后我将构建输出复制到 'deploy' 图像中:

Run → deploy

可玩的四层:

运行
  • 包含申请 运行 所需的任何包 - 例如libsqlite3-0
开发
  • FROM <projname>:run
  • 包含构建所需的包
    • 例如g++、cmake、libsqlite3-dev
  • Dockerfile 执行任何外部构建
    • 例如构建 boost-python3 的步骤(不在包管理器存储库中)
建造
  • FROM <projname>:develop
  • 包含来源
  • Dockerfile 执行内部构建(经常更改的代码)
  • 从该映像中复制构建的二进制文件以用于部署
部署
  • FROM <projname>:run
  • 将构建的输出复制到映像中并安装
  • RUNENTRYPOINT 用于启动应用程序

文件夹结构如下所示:

.
├── run
│   └── Dockerfile
├── develop
│   └── Dockerfile
├── build
│   ├── Dockerfile
│   └── removeOldImages.sh
└── deploy
    ├── Dockerfile
    └── pushImage.sh

设置构建服务器意味着执行:

docker build -f run -t <projName>:run
docker build -f develop -t <projName>:develop

每次我们构建时,都会发生这种情况:

# Execute the build
docker build -f build -t <projName>:build

# Install build outputs
docker build -f deploy -t <projName>:version

# If successful, push deploy image to dockerhub
docker tag <projName>:<version> <projName>:latest
docker push <projName>:<version>
docker push <projName>:latest

我建议人们将 Dockerfile 作为有关如何 build/run/install 项目的文档。

如果构建失败并且输出不足以进行调查,我可以 运行 /bin/bash<projname>:build 中四处寻找问题所在。


我围绕这个想法 a GitHub repository 整理了起来。它适用于 C++,但您可以将它用于任何用途。


我还没有探索这个功能,但@TaylorEdmiston 指出我这里的模式与 multi-stage builds 非常相似,我在想出这个的时候并不知道。它看起来像是一种更优雅(并且有更好的记录)的方式来实现同样的事情。

虽然其他答案中提出了解决方案——尤其是 Misha Brukman 在对 about using one Dockerfile for development and one for production -- would be considered idiomatic at the time the question was written, it should be noted that the problems they are trying to solve -- and in particular the issue of cleaning up the build environment to reduce image size while still being able to use the same container environment in development and production -- have effectively been solved by multi-stage builds 的评论中提出的建议,这些建议在 Docker 17.05 中引入。

这里的想法是将 Docker 文件分成两部分,一部分基于您最喜欢的开发环境,例如与创建二进制文件有关的成熟的 Debian 基础映像你想在一天结束时部署的,另一个只是在最小的环境中运行构建的二进制文件,比如 Alpine。

通过这种方式,您可以避免 blueskin 在其中一条评论中提到的开发环境和生产环境之间可能存在的差异,同时仍然确保您的生产映像不会被开发工具污染。

文档提供了以下多阶段构建 Go 应用程序的示例,然后您可以将其采用到 C++ 开发环境(一个问题是 Alpine 使用 musl,因此您必须在您的开发环境中链接时要小心)。

FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]