在这篇文章中,我们将讨论一些使用 Dockerfile 的最佳实践,探索一些注意事项,并使用 Dockerfile 和云原生 Buildpacks 构建应用。你将了解每种工具最擅长的工作是什么,以及如何决定何时使用它们。
Dockerfile 是一个包含命令的文本文件,Docker 将执行这些命令来构建一个容器镜像。Dockerfiles 总是以一个 FROM 指令开始,指定从基本镜像开始。后续命令构建并修改该基本镜像。
让我们通过使用 Dockerfile 构建一个小的“hello world”,一个文件的 Go 应用程序来更好地了解 Dockerfile。你不需要安装 Go 以跟随教程,Docker 会照顾依赖。
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", sayHello)
http.ListenAndServe(":8080", nil)
}
func sayHello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
现在,让我们创建一个简单的 Dockerfile。
FROM golang:1.16.5
WORKDIR /app
COPY main.go .
RUN go build main.go
CMD ./main
为了让我们的容器运行起来,我们需要通过从 docker.com 安装 Docker CLI 来设置 Docker。然后,运行以下命令来构建应用程序。
docker build -t hello .
我们新构建的镜像的大小是 868.3 MB
REPOSITORY TAG IMAGE ID CREATED SIZE
hello latest 005c27e8cd40 7 minutes ago 868.3MB
现在我们可以使用以下命令运行镜像:
docker run -it hello
这是一个很好的开始,但是镜像没有得到优化。
我们开始使用 golang:1.16.5 作为我们的 Go 应用程序的基本镜像。但我们实际上可以从以下两个镜像中选择:
1.16.5 862MB
1.16.5-alpine 302MB
golang:1.16.5-alpine 指定 Go 基准镜像的 Alpine 版本。Alpine 是一个专门为容器设计的微型 Linux 发行版。所以 Docker、Go 和 Alpine 是天生一对!
我们也可以在 Dockerfile 中添加一个 FROM scratch 行,它告诉 Docker 以一个全新的、完全空的容器镜像(这被称为 scratch 容器)重新开始,并将编译后的程序复制到其中。这是我们稍后将继续运行的容器镜像。
使用 scratch 镜像也节省了大量空间,因为我们实际上不需要 Go 工具或其他工具来运行编译后的程序。使用一个容器用于构建,另一个容器用于最终镜像,这称为多阶段构建。
我们更好的 Dockerfile 看起来像这样
FROM golang:1.14-alpine AS build
COPY main.go .
RUN CGO_ENABLED=0 go build -o /bin/demo
FROM scratch
COPY --from=build /bin/demo /bin/demo
ENTRYPOINT ["/bin/demo"]
当我们再次运行 docker 构建后,我们的镜像将会变小,新构建的镜像的大小大约为 8MB。
因为镜像是在构建过程的最后阶段构建的,所以你可以通过利用构建缓存[1]来最小化镜像层。
如果构建包含多个层,则可以将其从更改频率较低的层排序为更改频率较高的层,这确保了构建缓存是可重用的。
遵循以下步骤:
多阶段构建[2]允许你大幅减少最终镜像的大小,而不必费劲地减少中间层和文件的数量。下面是 Dockerfile 的示例。
FROM golang:1.16-alpine AS build
# Install tools required for project
# Run `docker build --no-cache .` to update dependencies
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
# List project dependencies with Gopkg.toml and Gopkg.lock
# These layers are only re-built when Gopkg files are updated
COPY Gopkg.lock Gopkg.toml /go/src/demo/
WORKDIR /go/src/demo/
# Install library dependencies
RUN dep ensure -vendor-only
# Copy the entire project and build it
# This layer is rebuilt when a file changes in the project directory
COPY . /go/src/demo/
RUN go build -o /bin/demo
# This results in a single layer image
FROM scratch
COPY --from=build /bin/demo /bin/demo
ENTRYPOINT ["/bin/demo"]
CMD ["--help"]
然而,Dockerfile 缓存是脆弱的,你必须小心如何编写 Dockerfile。如果,你不需要写呢?
构建包(buildpack)是将源代码转换为可运行的容器镜像的程序。通常,构建包封装了单一语言生态系统工具链。有针对 Ruby、Go、Node.js、Java、Python 等的构建包。
要设置构建包,请遵循Pack CLI 安装说明[3]。让我们使用下面的命令来构建应用程序
pack build hello --builder=paketobuildpacks/builder:tiny
该镜像的大小大约为 30 MB。
pack 使用构建包来帮助你轻松创建可以在任何地方运行的 OCI 镜像。
===> DETECTING
4 of 6 buildpacks participating
google.go.runtime 0.9.1
google.go.gopath 0.9.0
google.go.build 0.9.0
google.utils.label 0.0.1
===> ANALYZING
Previous image with name "go-app" not found
===> RESTORING
===> BUILDING
=== Go - Runtime (google.go.runtime@0.9.1) ===
构建包运行以下一组进程来构建应用程序的镜像。
除了构建镜像,pack 还让你为容器镜像生成一个材料清单。软件物料清单(Software Bill-of-Materials,BOM)提供了必要的信息,以了解容器中是什么以及它是如何构造的。
让我们为使用构建包构建的镜像运行下面的程序。
pack inspect-image your-image-name --bom
对我们的示例 Go 应用程序镜像运行它会得到以下结果。
{
"remote": null,
"local": [
{
"name": "go",
"metadata": {
"version": "1.17.1"
},
"buildpacks": {
"id": "google.go.runtime",
"version": "0.9.1"
}
}
]
}
云原生 Buildpacks 提供了两种形式的材料清单。
构建包为容器镜像创建“可复制的构建(reproducible builds)”。以可复制的方式创建镜像。可复制构建意味着无论何时运行:
pack build hello --builder=paketobuildpacks/builder:tiny
它将产生完全相同的镜像 ID(也称为 sha / digest),假设你有:
让我们为最近构建的容器演示一下
同一个 Go 应用的两个镜像使用相同的构建器镜像和构建包有相同的哈希值。
我们为什么需要它?
镜像 sha 考虑镜像层的内容,包括元数据,例如镜像生成的日期。可复制构建可以作为信任链的一部分;源代码可以被签名,确定性编译可以证明二进制文件是从可信的源代码编译的。
现在,尝试将新镜像部署到你最喜欢的云上,这里有一些文档[4]可以帮助你!
到目前为止,我们已经讨论了云原生 Buildpacks、Dockerfiles 以及使用它们构建的应用程序。对于 Dockerfiles 来说,它们的灵活性使它们熠熠发光。你构建的镜像只受限于你编写 Dockerfile 脚本的能力;你可以安装系统包,允许或限制根访问,从头开始,增加一个现有的镜像,使用任何一个 Docker 的认证镜像,天空是唯一限制!然而,真正的挑战在于同样的灵活性。你的 Dockerfile 将成为你必须维护的另一段代码。随着时间的推移,操作系统或运行时配置可能需要补丁或更新。标准化、维护和构建镜像的自动化完全取决于你。
云原生 Buildpacks 解决了 Dockerfiles 操作上的复杂性,并提供了大规模创建和维护镜像所需的结构,提供了简单的用户体验。从选择和维护基本镜像到为其余层提供内容,提供与镜像大小和分层、缓存和安全性相关的优化,以及特定于给定编程语言的标准和优化,Buildpacks 可以完成所有这些工作。生成的应用程序镜像通过元数据进行了丰富,使其易于检查,你还可以获得详细的软件材料清单(Software Bill of Materials,SBOM),包括运行时版本、应用程序依赖关系和其他细节。
虽然 buildpack 为大多数用例提供了解决方案,但在某些情况下,你可能需要更大的灵活性,例如,如果你正在使用当前的 Buildpacks 生态系统不支持的语言构建应用程序,那么在这种情况下,你可能必须编写自定义的 Buildpacks。在 Buildpacks 不能处理某些需求的情况下,你可能必须创建一个一次性的 Dockerfile。
现在,轮到你探索这些工具并找出最适合你需要的工具了!
Javier Romero 和 Joe Kutner。
[1]
利用构建缓存: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache
[2]
多阶段构建: https://docs.docker.com/develop/develop-images/multistage-build/
[3]
Pack CLI 安装说明: https://github.com/buildpacks/pack#getting-started
[4]
文档: https://docs.docker.com/language/golang/deploy/