如何创建一个小的容器镜像

如何创建一个小的容器镜像

对 golang 程序上线到了容器,在使用 scratch 的时候遇到了一个小小的问题,顺手对编译做了一些整理. 以下:

编写一个简单的Go应用程序,例如 gin 的例子 **main.go**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

例子1: Dockerfile 编译

使用容器来编译运行程序

1
2
3
4
5
6
7
# 使用官方的 Golang 镜像作为基础镜像
FROM golang:1.20
WORKDIR /app
COPY . .
RUN go build -o main
EXPOSE 8080
CMD ["./main"]

通过构建的到了容器大小为 1.16GB

1
docker-golang-build-example1              latest               cd34e318087d   40 seconds ago   1.16GB

这个时候会容器的内容又一些大了. 可以看看 https://hub.docker.com/_/golang

golang:1.20 是基于 Debian 来作为基础镜像,本身就很大.

例子2: 使用 apline

我们可以看到提供了 alpine 作为基础镜像的包 golang:<version>-alpine ,只需要将 FROM 后面的镜像加上 alpine 即可

1
2
3
4
5
6
7
# 使用官方的 Golang alpine 镜像作为基础镜像
FROM golang:1.20-alpine
WORKDIR /app
COPY . .
RUN go build -o main
EXPOSE 8080
CMD ["./main"]

Untitled.png

使用 alpine 作为基础镜像会存在一些问题, 它使用了 musl libc 来代替 glibc ,小的镜像和稳定性的讨论会有一些不同的意见: https://news.ycombinator.com/item?id=10782897

镜像的大小从 1.16G 变成了 570M

例子3: 分段编译,使用 busybox

在编译过程中, go build 会拉取依赖,使用分段将编译和运行的镜像分开. golang 在运行时不需要编译环境,这里我直接使用 busybox 来作为基础镜像,同时例子2中使用 alpine 会存在一些问题,所以我们使用 busybox 的 glibc 的包.

1
2
3
4
5
6
7
8
9
10
11
FROM golang:1.20 AS build
WORKDIR /app
COPY . .
RUN go build -o main

# 最终阶段
FROM busybox:glibc
WORKDIR /app
COPY --from=build /app/main /app
EXPOSE 8080
CMD ["./main"]

Untitled.png

编译后得到了 15.5M 的镜像.

例子4: 静态编译,使用 scratch

在容器镜像中有一个 0M 的基础镜像 scratch ,它是所以镜像的基础.

容器本质上是基于Linux内核的Namespace、Cgroups和Union FS等技术对进程进行封装隔离的操作系统层面的虚拟化技术。

我们可以通过静态编译的方式,基于 scratch 来运行. 构建Go应用程序,并确保它是静态链接的。可以使用以下命令来构建:

1
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app main.go

在编译中添加了 -a -installsuffix cgo 两个条件,并指定了CGO_ENABLED=0:

-a : 强制重新构建
-installsuffix cgo: 指示编译器将生成的目标文件和包安装到一个特定的目录中,以便将与C语言绑定的包(使用**cgo**工具)与其他Go包区分开。
CGO_ENABLED=0 : cgo 工具标识为禁用

对 例子三 中的 dockerfile 做编译做调整,这个Dockerfile分为两个阶段:构建阶段和最终阶段。在构建阶段,它使用了官方的**golang镜像来构建Go应用程序,然后在最终阶段使用了scratch**基础镜像来创建最终的容器。

1
2
3
4
5
6
7
8
9
10
# 构建阶段
FROM golang:1.20 AS build
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app

# 最终阶段
FROM scratch
COPY --from=build /app/app /app
CMD ["/app"]

Untitled.png

编译后 docker-golang-build-example4 的大小从 15.5M 西江到了 11.2M, 这一点点的差值几乎就是 busybox 的存储占用.

要注意的问题

使用 scratch 镜像是一个空镜像,所以需要特别注意一些依赖和系统调用.

涉及到依赖时,需要解依赖,比如证书依赖:

COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs/

涉及到系统调用,无法使用 scratch ,就需要考虑 apline 的方案.

例子5 加快编译速度

当把运行镜像的大小降下来后,还存在另一个问题,运行 go build 的时候都需要重新拉一遍golang 的环境. 可以通过挂在缓存的位置来减少应用程序编译时的时间.比如 golang 的 pkg目录 $GOPATH/pkg .在编译前提前做好 pkg 缓存,将缓存复制到目录中.

假设已经准备好了缓存目录 pkg. 在 例子4 中添加 COPY pkg/* /go/pkg/ 来加快编译速度.

1
2
3
4
5
6
7
8
9
10
11
# 构建阶段
FROM golang:1.20 AS build
WORKDIR /app
COPY pkg/* /go/pkg/
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app

# 最终阶段
FROM scratch
COPY --from=build /app/app /app
CMD ["/app"]

这个例子可能对 golang 的意义不是特别大, 对一些依赖比较多的 node 程序的 node_modele 目录缓存就很有用.

总结

通过四种方式,不断的降低了容器镜像的大小. 但是四种方式并没有特别的优劣之分,在不同的场景下,可以通过不同的策略来做镜像编译的方式.

  • 使用更小的镜像 apline 、busybox、scratch 来降低镜像大小
  • 使用分阶段构建来减少
  • 使用缓存来加快编译

在没有多阶段编译的时期,还有将多条命令写成一条的方式来降低容器的层数,或者借助一些工具来合并层.

以上例子放在了 github ,可以使用 docker-compose 来验证

docker-golang-build-example

docker build alpine dns error

使用 alpine 构建 Ddockerfile ,导致 docker 打包失败,错误如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fetch https://mirrors.aliyun.com/alpine/v3.13/main/x86_64/APKINDEX.tar.gz
fetch https://mirrors.aliyun.com/alpine/v3.13/community/x86_64/APKINDEX.tar.gz
v3.13.4-69-g5bcff43ec5 [https://mirrors.aliyun.com/alpine/v3.13/main]
v3.13.4-66-g32aee0eba0 [https://mirrors.aliyun.com/alpine/v3.13/community]
OK: 13892 distinct packages available
fetch https://mirrors.aliyun.com/alpine/v3.13/main/x86_64/APKINDEX.tar.gz
WARNING: Ignoring https://mirrors.aliyun.com/alpine/v3.13/main: DNS lookup error
fetch https://mirrors.aliyun.com/alpine/v3.13/community/x86_64/APKINDEX.tar.gz
WARNING: Ignoring https://mirrors.aliyun.com/alpine/v3.13/community: DNS lookup error
ERROR: unable to select packages:
bash (no such package):
required by: world[bash]
curl (no such package):
required by: world[curl]
ERROR: Service 'microservice-users-rpc' failed to build : The command '/bin/sh -c apk update && apk add --no-cache curl bash' returned a non-zero code: 2
make: *** [docker-compose-up] Error 1

阅读更多

docker_sysctl

Sysctl 用于配置运行时的内核参数,正常情况下修改 /etc/sysctl.conf . 然后执行 sysctl -p 就能够设置成功。

当使用 Docker 启动服务时,使用 sysctl -p 来配置 docker 的参数时会出现问题。比如

1
2
3
sysctl: cannot stat /proc/sys/net/core/rmem_max: No such file or directory
sysctl: cannot stat /proc/sys/net/core/wmem_max: No such file or directory

阅读更多

Dockerfile编写实践

Docker 镜像是由 Layers 组成,Dockerfile 中每一条指令都会创建一个层,层数最多 127 层。

  1. 选择更小的基础镜像通常我们使用的镜像有 Ubuntu 、CentOs、 debian 、Alpine 。其中推荐使用 Alpine ,Alpine 的基础镜像只有 4.4M 左右,1.1 scratch 镜像​ scratch 镜像是空镜像。如果要运行一个包含所有依赖的二进制文件,可以直接使用 scratch 作为基础镜像。1.2 busybox 镜像​ scratch是个空镜像,如果希望镜像里可以包含一些常用的Linux工具,busybox镜像是个不错选择,镜像本身只有1.16M,非常便于构建小镜像。
阅读更多

Docker配置TLS认证开启远程访问

默认情况下,Docker 通过监听本地的 unix socket 运行,同时还可以通过 TCP 进行通信,方便对 Docker 集群 管理。Docker 官方提供了通过 TLS 加密,来保证只有信任的客户端才能远程访问 Docker 服务。

采用私有 CA 签名证书。客户端只能够连接到该 CA 签名的证书和服务器。

阅读更多

docker build openwrt 遇到的小问题

在使用 docker 编译 openwrt 遇到了两个坑,问题不复杂,却折腾了一天。

环境如下

  • Dockerfile 定义编译 openwrt 的编译环境
  • docker-compose.yml 定义变化 openwrt 的运行配置,主要是将代码目录 data 使用 -v 映射到容器中
  • /data 代码存放目录
阅读更多

docker-compose 中的 external-links

在使用服务时使用同一台服务器的 mysql 的 docker 服务。想到 external-links 这个配置。

按照文档的配置添加了如下配置

1
2
3
external_links:
- mysql_mysql_1:mysql

但是配置无论如何都不成功。

https://docs.docker.com/compose/compose-file/#external_links

官方文档相关的描述,

If you’re using the version 2 or above file format, the externally-created containers must be connected to at least one of the same networks as the service that is linking to them.

也就是说想要使用 external_links 是需要两个服务在同一个网络段,这里只提了一句,对第一次写配置文件的开发者非常的不友好。

阅读更多

minikube

​​

Minikube

  1. 安装minikube阿里修改版的

阅读更多

Jenkins-docker-ansible自动化上线

从11月开始到现在,项目开始从 php 转换到 nodejs + 后端服务,相应的部署环境转换到docker。

在使用纯php环境,上线比较简单,主要是代码拉取,拷贝和软链切换。 运行环境使用docker后相应的需要引入一个打包的过程,所以前面所依靠的上线部署系统无法满足当前的需求。

目前来说,Jenkins 作为自动化的流水线工具,几乎是不二的选择。

将 Jenkins 作为流水线工具,在打包阶段依赖 docker-compose。 上线阶段依赖 ansible 。

阅读更多