欢迎各位码农!在本教程中,我们将研究多阶段 Docker 镜像以及如何使用它们来最小化生产 Go 应用程序所需的容器大小。
在本教程结束时,我们将涵盖以下概念:
- 什么是多阶段 Dockerfile。
- 我们如何为我们的 Go 应用程序构建简单的多阶段 Dockerfile
Docker 是一种强大的容器化技术,可用于轻松启动隔离且可重现的环境,在其中构建和运行我们的应用程序。它越来越受欢迎,越来越多的云服务提供商提供本地 docker 支持,让您可以轻松部署容器化应用程序,让全世界看到!
多阶段 Dockerfile 的需求是什么?
为了了解多阶段 Dockerfile 为何有用,我们将创建一个简单的 Dockerfile,其中一个阶段用于构建和运行我们的应用程序,另一个 Dockerfile 具有构建阶段和生产阶段。
一旦我们创建了这两个不同的 Dockerfile,我们应该能够比较它们,并希望自己看到多阶段 Dockerfile 比它们更简单的对应物更受欢迎!
因此,在我之前的教程中,我们创建了一个非常简单的 Docker 镜像,我们的 Go 应用程序在其中构建和运行。这Dockerfile看起来像这样:
## We specify the base image we need for our
## go application
FROM golang:1.12.0-alpine3.9
## We create an /app directory within our
## image that will hold our application source
## files
RUN mkdir /app
## We copy everything in the root directory
## into our /app directory
ADD . /app
## We specify that we now wish to execute
## any further commands inside our /app
## directory
WORKDIR /app
## we run go build to compile the binary
## executable of our Go program
RUN go build -o main .
## Our start command which kicks off
## our newly created binary executable
CMD ["/app/main"]
有了这个,我们随后可以使用一个非常简单的docker build 命令来构建我们的 Docker 镜像:
$ docker build -t go-simple .
这将创建一个镜像并将其存储在我们的本地 docker 镜像存储库中,最终看起来像这样:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
go-simple latest 761b9dd5f9a4 4 seconds ago 793MB
您应该希望注意到最后一列指出此镜像的大小为 793MB。对于构建和运行一个非常简单的 Go 应用程序的东西来说,这绝对是巨大的。
在这个镜像中将包含编译和运行我们的 Go 应用程序所需的所有包和依赖项。使用多阶段 dockerfile,我们实际上可以通过将内容分成两个不同的阶段来显着减小这些镜像的大小。
一个简单的多阶段 Dockerfile
使用多阶段 Dockerfile,我们可以将构建和运行 Go 应用程序的任务分成不同的阶段。通常,我们从一个大镜像开始,其中包含编译 Go 应用程序的二进制可执行文件所需的所有必要依赖项、包等。这将被归类为我们的builder舞台。
然后,我们为我们的run阶段拍摄一个更轻量级的镜像,其中仅包含运行二进制可执行文件绝对需要的内容。这通常被归类为production阶段或类似的东西。
## We use the larger image which includes
## all of the dependencies that we need to
## compile our program
FROM bigImageWithEverything AS Builder
RUN go build -o main ./...
## We then define a secondary stage which
## is built off a far smaller image which
## has the absolute bare minimum needed to
## run our binary executable application
FROM LightweightImage AS Production
CMD ["./main"]
通过这种方式,我们受益于一致的构建阶段,并且我们受益于我们的应用程序将在生产环境中运行的绝对微小的镜像。
注意 – 在上面 的 psuedo-Dockerfile 中 ,我使用AS关键字为我的 镜像 添加了别名。这可以帮助我们区分 Dockerfile 的不同阶段,我们可以使用该–target 标志来构建特定阶段。
一个真实的例子
现在我们已经介绍了基本概念,让我们看看如何定义一个真正的多阶段 Dockerfile,它将首先编译我们的应用程序,然后在轻量级 Dockeralpine映像中运行我们的应用程序。
出于本教程的目的,我们将从我的新Go WebSockets 教程中窃取代码,因为这演示了下载依赖项,并且是一个不平凡的示例,因此比标准示例更接近 真正的 Go 应用程序hello world。
在项目目录中创建一个名为的新文件main.go并添加以下代码:
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
// We'll need to define an Upgrader
// this will require a Read and Write buffer size
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// define a reader which will listen for
// new messages being sent to our WebSocket
// endpoint
func reader(conn *websocket.Conn) {
for {
// read in a message
messageType, p, err := conn.ReadMessage()
if err != nil {
log.Println(err)
return
}
// print out that message for clarity
fmt.Println(string(p))
if err := conn.WriteMessage(messageType, p); err != nil {
log.Println(err)
return
}
}
}
func homePage(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Home Page")
}
func wsEndpoint(w http.ResponseWriter, r *http.Request) {
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
fmt.Println(r.Host)
// upgrade this connection to a WebSocket
// connection
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
}
// listen indefinitely for new messages coming
// through on our WebSocket connection
reader(ws)
}
func setupRoutes() {
http.HandleFunc("/", homePage)
http.HandleFunc("/ws", wsEndpoint)
}
func main() {
fmt.Println("Hello World")
setupRoutes()
log.Fatal(http.ListenAndServe(":8080", nil))
}
注意 – 我已经使用go mod init命令初始化了这个项目以使用 go 模块。这可以使用 Go 版本 1.11 并通过调用在 docker 容器外部本地运行 go run ./…
接下来,我们将Dockerfile在与main.go上面的文件相同的目录中创建一个。这将具有一个builder阶段和一个production阶段,该阶段将由两个不同的基础镜像构建:
## We'll choose the incredibly lightweight
## Go alpine image to work with
FROM golang:1.11.1 AS builder
## We create an /app directory in which
## we'll put all of our project code
RUN mkdir /app
ADD . /app
WORKDIR /app
## We want to build our application's binary executable
RUN CGO_ENABLED=0 GOOS=linux go build -o main ./...
## the lightweight scratch image we'll
## run our application within
FROM alpine:latest AS production
## We have to copy the output from our
## builder stage to our production stage
COPY --from=builder /app .
## we can then kick off our newly compiled
## binary exectuable!!
CMD ["./main"]
现在我们已经定义了这个多阶段 Dockerfile,我们可以使用标准docker build命令继续构建它:
$ docker build -t go-multi-stage .
现在,当我们将简单镜像的大小与多阶段镜像进行比较时,我们应该会看到大小的显着差异。我们之前的go-simple镜像大小约为 800MB,而这个多阶段镜像的大小约为 1/80。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
go-multi-stage latest 12dd51472827 24 seconds ago 12.3MB
如果我们想尝试运行它来验证它是否正常工作,我们可以使用以下docker run命令来实现:
$ docker run -d -p 8080:8080 go-multi-stage
这将启动以-d分离模式运行的 docker 容器,我们应该能够在浏览器中打开并看到 Go 应用程序将 Hello World消息返回给我们!
练习 -index.html从Go WebSockets 教程中复制并在浏览器中打开它,您应该会看到它连接到我们的容器化 Go 应用程序,并且您应该能够使用该docker logs命令查看日志 。
结论
总结一下,在本教程中,我们研究了如何定义一个非常简单的 Dockerfile 来创建一个沉重的 Docker 映像。然后,我们研究了如何通过使用 multi-stage 来优化这一点,Dockerfile这给我们留下了令人难以置信的轻量级镜像。
如果您喜欢本教程,或者您有任何意见/反馈/建议,那么我很乐意在下面的建议框中听到它们!