├── README.md └── eBook ├── 01.md ├── 02.md ├── 03.md ├── 04.md ├── directory.md ├── images ├── image-overlayfs-demo.png └── upper-lower-dir-768x371.png └── preface.md /README.md: -------------------------------------------------------------------------------- 1 | # 如何构建体积小的容器镜像 2 | 3 | ## 介绍 4 | 5 | 很多人不了解 overlayfs 和它的 diff 机制,以及 Dockerfile 、rootfs 和依赖安装等相关经验积累,导致构建的容器镜像体积会很大,而市面上看到的一些文章都没有从这两方面完整梳理。决定来分享下个人这块的经验积累。 6 | 7 | ## 喜欢本分享 8 | 9 | 10 | ## 开始阅读 11 | 12 | - 您可以选择以下方式阅读本书: 13 | - [GitHub在线](./eBook/preface.md) 14 | 15 | 想读书的人,不会找不到 [目录](eBook/directory.md) :) 16 | -------------------------------------------------------------------------------- /eBook/01.md: -------------------------------------------------------------------------------- 1 | # docker 的 storage driver 2 | 3 | ## 为什么选择 overlay2 4 | 5 | 容器就是隔离的进程,而运行一个进程需要 `linux 内核` + `rootfs` + `相关文件`,内核是使用宿主机的,非内核部分就是容器镜像,docker 需要存储的就是 **容器镜像** 和 **容器层**(容器是单独的一层读写层),从单个容器镜像来讲,容器镜像包含的内容可能性大体分为以下几类: 6 | - rootfs + bin 7 | - rootfs + runtime + script/file 8 | - rootfs + package + bin 9 | - rootfs + package + runtime + script/file 10 | - bin,例如 c 语言静态编译的 k8s pause 镜像,或者静态编译的 go 二进制 11 | 12 | 而且实际使用中,主要的不同是后面的部分,例如 rootfs 都是 debian ,后面的部分有差异性,如果每个镜像都单独存一份,会造成空间浪费没有复用。docker 这方面有很多存储驱动实现,支持的 storage driver 有以下: 13 | - btrfs 14 | - devicemapper 15 | - ~~overlay~~ 16 | - overlay2 17 | - zfs 18 | - vfs 19 | - ~~AUFS~~ 20 | - ... 21 | 22 | AUFS 和 overlay 都废弃了,被 overlay2 替代了,devicemapper 从接触角度来讲 bug 挺多的不推荐使用,btrfs 和 zfs 也支持写时复制,前者需要系统支持,后者是新版本内核特性且存在 bug,overlay2 目前是最广泛使用和最稳定的,而且性能也不错,而 vfs 是兜底的存储,在都不支持的时候可以考虑它,它的缺点是无法层复用导致占据的容量会多很多。 23 | 24 | ![](images/image-overlayfs-demo.png) 25 | 26 | 图里是一个简单例子,两个 docker 镜像各自看都有三层,实际并不是占用 6 层大小,而是相同的两层 + 各自不同的一层大小。 27 | 28 | ## 镜像存储结构 29 | 30 | 下面是一个干净机器上安装 docker 后拉取 alpine 的目录变化: 31 | 32 | ```shell 33 | $ docker info --format '{{.Driver}}' 34 | overlay2 35 | $ cd /var/lib/docker/;ll overlay2/ 36 | total 0 37 | brw------- 1 root root 8, 16 May 30 14:23 backingFsBlockDev 38 | drwx------ 2 root root 6 May 30 14:23 l 39 | $ docker pull alpine 40 | $ ll overlay2/ 41 | total 0 42 | drwx--x--- 3 root root 30 May 30 14:24 6f40b16cd210a5aaa4415b086da6b6e138d0f7ca24089dcd88d5ad3bd02a988b 43 | brw------- 1 root root 8, 16 May 30 14:23 backingFsBlockDev 44 | drwx------ 2 root root 40 May 30 14:24 l 45 | $ ll overlay2/6f40b16cd210a5aaa4415b086da6b6e138d0f7ca24089dcd88d5ad3bd02a988b/ 46 | total 4 47 | drwxr-xr-x 19 root root 199 May 30 14:24 diff 48 | -rw-r--r-- 1 root root 26 May 30 14:24 link 49 | $ ll overlay2/6f40b16cd210a5aaa4415b086da6b6e138d0f7ca24089dcd88d5ad3bd02a988b/diff/ 50 | total 12 51 | drwxr-xr-x 2 root root 4096 May 22 17:47 bin 52 | drwxr-xr-x 2 root root 6 May 22 17:47 dev 53 | drwxr-xr-x 17 root root 4096 May 22 17:47 etc 54 | drwxr-xr-x 2 root root 6 May 22 17:47 home 55 | drwxr-xr-x 6 root root 230 May 22 17:47 lib 56 | drwxr-xr-x 5 root root 44 May 22 17:47 media 57 | drwxr-xr-x 2 root root 6 May 22 17:47 mnt 58 | drwxr-xr-x 2 root root 6 May 22 17:47 opt 59 | dr-xr-xr-x 2 root root 6 May 22 17:47 proc 60 | drwx------ 2 root root 6 May 22 17:47 root 61 | drwxr-xr-x 2 root root 6 May 22 17:47 run 62 | drwxr-xr-x 2 root root 4096 May 22 17:47 sbin 63 | drwxr-xr-x 2 root root 6 May 22 17:47 srv 64 | drwxr-xr-x 2 root root 6 May 22 17:47 sys 65 | drwxrwxrwt 2 root root 6 May 22 17:47 tmp 66 | drwxr-xr-x 7 root root 66 May 22 17:47 usr 67 | drwxr-xr-x 12 root root 137 May 22 17:47 var 68 | $ cat overlay2/6f40b16cd210a5aaa4415b086da6b6e138d0f7ca24089dcd88d5ad3bd02a988b/diff/etc/os-release 69 | NAME="Alpine Linux" 70 | ID=alpine 71 | VERSION_ID=3.20.0 72 | PRETTY_NAME="Alpine Linux v3.20" 73 | HOME_URL="https://alpinelinux.org/" 74 | BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues" 75 | ``` 76 | 77 | alpine ubuntu 和 debian 很多基础镜像的 Dockerfile 都是从空镜像构建的,类似下面这样: 78 | 79 | ```Dockerfile 80 | FROM scratch 81 | ADD xx-base-rootfs.tar.gz / 82 | CMD ["/bin/bash"] 83 | ``` 84 | 85 | **为了便于阅读,下面的 overlay2 的长目录名字会缩写** ,然后构建图片左侧镜像: 86 | 87 | ```shell 88 | $ cat > Dockerfile << EOF 89 | FROM alpine 90 | RUN apk add --no-cache ca-certificates 91 | RUN apk add --no-cache nginx 92 | EOF 93 | $ docker build . -t nginx 94 | $ ls -al overlay2/ 95 | total 0 96 | drwx--x--- 6 root root 256 May 30 16:26 . 97 | drwx--x--- 13 root root 189 May 30 14:23 .. 98 | drwx--x--- 4 root root 55 May 30 16:26 33741b8629298eac70033bd71845baf11e0d2f78093d5340c8ee762241bf5e23 99 | drwx--x--- 3 root root 47 May 30 16:26 6f40b16cd210a5aaa4415b086da6b6e138d0f7ca24089dcd88d5ad3bd02a988b 100 | drwx--x--- 4 root root 72 May 30 16:26 b2c19bb33e1cfe439bd7319cb07cf90b02f9f557c6d48eac12adb17fe345e1e4 101 | brw------- 1 root root 8, 16 May 30 14:23 backingFsBlockDev 102 | drwx------ 2 root root 108 May 30 16:26 l 103 | $ find overlay2/b2c19bb33.../diff/ -type d 104 | overlay2/b2c19bb33.../diff/ 105 | overlay2/b2c19bb33.../diff/etc 106 | overlay2/b2c19bb33.../diff/etc/apk 107 | overlay2/b2c19bb33.../diff/etc/apk/protected_paths.d 108 | overlay2/b2c19bb33.../diff/etc/ca-certificates 109 | overlay2/b2c19bb33.../diff/etc/ca-certificates/update.d 110 | overlay2/b2c19bb33.../diff/etc/ssl 111 | overlay2/b2c19bb33.../diff/etc/ssl/certs 112 | overlay2/b2c19bb33.../diff/lib 113 | overlay2/b2c19bb33.../diff/lib/apk 114 | overlay2/b2c19bb33.../diff/lib/apk/db 115 | overlay2/b2c19bb33.../diff/lib/apk/exec 116 | overlay2/b2c19bb33.../diff/usr 117 | overlay2/b2c19bb33.../diff/usr/bin 118 | overlay2/b2c19bb33.../diff/usr/local 119 | overlay2/b2c19bb33.../diff/usr/local/share 120 | overlay2/b2c19bb33.../diff/usr/local/share/ca-certificates 121 | overlay2/b2c19bb33.../diff/usr/sbin 122 | overlay2/b2c19bb33.../diff/usr/share 123 | overlay2/b2c19bb33.../diff/usr/share/ca-certificates 124 | overlay2/b2c19bb33.../diff/usr/share/ca-certificates/mozilla 125 | $ find overlay2/33741b86.../diff/ -type d 126 | overlay2/33741b86.../diff/ 127 | overlay2/33741b86.../diff/etc 128 | overlay2/33741b86.../diff/etc/apk 129 | overlay2/33741b86.../diff/etc/logrotate.d 130 | overlay2/33741b86.../diff/etc/nginx 131 | overlay2/33741b86.../diff/etc/nginx/http.d 132 | overlay2/33741b86.../diff/etc/nginx/modules 133 | overlay2/33741b86.../diff/lib 134 | overlay2/33741b86.../diff/lib/apk 135 | overlay2/33741b86.../diff/lib/apk/db 136 | overlay2/33741b86.../diff/lib/apk/exec 137 | overlay2/33741b86.../diff/run 138 | overlay2/33741b86.../diff/run/nginx 139 | overlay2/33741b86.../diff/usr 140 | overlay2/33741b86.../diff/usr/lib 141 | overlay2/33741b86.../diff/usr/lib/nginx 142 | overlay2/33741b86.../diff/usr/lib/nginx/modules 143 | overlay2/33741b86.../diff/usr/sbin 144 | overlay2/33741b86.../diff/usr/share 145 | overlay2/33741b86.../diff/usr/share/nginx 146 | overlay2/33741b86.../diff/var 147 | overlay2/33741b86.../diff/var/lib 148 | overlay2/33741b86.../diff/var/lib/nginx 149 | overlay2/33741b86.../diff/var/lib/nginx/html 150 | overlay2/33741b86.../diff/var/lib/nginx/tmp 151 | overlay2/33741b86.../diff/var/log 152 | overlay2/33741b86.../diff/var/log/nginx 153 | overlay2/33741b86.../diff/var/www 154 | overlay2/33741b86.../diff/var/www/localhost 155 | overlay2/33741b86.../diff/var/www/localhost/htdocs 156 | ``` 157 | 158 | 然后构建右侧的 python 镜像: 159 | 160 | ```shell 161 | $ cat > Dockerfile << EOF 162 | FROM alpine 163 | RUN apk add --no-cache ca-certificates 164 | RUN apk add --no-cache python3 165 | EOF 166 | $ docker build . -t python 167 | $ ls -al overlay2/ 168 | total 0 169 | drwx--x--- 7 root root 328 May 30 16:38 . 170 | drwx--x--- 13 root root 189 May 30 14:23 .. 171 | drwx--x--- 4 root root 55 May 30 16:26 33741b8629298eac70033bd71845baf11e0d2f78093d5340c8ee762241bf5e23 172 | drwx--x--- 3 root root 47 May 30 16:26 6f40b16cd210a5aaa4415b086da6b6e138d0f7ca24089dcd88d5ad3bd02a988b 173 | drwx--x--- 4 root root 55 May 30 16:38 8f6ad7d43844e11e411fe6b2b635299b1213eccc4269a5ad825880c80528444a 174 | drwx--x--- 4 root root 72 May 30 16:38 b2c19bb33e1cfe439bd7319cb07cf90b02f9f557c6d48eac12adb17fe345e1e4 175 | brw------- 1 root root 8, 16 May 30 14:23 backingFsBlockDev 176 | drwx------ 2 root root 142 May 30 16:38 l 177 | # 增加了一个 8f6 目录 178 | $ find overlay2/8f6ad7d4384.../diff -type d 179 | ... 180 | overlay2/8f6ad7d4384.../diff/lib 181 | overlay2/8f6ad7d4384.../diff/lib/apk 182 | overlay2/8f6ad7d4384.../diff/lib/apk/db 183 | overlay2/8f6ad7d4384.../diff/lib/apk/exec 184 | overlay2/8f6ad7d4384.../diff/usr 185 | overlay2/8f6ad7d4384.../diff/usr/bin 186 | overlay2/8f6ad7d4384.../diff/usr/include 187 | overlay2/8f6ad7d4384.../diff/usr/include/python3.12 188 | overlay2/8f6ad7d4384.../diff/usr/lib 189 | overlay2/8f6ad7d4384.../diff/usr/lib/python3.12 190 | overlay2/8f6ad7d4384.../diff/usr/lib/python3.12/__phello__ 191 | overlay2/8f6ad7d4384.../diff/usr/lib/python3.12/__phello__/__pycache__ 192 | overlay2/8f6ad7d4384.../diff/usr/lib/python3.12/__pycache__ 193 | ... 194 | ``` 195 | 196 | 可以实际看到目录是只占用 4 层大小。Docker 镜像实际由多个只读层和元数据组成: 197 | 198 | 1. 层(Layers):每一层代表一次文件系统变更(例如添加、修改或删除文件)。这些层是只读的,并且可以在多个镜像之间共享,从而节省存储空间。 199 | 2. 元数据(Metadata):包含镜像的各种配置和信息: 200 | - 配置(Config):包括容器的默认命令(CMD)、启动进程(ENTRYPOINT)、工作目录(WORKDIR)、环境变量(ENV)、暴露端口(EXPOSE)等。 201 | - 标签(Labels):用于存储镜像的额外信息,如版本号、作者等,以键值对的形式存在。 202 | - 环境变量(Environment Variables):在构建镜像时通过 ENV 指令设置的环境变量,运行容器时会传递给容器内的进程。 203 | - 历史记录(History):记录了每一层的创建信息、构建命令历史等,用于追踪镜像的构建过程。 204 | 205 | ## overlay2 存储驱动原理 206 | 207 | 在 linux 里,我们把一个块设备 `/dev/sdb1` 挂载到一个路径 `/data` 上,如果 `/data` 目录之前有内容,会被隐藏,`/data/` 下是块设备 `/dev/sdb1` 上的文件系统上的文件。那么有没有一种挂载相关的,让我们能够把 `>=2` 个目录合并挂载到一个路径上,overlay2 就是这样的,它的概念如图所示 208 | 209 | ![](images/upper-lower-dir-768x371.png) 210 | 211 | overlayfs 分为 lowerdir 、upperdir 、workdir 和 merged,按照图例所示: 212 | 1. 底下第一层是 File 1 2 3 213 | 2. 第二层是 File1 修改了,File 2 删除了,添加了 File 4 214 | 3. 最终叠加的目录 merged 内的内容就是如图顶部所示 215 | 216 | upperdir 对于 lowerdir 文件的修改,并不会实际的修改下层文件,而是复制到当前层修改。而删除一个 lowerdir 文件,则是会在 upperdir 创建 whiteout 文件,相当与 `mknod c 0 0`,从上到下读取到这样的 char 字符设备会直接返回不存在。 217 | 218 | 存储的 layer + overlayfs 的叠加挂载,然后容器的 rootfs 就是完整的了。 219 | 220 | ## 容器镜像 diff 221 | 222 | 前面的图,可以类比成下面的 Dockerfile 223 | 224 | ```Dockerfile 225 | FROM xxx #包含 File 1 2 3 226 | RUN set -eux; \ 227 | sed -i 's#xxx#yyy#' File1; \ 228 | rm -f File2; \ 229 | echo 123 > File4; 230 | ``` 231 | 232 | 构建的镜像会把 upperdir 的变动也保存一层,整体镜像有两层,实际上 File2 并没有真正删掉,按照实际举例很多人会这样写: 233 | 234 | ```Dockerfile 235 | FROM xxxx 236 | COPY xxx.tar.gz /tmp 237 | RUN set -eux; \ 238 | tar zxf xxx.tar.gz; \ 239 | cd xxx; \ 240 | ./configure; \ 241 | make; \ 242 | make install; \ 243 | rm -rf /tmp/xxx xxx; 244 | ``` 245 | 246 | 假如 `xxx.tar.gz` 有 200M,而编译安装只会产生 10M ,那么最终镜像的大小会是 `基础镜像` + COPY 层的 `200M` + RUN 层的 `10M`,这显然是不合理的。 247 | 248 | 最常见的解决办法就是 RUN 里直接下载处理: 249 | 250 | ``` 251 | FROM xxxx 252 | RUN set -eux; \ 253 | wget https://xxx/xxx.tar.gz; \ 254 | tar zxf xxx.tar.gz; \ 255 | cd xxx; \ 256 | ./configure; \ 257 | make; \ 258 | make install; \ 259 | rm -rf xxx; 260 | ``` 261 | 262 | ## 链接 263 | 264 | - [目录](directory.md) 265 | - 上一节:[前言](preface.md) 266 | - 下一节:[diff 的更多案例](02.md) 267 | 268 | ## 一些额外信息 269 | 270 | `/etc/docker/daemon.json` 没有配置 `"storage-driver": "overlay2",` 下,docker 会默认探测使用的驱动优先级: 271 | 272 | ```golang 273 | // https://github.com/moby/moby/blob/master/daemon/graphdriver/driver_linux.go#L53 274 | priority = "overlay2,fuse-overlayfs,btrfs,zfs,vfs" 275 | ``` 276 | 277 | overlay2 是在 Linux 4.x 以上版本的内核添加的,但是 RHEL7/CentOS7 在 3.10.0-514 以上的内核也可以使用,因为红帽会 backport 一些内核特性到 3.10 上。 278 | 279 | 同时有个注意点,overlay2 可以在 ext4 和 xfs 文件系统上使用,但是如果 xfs 分区格式化不支持 `ftype=1` 会回退,最常见就是回退到 vfs ,然后导致层没复用,容量占用非常离谱。 280 | 281 | ```golang 282 | // https://github.com/moby/moby/blob/e622cea55698e824ed6e362effe1701fd1e1552f/daemon/graphdriver/overlay2/overlay.go#L152-L158 283 | supportsDType, err := fs.SupportsDType(testdir) 284 | if err != nil { 285 | return nil, err 286 | } 287 | if !supportsDType { 288 | return nil, overlayutils.ErrDTypeNotSupported("overlay2", backingFs) 289 | } 290 | ``` 291 | -------------------------------------------------------------------------------- /eBook/02.md: -------------------------------------------------------------------------------- 1 | # 更多的案例 2 | 3 | 来列举下更多的 overlay diff 遗留案例 4 | 5 | ## 案例场景 6 | 7 | 有些场景下,`xxx.tar.gz` 并没有可供下载的直链,例如存在对象存储里,而 ARG 指令传递 aksk 之类的是会在 docker 镜像构建信息里看到,可以本地起一个监听 docker0 web(这样非本机无法访问,杜绝安全问题),类似下面这样: 8 | 9 | ```Makefile 10 | RANDOM_PORT := $(shell shuf -i 1024-65535 -n 1) 11 | CTR_NAME ?= build-$(RANDOM_PORT) 12 | DOCKER0_IP ?= $(shell ip -4 addr show docker0 | awk -F '[ /]+' '/inet/{print $$3;exit}') 13 | FILE_URL ?= $(DOCKER0_IP):$(RANDOM_PORT) 14 | 15 | download: 16 | mc cp xxx/ops/xxx/xxx.tar.gz ./ 17 | docker run -d --name $(CTR_NAME) \ 18 | -v $$PWD/xxx.tar.gz:/shared/xxx.tar.gz \ 19 | -p $(RANDOM_PORT):80 webshare-img 20 | image: download 21 | docker build . -t xxx --build-arg GZ_URL=$(FILE_URL) 22 | docker rm -f $(CTR_NAME) 23 | ``` 24 | 25 | 然后几个相关文件内容为如下: 26 | 27 | ``` 28 | # .gitignore 29 | xxx.tar.gz 30 | 31 | # .dockerignore 32 | xxx.tar.gz 33 | 34 | # Dockerfile 35 | FROM xxxx 36 | ARG GZ_URL 37 | RUN set -eux; \ 38 | wget http://${GZ_URL}/xxx.tar.gz; \ 39 | tar zxf xxx.tar.gz; \ 40 | cd xxx; \ 41 | ./configure; \ 42 | make; \ 43 | make install; \ 44 | rm -rf xxx; 45 | ``` 46 | 47 | 除了下层添加,上层删除以外,实际上在上层修改也会产生这样的问题,镜像会附带 `底层文件` + `上层修改后的文件` ,类似下面这种: 48 | 49 | ```Dockerfile 50 | FROM xxx 51 | WORKDIR /opt/app 52 | COPY . . 53 | RUN chown -R node:node /opt/app 54 | ... 55 | ``` 56 | 57 | 例如上面这个,假设源码有 100M,最后镜像附带就有两份大小了,可以下面这样避免: 58 | 59 | ```Dockerfile 60 | FROM xxx 61 | WORKDIR /opt/app 62 | COPY --chown=node:node . . 63 | ``` 64 | 65 | 同样的还有下面这样类似: 66 | 67 | ```Dockerfile 68 | FROM xxx 69 | ADD xxx.tar.gz /opt/xxx/font 70 | RUN chmod -R 755 /opt/xxx/font 71 | ``` 72 | 73 | 上面这种最好文件打包 `xxx.tar.gz` 前就处理好目录权限,或者多阶段构建处理: 74 | 75 | ```Dockerfile 76 | FROM xxx as build 77 | ADD xxx.tar.gz /opt/xxx/font 78 | RUN chmod -R 755 /opt/xxx/font 79 | 80 | FROM xxx 81 | COPY --from=build /opt/xxx/font /opt/xxx/font 82 | ``` 83 | 84 | 不光是目录,单个文件的属性变更也一样,这个是之前业务 golang 里之前一个优化的案例: 85 | 86 | ```Dockerfile 87 | FROM golang as build 88 | WORKDIR /opt/app 89 | COPY . . 90 | RUN make build 91 | 92 | FROM base-run 93 | COPY --from=build --chown=app:app /opt/app/bin_file /opt/app 94 | RUN chmod +x /opt/app/bin_file # <--- 95 | ``` 96 | 97 | 编译的二进制有 154M,然后让业务去掉第二阶段结尾 RUN 后镜像缩减了 154M,go build 的文件都有 `chmod a+x` 权限的,不需要额外 chmod。 98 | 99 | 接下来介绍一个插件案例,同事在 es 镜像里添加一些自研插件: 100 | 101 | ```Dockerfile 102 | FROM elasticsearch:7.x 103 | RUN set -eux; \ 104 | es_version=$(./bin/elasticsearch -V | awk -F'[: ,]' '{print $3}'); \ 105 | ./bin/elasticsearch-plugin install https://xxx; \ 106 | chown -R elasticsearch:root /usr/share/elasticsearch 107 | ``` 108 | 109 | 镜像里目录大小 `516M /usr/share/elasticsearch` ,让镜像浪费了挺多空间的,后面修改为 find 配合 chown 只处理 plugins 目录解决: 110 | 111 | ``` 112 | ...; \ 113 | find plugins \! -user elasticsearch -exec chown elasticsearch '{}' +; \ 114 | ``` 115 | 116 | 同时,不要以为 owner 一样,再次执行 chown 同样属性文件还是一样的,例如下面: 117 | 118 | ```shell 119 | $ cat > Dockerfile << EOF 120 | FROM alpine 121 | RUN dd if=/dev/urandom of=random-file bs=1M count=10 122 | RUN chown root:root random-file 123 | EOF 124 | $ docker build . -t test 125 | $ docker history test 126 | IMAGE CREATED CREATED BY SIZE COMMENT 127 | 81f6f4a0e427 About a minute ago /bin/sh -c chown root:root random-file 10.5MB 128 | 9d7678242207 About a minute ago /bin/sh -c dd if=/dev/urandom of=random-file… 10.5MB 129 | 1d34ffeaf190 8 days ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B 130 | 8 days ago /bin/sh -c #(nop) ADD file:e3abcdba177145039… 7.79MB 131 | # 折叠的话记得看最右边的 Size ---> 132 | ``` 133 | 134 | 可以看到文件同样的 owner 是 `root:root` ,因为 overlayfs 是遵循写时复制,对文件会产生修改行为的,都会从下层复制到本层里处理。对于以上 overlay diff 浪费了容量的,可以使用 [dive](https://github.com/wagoodman/dive) 工具分析: 135 | 136 | ```shell 137 | $ CI=true dive test 138 | Using default CI config 139 | Image Source: docker://test 140 | Fetching image... (this can take a while for large images) 141 | Analyzing image... 142 | efficiency: 63.5422 % 143 | wastedBytes: 20971520 bytes (21 MB) 144 | userWastedPercent: 100.0000 % 145 | Inefficient Files: 146 | Count Wasted Space File Path 147 | 2 21 MB /random-file 148 | ``` 149 | 150 | 如果不在 CI 构建里使用,可以去掉前面的 `CI=true ` 交互使用,dive 会分析 layer 里上下层重复以及添加的文件。例如上面左下角,说 `/random-file` 出现了两次。 151 | 152 | ## 总结 153 | 154 | - `COPY/ADD` 添加文件后权限会变化的,可以 `COPY --chown` 、多阶段或者 Makefile 之类预处理解决 155 | - `COPY` 添加的文件被后面的 `RUN` 使用完删除掉的,换成 `RUN` 里下载 156 | - 不要直接 chown 和 chmod 修改下层的文件,而是使用 find 条件 + -exec 组合使用,只修改不符合的文件 157 | 158 | ## 链接 159 | 160 | - [目录](directory.md) 161 | - 上一节:[overlay2 和层 diff](01.md) 162 | - 下一节:[一些最小依赖优化大小经验](03.md) 163 | -------------------------------------------------------------------------------- /eBook/03.md: -------------------------------------------------------------------------------- 1 | # 一些最小依赖优化大小经验 2 | 3 | 然后接下来的部分就是和 overlay 层无关的了,主要是一些经验和冷门知识。 4 | 5 | ## 多阶段构建 6 | 7 | 构建基本分为两个大阶段,构建 + 打包,后者是打包成最小的运行依赖,前者则会附带很多编译工具,这些都是没必要附带除去的 8 | 9 | ### golang 10 | 11 | ```Dockerfile 12 | FROM golang:1.22 AS build 13 | WORKDIR /opt/app 14 | COPY . . 15 | RUN CGO_ENABLED=0 go build -o app_serve . 16 | 17 | FROM photon:5.0 18 | WORKDIR /opt/app 19 | COPY --from=build /opt/app/app_serve . 20 | ... 21 | ``` 22 | 23 | daemon 类进程,都不推荐使用 musl libc 的 alpine,很多 glibc 的特性都不支持。 24 | 25 | ### nodejs 26 | 27 | 在 Node.js 中,分阶段构建的主要原因是为了优化构建过程和减少不必要的依赖。在开发过程中,我们会使用 npm install 命令安装所有的依赖,包括开发依赖(devDependencies)。这些开发依赖可能包含一些仅在开发过程中需要的工具、测试框架等,而在生产环境中并不需要。同时,在私有化里,一些客户现场会扫描主机文件,也就是会扫描到 `/var/lib/docker/overlay2/` 下的文件,减少不必要的依赖可以降低潜在的安全风险,使用 slim 镜像也是同理。 28 | 29 | ```Dockerfile 30 | FROM node:18 AS build 31 | WORKDIR /app 32 | COPY package*.json ./ 33 | RUN npm install 34 | COPY . . 35 | RUN npm run build 36 | 37 | FROM node:18-slim 38 | WORKDIR /app 39 | COPY --from=build /app/package*.json ./ 40 | RUN npm install --only=production 41 | COPY --from=build /app/build ./build 42 | CMD ["node", "build/server.js"] 43 | ``` 44 | 45 | 其他也是类似的,这里不再举例。 46 | 47 | 参考: 48 | - [nodejs-web-applications-with-docker](https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/) 49 | 50 | ## 清理缓存 51 | 52 | 这部分内容主要是讲解一些在使用包管理或者依赖相关工具的时候,会把下载的包缓存存放着,这些缓存可以清理掉。例如: 53 | 54 | ```Dockerfile 55 | RUN set -eux; \ 56 | yum install -y libpng; \ 57 | yum clean all; \ 58 | rm -rf /var/cache/yum 59 | ``` 60 | 61 | 其他类似,如下: 62 | 63 | - `pip install --no-cache-dir xxx && find / -type d -name __pycache__ -prune -exec rm -rf {} \;` 64 | - `apk add --no-cache xxx` 65 | - `npm install --production && npm cache clean --force` 66 | - `apt install xxx && apt clean && rm -rf /var/cache/apt/* /var/lib/apt/lists/*` 67 | 68 | apt 这块还有额外注意的一点,例如安装一个 curl 的包: 69 | 70 | ``` 71 | $ apt install -y curl 72 | ... 73 | The following additional packages will be installed: 74 | ca-certificates krb5-locales libbrotli1 libcurl4 libgssapi-krb5-2 libk5crypto3 libkeyutils1 libkrb5-3 libkrb5support0 libldap-2.5-0 libldap-common libnghttp2-14 libpsl5 librtmp1 libsasl2-2 libsasl2-modules libsasl2-modules-db 75 | libssh2-1 libssl3 openssl publicsuffix 76 | Suggested packages: 77 | krb5-doc krb5-user libsasl2-modules-gssapi-mit | libsasl2-modules-gssapi-heimdal libsasl2-modules-ldap libsasl2-modules-otp libsasl2-modules-sql 78 | The following NEW packages will be installed: 79 | ca-certificates curl krb5-locales libbrotli1 libcurl4 libgssapi-krb5-2 libk5crypto3 libkeyutils1 libkrb5-3 libkrb5support0 libldap-2.5-0 libldap-common libnghttp2-14 libpsl5 librtmp1 libsasl2-2 libsasl2-modules libsasl2-modules-db 80 | libssh2-1 libssl3 openssl publicsuffix 81 | 0 upgraded, 22 newly installed, 0 to remove and 0 not upgraded. 82 | ``` 83 | 84 | apt 安装的时候会有三部分: 85 | 86 | - Required packages 87 | - Recommended packages 88 | - Suggested packages 89 | 90 | apt 提供选项 `--no-install-recommends` 不安装推荐的包,来减少容量。 91 | 92 | ```shell 93 | $ apt install --no-install-recommends curl 94 | ... 95 | The following additional packages will be installed: 96 | libbrotli1 libcurl4 libgssapi-krb5-2 libk5crypto3 libkeyutils1 libkrb5-3 libkrb5support0 libldap-2.5-0 libnghttp2-14 libpsl5 librtmp1 libsasl2-2 libsasl2-modules-db libssh2-1 libssl3 97 | Suggested packages: 98 | krb5-doc krb5-user 99 | Recommended packages: 100 | ca-certificates krb5-locales libldap-common publicsuffix libsasl2-modules 101 | The following NEW packages will be installed: 102 | curl libbrotli1 libcurl4 libgssapi-krb5-2 libk5crypto3 libkeyutils1 libkrb5-3 libkrb5support0 libldap-2.5-0 libnghttp2-14 libpsl5 librtmp1 libsasl2-2 libsasl2-modules-db libssh2-1 libssl3 103 | 0 upgraded, 16 newly installed, 0 to remove and 0 not upgraded 104 | ``` 105 | 106 | 另外,如果看过很多官方的 Dockerfile,发现在 `apt install` 后并没有清理缓存,那是因为 debian 那些基础镜像的 rootfs 里自带了 apt 的 Post-Invoke 之类的配置文件: 107 | 108 | ```shell 109 | $ ls -l /etc/apt/apt.conf.d/ 110 | total 24 111 | -rw-r--r-- 1 root root 399 May 25 2023 01autoremove 112 | -rw-r--r-- 1 root root 182 Jan 8 2023 70debconf 113 | -rw-r--r-- 1 root root 754 May 13 00:00 docker-autoremove-suggests 114 | -rw-r--r-- 1 root root 1175 May 13 00:00 docker-clean 115 | -rw-r--r-- 1 root root 481 May 13 00:00 docker-gzip-indexes 116 | -rw-r--r-- 1 root root 269 May 13 00:00 docker-no-languages 117 | ``` 118 | 119 | 参考: 120 | - [moby/moby 的 mkimage/debootstrap](https://github.com/moby/moby/blob/03e2923e42446dbb830c654d0eec323a0b4ef02a/contrib/mkimage/debootstrap#L82-L105) 121 | - [reduced-our-docker-images](https://ubuntu.com/blog/we-reduced-our-docker-images-by-60-with-no-install-recommends) 122 | 123 | ## 用完删除 124 | 125 | 同时,包管理安装一些工具或者编译工具,可以在使用完成后删掉,例如下面利用 `apt-mark` 使用完后把没 mark 的包删除掉: 126 | 127 | ```Dockerfile 128 | ENV GOSU_VERSION 1.17 129 | RUN set -eux; \ 130 | savedAptMark="$(apt-mark showmanual)"; \ 131 | apt-get update; \ 132 | apt-get install -y --no-install-recommends ca-certificates gnupg wget; \ 133 | rm -rf /var/lib/apt/lists/*; \ 134 | arch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \ 135 | ... 136 | wget -O /usr/local/bin/gosu.asc "$url.asc"; \ 137 | wget -O /usr/local/bin/gosu "$url"; \ 138 | echo "$sha256 */usr/local/bin/gosu" | sha256sum -c -; \ 139 | export GNUPGHOME="$(mktemp -d)"; \ 140 | gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \ 141 | gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \ 142 | gpgconf --kill all; \ 143 | rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \ 144 | apt-mark auto '.*' > /dev/null; \ 145 | [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \ 146 | apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ 147 | chmod +x /usr/local/bin/gosu; \ 148 | gosu --version; \ 149 | gosu nobody true 150 | ``` 151 | 152 | 案例二,pm2 安装会用到 git: 153 | 154 | ```Dockerfile 155 | RUN set -eux; \ 156 | savedAptMark="$(apt-mark showmanual)"; \ 157 | apt-get update; \ 158 | apt-get install -y --no-install-recommends git openssh-client ca-certificates; \ 159 | npm config set registry https://registry.npmmirror.com; \ 160 | npm install -g pm2; \ 161 | git config --global url."https://github.com".insteadOf ssh://git@github.com; \ 162 | pm2 install pm2-intercom; \ 163 | apt-mark auto '.*' > /dev/null; \ 164 | [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \ 165 | apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ 166 | apt-get clean; \ 167 | npm cache clean --force; \ 168 | rm -rf /tmp/* /var/cache/apt/* /var/lib/apt/lists/* ~/.git 169 | ``` 170 | 171 | 还有编译安装,一个 `RUN` 里安装编译依赖,下载源码,编译安装完成后,清理掉编译依赖: 172 | 173 | ``` 174 | RUN set -eux; \ 175 | \ 176 | apk add --no-cache --virtual .build-deps \ 177 | coreutils \ 178 | dpkg-dev dpkg \ 179 | gcc \ 180 | linux-headers \ 181 | make \ 182 | musl-dev \ 183 | openssl-dev \ 184 | wget \ 185 | ; \ 186 | .... 187 | apk add --no-network --virtual .redis-rundeps $runDeps; \ 188 | apk del --no-network .build-deps; \ 189 | ``` 190 | 191 | ## 合并一些层 192 | 193 | 另外一些 rootfs 的包管理会有 db 文件存储,例如 centos7 的 yum : 194 | 195 | ```shell 196 | $ ls -lh /var/lib/rpm 197 | total 23M 198 | -rw-r--r-- 1 root root 944K Nov 13 2020 Basenames 199 | -rw-r--r-- 1 root root 8.0K Nov 13 2020 Conflictname 200 | -rw-r--r-- 1 root root 284K Nov 13 2020 Dirnames 201 | -rw-r--r-- 1 root root 8.0K Nov 13 2020 Group 202 | -rw-r--r-- 1 root root 12K Nov 13 2020 Installtid 203 | -rw-r--r-- 1 root root 16K Nov 13 2020 Name 204 | -rw-r--r-- 1 root root 8.0K Nov 13 2020 Obsoletename 205 | -rw-r--r-- 1 root root 19M Nov 13 2020 Packages 206 | -rw-r--r-- 1 root root 1.8M Nov 13 2020 Providename 207 | -rw-r--r-- 1 root root 84K Nov 13 2020 Requirename 208 | -rw-r--r-- 1 root root 24K Nov 13 2020 Sha1header 209 | -rw-r--r-- 1 root root 16K Nov 13 2020 Sigmd5 210 | -rw-r--r-- 1 root root 8.0K Nov 13 2020 Triggername 211 | -rw-r--r-- 1 root root 344K Nov 13 2020 __db.001 212 | -rw-r--r-- 1 root root 96K Nov 13 2020 __db.002 213 | -rw-r--r-- 1 root root 1.3M Nov 13 2020 __db.003 214 | ``` 215 | 216 | 很多包管理工具存储了很多信息,而在使用 Dockerfile 的时候分层较多: 217 | 218 | ```Dockerfile 219 | # 安装编译工具1 220 | RUN set -eux; \ 221 | yum install xxx; \ 222 | ... 223 | yum clean all; \ 224 | rm -rf /var/cache/yum 225 | # 安装编译工具1 226 | RUN set -eux; \ 227 | yum install xxx2; \ 228 | ... 229 | yum clean all; \ 230 | rm -rf /var/cache/yum 231 | 232 | RUN ... 233 | ``` 234 | 235 | rpm 变动则会改变 `Packages` 文件内容,后期这个 `Packages` db 文件会很大,例如到 100MB,而由于 overlay 特性 n 个层都附带不一样的 Pakcages 文件,会很浪费空间。所以可以合并成一层 RUN,然后写好注释,每个依赖给哪个工具和二进制使用。 236 | 237 | ## .dockerignore 238 | 239 | 善用 `.dockerignore` 减少不必要的文件到镜像里 240 | 241 | ## docker-slim 242 | 243 | [slimtoolkit/slim](https://github.com/slimtoolkit/slim) ,这个工具我是存疑的,把我们的部分镜像处理成 2M ,还是不够智能。 244 | 245 | ## dockle 246 | 247 | [goodwithtech/dockle](https://github.com/goodwithtech/dockle) 是一个用于检查 Docker 镜像安全性和最佳实践的工具,例如前面缓存没清理它能扫描到。 248 | 249 | ## 链接 250 | 251 | - [目录](directory.md) 252 | - 上一节:[diff 的更多案例](02.md) 253 | - 下一节:[一些镜像介绍](04.md) 254 | -------------------------------------------------------------------------------- /eBook/04.md: -------------------------------------------------------------------------------- 1 | # 一些镜像介绍 2 | 3 | 前面都是 Dockerfile 层面,更进一步的话,可以选择更小的运行时镜像,daemon 类进程不推荐使用 alpine: 4 | 5 | - [GoogleContainerTools/distroless](https://github.com/GoogleContainerTools/distroless): 非常小的依赖 6 | - [vmware/photon](https://github.com/vmware/photon): glibc + bash + tdnf 7 | - [bitnami/minideb](https://github.com/bitnami/minideb) 精简的 debian 镜像 8 | - `registry.access.redhat.com/ubi`: 红帽的精简镜像,去掉内置自带的python 而使用 c 实现的 `microdnf`作为包管理工具,使用 centos 的可以考虑切换过去 9 | 10 | 不想太小也不想太重,可以使用 `debian:slim`,在关于一些中间件镜像方面,容量小和安全性方面,如果官方的镜像不满足,可以使用 [bitnami/containers](https://github.com/bitnami/containers) 制作的镜像。 11 | -------------------------------------------------------------------------------- /eBook/directory.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | - [前言](preface.md) 3 | 4 | - [overlay2 和层 diff](01.md) 5 | - [diff 的更多案例](02.md) 6 | - [一些最小依赖优化大小经验](03.md) 7 | - [一些镜像介绍](04.md) 8 | 9 | -------------------------------------------------------------------------------- /eBook/images/image-overlayfs-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangguanzhang/small-container-image/54c59248a60b1deab8ff409a982cf56e0d2d875f/eBook/images/image-overlayfs-demo.png -------------------------------------------------------------------------------- /eBook/images/upper-lower-dir-768x371.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangguanzhang/small-container-image/54c59248a60b1deab8ff409a982cf56e0d2d875f/eBook/images/upper-lower-dir-768x371.png -------------------------------------------------------------------------------- /eBook/preface.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | ## 关于本分享 4 | 5 | 此次分享都会是在 linux 上使用 docker 进行方便演示,很多要点是通用的,如果使用 docker 照着学习,请使用尽量新的 docker,而不是使用类似 redhat7 devicemapper 那种 `1.13.x` docker。 6 | 7 | 不会过多介绍一些底层细节(例如手动创建目录给你挂载一个 overlay 目录,然后删除添加修改文件之类的给你演示 overlayfs 的特性),而主要是图文和简单实践来理解。部分章节的尾部后面可能会介绍一些额外知识点。 8 | 9 | ## 链接 10 | 11 | - [目录](directory.md) 12 | - 下一部分: [overlay2 和层 diff](01.md) 13 | --------------------------------------------------------------------------------