本文演示了如何构建一个最小化的容器镜像(当然该镜像还得能做点有用的事:))。通过利用多段构建(multistage builds)、scratch基础镜像和基于汇编(assembly)的http服务器程序, 我们能够将其尺寸降低到6.32kB!
膨胀的容器
容器经常被奉为解决所有与软件运维相关难题的灵丹妙药。虽然我喜欢容器,但我经常会碰到有各种问题的野路子镜像。一个常见的问题是镜像尺寸,有时一个镜像可能会达到数GB!
因此,我决定挑战一下自己,构建一个尽可能小的容器镜像。
挑战
规则很简单:
- 容器应在特定端口上通过http提供对文件内容的访问
- 不允许卷安装
最朴素的解决方案
作为对照,我们先用node.js创建一个简单的服务器 index.js:
const fs = require("fs");
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/html' })
fs.createReadStream('index.html').pipe(res)
})
server.listen(port, hostname, () => {
console.log(`Server: http://0.0.0.0:8080/`);
});
并把它放到一个镜像里(该容器使用nodejs官方镜像):
FROM node:14
COPY . .
CMD ["node", "index.js"]
这个镜像构建后的尺寸为943MB!
更小的基础镜像
为了减小目标镜像的尺寸,最简单的策略就是使用较小的基本镜像。nodejs的官方镜像有一个叫slim的变体(仍基于debian,但预安装的依赖项较少)和一个叫做alpine的基于Alpine Linux的变体。
使用node:14-slim和node:14-alpine作为基本镜像,会使最终的镜像尺寸分别减小到167MB和116MB。
由于docker镜像的分层叠加特性,新增的每一层都要构建在原有的基础镜像上,所以也就没什么更好的能减小nodejs镜像尺寸的办法了。
编译语言
为了更进一步,我们可以使用运行时依赖项更少的编译语言。当然有多种选择,但是对于构建Web服务,golang是一个比较"流行"的选择。
为此我创建了一个最基础的的文件服务器server.go,如下:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
fileServer := http.FileServer(http.Dir("./"))
http.Handle("/", fileServer)
fmt.Printf("Starting server at port 8080\n")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
并使用golang官方的基础镜像做构建:
FROM golang:1.14
COPY . .
RUN go build -o server .
CMD ["./server"]
最后的尺寸是 818MB。😡
这里的问题是,golang的基础镜像里安装了许多依赖项,这些依赖在构建golang程序时很有用,但运行时一般都不需要。
多段构建
Docker有一个被称为 多段构建 (Multi-stage Builds) 的特性,可以在安装了所有必要依赖项的环境中构建代码,然后将生成的可执行文件复制到其他镜像中去。
这个特性往往很有用,最主要原因之一是可以大幅度的减小镜像尺寸!
通过如下方式重构Dockerfile:
### build stage ###
FROM golang:1.14-alpine AS builder
COPY . .
RUN go build -o server .
### run stage ###
FROM alpine:3.12
COPY --from=builder /go/server ./server
COPY index.html index.html
CMD ["./server"]
镜像尺寸一下子变成了13.2MB!🙂
静态编译 + scratch镜像
13MB还算不错,但是我们仍然可以通过一些方法让它变得更小。
有一个叫做 scratch 的基本镜像,该镜像不包含任何内容,大小为零。所以使用它构建的任何镜像都必须自带所有必需的依赖项。
为了让我们的go服务器做到这一点,需要在编译过程中添加一些标志,以确保必要的库能以静态链接的方式被编译到可执行文件中:
### build stage ###
FROM golang:1.14 as builder
COPY . .
RUN go build \
-ldflags "-linkmode external -extldflags -static" \
-a server.go
### run stage ###
FROM scratch
COPY --from=builder /go/server ./server
COPY index.html index.html
CMD ["./server"]
具体来说,我们将链接模式设置为external,并将-static标志传递给了外部链接器。
这两个更改使镜像大小缩减到8.65MB!😀
ASM为赢!
用Go之类的语言编写的小于10MB的镜像几乎已经到达极限了... 但其实我们还可以继续缩小尺寸!
Github用户 nemasu 在github上用汇编形式编写了一个全功能http服务器 assmttpd。
对其进行容器化,需要做的就是在make release之前将一些构建所需的依赖项安装到ubuntu基础镜像中去:
### build stage ###
FROM ubuntu:18.04 as builder
RUN apt update
RUN apt install -y make yasm as31 nasm binutils
COPY . .
RUN make release
### run stage ###
FROM scratch
COPY --from=builder /asmttpd /asmttpd
COPY /web_root/index.html /web_root/index.html
CMD ["/asmttpd", "/web_root", "8080"]
将生成的可执行文件asmttpd复制到scratch映像中,并通过CMD来调用。
这样产生的镜像大小仅为6.34kB!🥳
下图为容器镜像列表,可供参考。
从最初的943MB的nodejs镜像一直到这个微小的6.34kB 汇编镜像,希望您喜欢这个过程。
通过对上述过程的理解,希望您也能对自己的容器镜像做优化,让它们变得更小。