[TOC]
0x01 前言简述
描述:前面我们学习并且记录了 Dockerfile 最佳实践的一些规则,但是仅仅停在理论中并不是我的风格,所以出现了本篇文章同时也加深学习;
从最佳实践原则我们知道要缩小镜像大小,与选择的基础镜像是非常有关系的,比如buysbox (工具箱)与alpine (操作系统)
镜像小的您超出您的想象,需要
[TOC]
描述:前面我们学习并且记录了 Dockerfile 最佳实践的一些规则,但是仅仅停在理论中并不是我的风格,所以出现了本篇文章同时也加深学习;
从最佳实践原则我们知道要缩小镜像大小,与选择的基础镜像是非常有关系的,比如buysbox (工具箱)与alpine (操作系统)
镜像小的您超出您的想象,需要
[TOC]
描述:前面我们学习并且记录了 Dockerfile 最佳实践的一些规则,但是仅仅停在理论中并不是我的风格,所以出现了本篇文章同时也加深学习;
从最佳实践原则我们知道要缩小镜像大小,与选择的基础镜像是非常有关系的,比如buysbox (工具箱)与alpine (操作系统)
镜像小的您超出您的想象,需要1
2
3
4
5docker pull alpine # 容器中最小的Linux发行版
docker pull busybox # 嵌入式以及物联网系统中最常用的Linux下的工具箱
docker images | grep -E "busybox|alpine"
busybox latest c7c37e472d31 3 weeks ago 1.22MB
alpine latest a24bb4013296 8 weeks ago 5.57MB
对于刚接触容器的人来说他们很容易被自己构建的 Docker 镜像体积吓到,我只需要一个几 MB 的可执行文件而已,为何镜像的体积会达到 1 GB 以上?
答:相信下面的奇技淫巧会帮助你精简镜像,同时又不牺牲开发人员和运维人员的操作便利性。
基础环境.实验(实践)环境准备:1
2
3
4
5[root@k8s-yum-server ~]# cat /etc/system-release
CentOS Linux release 7.8.2003 (Core)
[root@k8s-yum-server ~]# uname -r
5.7.0-1.el7.elrepo.x86_64
(1) 建立一个空白目录进行构建上下文准备,切记不要在家里录下创建一个 Dockerfile 紧接着 docker build 一把梭
1 | # 正确做法是为项目建立一个文件夹,把构建镜像时所需要的资源放在这个文件夹下 |
.dockerignore
文件来忽略不需要的文件发送到 docker 守护进程(2) 使用体积较小的基础镜像,比如 alpine 或者 debian:buster-slim,像 openjdk 可以选用openjdk:8-slim;
1 | $docker pull debian:buster-slim |
musl libc
而不是正统的 glibc 库,另外对于一些依赖 glibc 的大型项目像 openjdk 、tomcat、rabbitmq
等都不建议使用 alpine 基础镜像,因为 musl libc 可能会导致 jvm 一些奇怪的问题,这也是为什么 tomcat 官方没有给出基础镜像是 alpine 的 Dockerfile 的原因。(3) 更改为国内镜像软件源,提升容器构建速度目前国内稳定可靠的镜像站主要有,华为云、阿里云、腾讯云、163等。
1 | # alpine 基础镜像修改软件源 |
(4) 镜像时区设置由于绝大多数基础镜像都是默认采用UTC的时区与北京时间相差8个小时,将会会导致容器内的时间与北京时间不一致,因而会对一些应用造成一些影响,还会影响容器内日志和监控的数据,可以通过以下操作进行解决;
1 | # 方式1.通过设置环境变量来设定容器内的时区。 |
1 | # 方式1 |
(5) 使用URL添加源码,如果不采用分阶段构建对于一些需要在容器内进行编译的项目,最好通过 git 或者 wegt 的方式将源码打入到镜像内,而非采用 ADD 或者 COPY ,因为源码编译完成之后源码就不需要可以删掉了,而通过 ADD 或者 COPY 添加进去的源码已经用在下一层镜像中了是删不掉滴啦;
1 | # centos 7 |
1 | FROM alpine:3.10 |
构建之后的对比使用项目默认的 Dockerfile 进行构建的话,镜像大小接近 500MB 😂,而经过一些的优化,将所有的 RUN 指令合并为一条,最终构建出来的镜像大小为 30MB 😂。
1 | # 结论: 建议采用alpine进行构建只不过需要注意其alpine的c库是`musl libc`而不是正统的glibc库 |
(6) 使用虚拟编译环境对于只在编译过程中使用到的依赖,我们可以将这些依赖安装在虚拟环境中,编译完成之后可以一并删除这些依赖
1 | #比如 alpine 中可以使用 apk add --no-cache --virtual .build-deps ,后面加上需要安装的相关依赖。 |
.build-deps
后面接的是编译时以来的软件包,并不是所有的编译依赖都可以删除,不要把运行时的依赖包接在后面最好单独 add 一下。(7) 最小化层数至docker 在 1.10 以后,只有 RUN、COPY 和 ADD 指令会创建层,其他指令会创建临时的中间镜像但是不会直接增加构建的镜像大小了;
1 | # 如果多个文件需要添加到容器中不同的路径,每个文件使用一条 ADD 指令的话就会增加一层镜像,可以通过以下方式进行精简镜像构建时的大小; |
;
,在学习官方的DockerFile则发现使用的;
的居多;(8) 采用多阶段构建镜像可以减小镜像大小,但是注意为了保证镜像正常构建运行,需要在COPY --from=0
指令执行时候将上阶段的成果采用绝对路径复制防止构建出错;
1 | FROM golang # 默认工作空间为/go |
描述:每一个初次使用自己写好的代码指令构建Docker镜像时候都会被镜像的大小所吓倒因为确实太大,当然前提是没有进行 DockerFile 优化中;
比如: 构建一个镜像采用编译.c的C程序然后并运行容器
示例C程序:1
2
3
4
5
6
7
8
9mkdir /opt/hello && cd $_
cat > hello.c <<EOF
#include <stdio.h>
/* hello.c */
int main () {
puts("Hello, world!");
return 0;
}
EOF
DockerFile1
2
3
4
5
6cat > Dockerfile <<EOF
FROM gcc
COPY hello.c .
RUN gcc -o hello hello.c
CMD ["./hello"]
EOF
基础实践:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18$docker build --tag gcc-hello:latest .
# 表示 docker cli 命令行客户端将我们当前目录(即构建上下文) build context 打包发送 Docker daemon 守护进程 (即 dockerd)的过程。
# KB: 代表当前上下文大小
Sending build context to Docker daemon 3.072kB
....
Successfully built eb9ae8b1c49c
Successfully tagged gcc-hello:latest
# 此时您会发现构建成功的镜像体积远远超过了 1 GB, 因为该镜像包含了整个 gcc 镜像的内容;
$docker images gcc-hello
REPOSITORY TAG IMAGE ID CREATED SIZE
gcc-hello latest eb9ae8b1c49c 5 minutes ago 1.19GB
# 如果使用 Ubuntu 镜像安装 C 编译器,最后编译程序你会得到一个大概 300 MB 大小的镜像比上面的镜像小多了
$docker run -it --rm gcc-hello
Hello, world!
$docker run -it --rm gcc-hello ls -alh hello
-rwxr-xr-x. 1 root root 16K Jul 27 08:21 hello #因为编译好的可执行文件还不到 20 KB
示例Go程序:1
2
3
4
5
6
7
8
9mkdir /opt/go-hello/ && cd $_
cat > hello.go <<EOF
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
EOF
Dockerfile:1
2
3
4
5
6cat > dockerfile <<EOF
FROM golang
COPY hello.go .
RUN go build hello.go
CMD ["./hello","ls -alh ./hello"]
EOF
使用基础镜像 golang 构建的镜像大小是 800 MB,而编译后的可执行文件只有 2 MB 大小:1
2
3
4
5
6
7
8
9
10
11
12
13# 镜像构建
$docker build --tag go-hello .
Sending build context to Docker daemon 3.072kB
....
Successfully built d1bb1eb974f4
Successfully tagged go-hello:latest
# 运行镜像
[root@k8s-yum-server go-hello]$ docker run -it --rm go-hello
Hello, world!
[root@k8s-yum-server go-hello]$ docker run -it --rm go-hello bash
root@f610b4030f2d:/go# ls -alh hello
-rwxr-xr-x. 1 root root 2.0M Jul 27 08:43 hello # 比gcc生成的大多了
Tips: 为了更直观地对比不同镜像的大小,所有镜像都使用相同的镜像名不同的标签,当然您需要在构建镜像时候采用--tag
参数进行。
如何缩减镜像大小?
答:要想大幅度减少镜像的体积,多阶段构建是必不可少的。
多阶段构建的想法很简单: “我不想在最终的镜像中包含一堆 C 或 Go 编译器和整个编译工具链,我只要一个编译好的可执行文件!
“
多阶段构建可以由多个 FROM 指令识别,每一个 FROM 语句表示一个新的构建阶段,阶段名称可以用 AS 参数指定,例如:
本例使用基础镜像 gcc 来编译程序 hello.c,然后启动一个新的构建阶段,它以 ubuntu 作为基础镜像,将可执行文件 hello 从上一阶段拷贝到最终的镜像中1
2
3
4
5
6
7
8cat > dockerfile <<EOF
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM ubuntu
COPY --from=mybuildstage hello .
CMD ["./hello"]
EOF
镜像构建:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27$docker build -t go-hello:stage -f Dockerfile .
Sending build context to Docker daemon 3.072kB
Step 1/6 : FROM gcc AS mybuildstage
---> 21f378ba43ec
Step 2/6 : COPY hello.c .
---> Using cache
---> 09252ee7e000
Step 3/6 : RUN gcc -o hello hello.c
---> Using cache
---> 9615c168cdc0
Step 4/6 : FROM ubuntu
latest: Pulling from library/ubuntu
3ff22d22a855: Pull complete
e7cb79d19722: Pull complete
323d0d660b6a: Pull complete
b7f616834fd0: Pull complete
Digest: sha256:5d1d5407f353843ecf8b16524bc5565aa332e9e6a1297c73a92d3e754b8a636d
Status: Downloaded newer image for ubuntu:latest
---> 1e4467b07108
Step 5/6 : COPY --from=mybuildstage hello .
---> 4659f572446e
Step 6/6 : CMD ["./hello"]
---> Running in be24d1c6b4aa
Removing intermediate container be24d1c6b4aa
---> 5934753f8f4f
Successfully built 5934753f8f4f
Successfully tagged go-hello:stage
最终的镜像大小是73.9 MB,比之前的 1.1 GB 减少了 95%1
2
3
4
5
6
7
8$docker images go-hello
REPOSITORY TAG IMAGE ID CREATED SIZE
go-hello stage 5934753f8f4f 9 minutes ago 73.9MB
ubuntu latest 1e4467b07108 2 days ago 73.9MB
gcc latest 21f378ba43ec 3 days ago 1.19GB
$docker run -it --rm go-hello:stage
Hello, world!
如果 Dockerfile 内容不是很复杂,构建阶段也不是很多可以直接使用序号表示构建阶段。一旦 Dockerfile 变复杂了,构建阶段增多了,最好还是通过关键词 AS 为每个阶段命名这样也便于后期维护。1
2
3# 在声明构建阶段时可以不必使用关键词 AS,最终阶段拷贝文件时可以直接使用序号表示之前的构建阶段(从零开始)下面两行是等效的
COPY --from=0 hello . # 不是很复杂时候使用
COPY --from=mybuildstage hello . # 复杂时候一定要用阶段名称而不是阶段索引;
描述:回到我们的 hello world,C 语言版本的程序大小为 16 kB,Go 语言版本的程序大小为 2 MB,那么我们到底能不能将镜像缩减到这么小?能否构建一个只包含我需要的程序,没有任何多余文件的镜像?
答案:是肯定的,你只需要将多阶段构建的第二阶段的基础镜像改为 scratch (一个虚拟镜像),不能被 pull,也不能运行,因为它表示空、nothing!这就意味着新镜像的构建是从零开始,不存在其他的镜像层;
虽然它可以极大的缩小镜像大小,但是使用它 scratch 作为基础镜像时会带来很多的不便(事物往往都不是那么完美的)。
使用scratch作为基础镜像缺点:
1 | FROM scratch |
ls、ps、ping
等命令统统没有,当然了shell 也没有(上文提过了),你无法使用·docker exec
进入容器,也无法查看网络堆栈信息等等。1 | $docker run --rm -it go-hello:scratch ls |
1 | # 错误信息:standard_init_linux.go:211: exec user process caused "no such file or directory" |
C程序/Go 程序使用的是动态链接
。上面的 hello world 程序使用了标准库文件 libc.so.6
,所以只有镜像中包含该文件,程序才能正常运行。Q:那么该如何解决标准库的问题呢?
答:有三种方案。
1.使用静态库我们可以让编译器使用静态库编译程序办法有很多,如果使用 gcc 作为编译器,只需加上一个参数 -static(推荐方式:以大小牺牲满足程序的健壮性):1
$gcc -o hello hello.c -static
编译完的可执行文件大小为 760 kB相比于之前的 16kB 是大了好多,这是因为可执行文件中包含了其运行所需要的库文件,编译完的程序就可以跑在 scratch 镜像中了。
如果使用 alpine 镜像作为基础镜像来编译得到的可执行文件会更小(< 100kB)。
2.拷贝库文件到镜像中为了找出程序运行需要哪些库文件可以使用 ldd 工具:1
2
3
4
5
6$ldd hello
linux-vdso.so.1 (0x00007ffdf8acb000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007ff897ef6000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ff8980f7000)
# 从输出结果可知该程序只需要 libc.so.6 这一个库文件,linux-vdso.so.1 与一种叫做VDSO[3]的机制有关,用来加速某些系统调用可有可无, ld-linux-x86-64.so.2 表示动态链接器本身,包含了所有依赖的库文件的信息。
你可以选择将 ldd 列出的所有库文件拷贝到镜像中但这会很难维护,特别是当程序有大量依赖库时(不切实际了不建议使用);
对于 hello world 程序来说,拷贝库文件完全没有问题,但对于更复杂的程序(例如使用到 DNS 的程序)就会遇到令人费解的问题,glibc(GNU C library)通过一种相当复杂的机制来实现 DNS
,这种机制叫 NSS(Name Service Switch, 名称服务开关)。
它需要一个配置文件 /etc/nsswitch.conf 和额外的函数库,但使用 ldd 时不会显示这些函数库,因为这些库在程序运行后才会加载。
如果想让 DNS 解析正确工作,必须要拷贝这些额外的库文件(/lib64/libnss_*)。
3.使用 busybox:glibc 作为基础镜像
有一个镜像可以完美解决所有的这些问题那就是 busybox:glibc, 因为它只有 5 MB 大小并且包含了 glibc 和各种调试工具。
如果你想选择一个合适的镜像来运行使用动态链接的程序busybox:glibc是最好的选择。
注意:如果你的程序使用到了除标准库之外的库,仍然需要将这些库文件拷贝到镜像中。
实际案例:1
2
3
4
5
6
7
8
9cat > Dockerfile<<EOF
FROM golang
WORKDIR /go
COPY hello.go .
RUN go build hello.go
FROM scratch
COPY --from=0 /go/hello .
CMD ["./hello"]
EOF
镜像构造:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27$docker build -f Dockerfile -t go-hello:scratch .
Sending build context to Docker daemon 4.096kB
Step 1/7 : FROM golang
...
---> Running in 8eba3646dcc4
Removing intermediate container 8eba3646dcc4
---> b3d1964b4d47
Step 5/7 : FROM scratch
Step 6/7 : COPY --from=0 /go/hello .
---> 52e976b8f1f3
Step 7/7 : CMD ["./hello"]
---> Running in 2247a541f3c6
Removing intermediate container 2247a541f3c6
---> cb05b87d0012
Successfully built cb05b87d0012
Successfully tagged go-hello:scratch
$docker run --rm -it go-hello:scratch
Hello, world!
# 构建镜像的大小比对
$docker images go-hello
REPOSITORY TAG IMAGE ID CREATED SIZE
go-hello scratch cb05b87d0012 3 minutes ago 2.07MB #构建的镜像大小正好就是 2 MB
go-hello stage 5934753f8f4f 5 hours ago 73.9MB
go-hello latest d1bb1eb974f4 6 hours ago 812MB
静态库实践:1
2
3
4
5
6
7
8
9
10mkdir /opt/gcc-static && cd $_
cat > Dockerfile-static<<EOF
FROM gcc AS mybuildstage
WORKDIR /src
COPY hello.c .
RUN gcc -o hello hello.c -static
FROM scratch
COPY --from=0 /src/hello .
CMD ["./hello"]
EOF
构建实践:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32$docker build -f Dockerfile-static -t gcc-static:scratch .
Sending build context to Docker daemon 3.072kB
Step 1/7 : FROM gcc AS mybuildstage
---> 21f378ba43ec
Step 2/7 : WORKDIR /src
---> Running in 7055a4f90b2a
Removing intermediate container 7055a4f90b2a
---> a02107ce9204
Step 3/7 : COPY hello.c .
---> cd278ef2de60
Step 4/7 : RUN gcc -o hello hello.c -static
---> Running in 368cc4437ac4
Removing intermediate container 368cc4437ac4
---> 506e744092b2
Step 5/7 : FROM scratch
Step 6/7 : COPY --from=0 /src/hello .
---> 1867700fa780
Step 7/7 : CMD ["./hello"]
---> Running in bb19b78211bd
Removing intermediate container bb19b78211bd
---> a6991f9571b3
Successfully built a6991f9571b3
Successfully tagged gcc-static:scratch
# 镜像查看以及执行镜像
$docker images gcc-static
REPOSITORY TAG IMAGE ID CREATED SIZE
gcc-static scratch a6991f9571b3 56 seconds ago 945kB #此种方式比golang多阶段构建生成的镜像更小
# 但是调试是真的烦
$docker run --rm -it gcc-static:scratch
Hello, world!
总结
描述:最后来对比一下不同构建方法构建的镜像大小:
glibc
:6.5 MBglibc
:940 kBmusl libc
:94 kB描述:记录镜像分析缩减建议工具与学习阶段所遇的一些技巧记录
描述:在进行Docker安装Tomcat前我们先简单聊到openjdk镜像的tag说明,因为Tomcat属于Java应用所以安装JDK环境是必不可少的;
将java应用作成docker镜像时,需要镜像中带有jdk或者jre环境,通常有三种情况:
三种方式各有优劣
为了更加精简以及程序可以正常运行所以我们必须对其基础镜像选择有一个简单的了解;
在hub.docker.com上搜索jdk官方镜像关键字openjdk,点进详情页后寻找我们常用的jdk8的镜像有多个Tags例如:https://hub.docker.com/_/openjdk?tab=tags
实际上Docker大多数应用都默认采用Debian操作系统进行构建镜像,所以我们需要对debian版本号进行一个简单的了解:
|debian发行版本号 | 含义 |
|:- | :- |
|buster|当前的稳定版|
|stretch |旧的稳定版,包含了Debian官方最近一次发行的软件包,优先推荐使用的版本|
|testing |测试版本,包含了哪些暂时未被收录进”稳定版“的软件包|
|ubstable |不稳定版,开发版本|
Q:如此多的的tag我们又该如何选择呢?
答: 常规的应用其Docker镜像Tag往往是相互组合的版本+操作系统发行版本号+ea/slim
等关键字组合1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35# 版本号 openjdk:<version>
# Tag : openjdk:8
基础镜像为Debain上构建的openjdk镜像;
# stretch 关键字
# Tag : 8-jdk-stretch / 8-jre-stretch
其中的stretch表明这个镜像的操作系统是debian9在此基础上构建了Jdk8或者Jre8环境的docker镜像
# ea关键字
# Tag : 16-ea-jdk
其中的ea表示的意思是"Early Access"正是发布之前的预览版本,该版本带有新特性并且修复了若干bug,非Release版本不建议生产环境使用;
# alpine 关键字 openjdk:<version>-alpine
# Tag : 13-ea-19-jdk-alpine3.9
其中以alpine作为基础镜像构建出的openjdk镜像
# oraclelinux7关键字 openjdk:<version>-oraclelinux7
# Tag : 13-ea-oraclelinux7
其中的oraclelinux7表明镜像的操作系统是Oracle Linux 7,从jdk12开始openjdk官方开始提供基于Oracle Linux 7的jdk镜像;
# buster 关键字
# Tag : 15-jdk-buster
其中buster表明当前的是稳定的版本
# slim 关键字
# Tag : 15-jdk-slim
其中slim表明当前的jre并非标准jre版本而是headless版本,该版本的特点是去掉了UI、键盘、鼠标相关的库,因此更加精简适合服务端应用使用
# slim-buster 关键字
# Tag : 15-slim-buster
其中slim-buster表示当前镜像是精简稳定版本
# windowsservercore 关键字 openjdk:<version>-windowsservercore
# Tag : 15-windowsservercore-ltsc2016 #其大小超乎您的想象
基于Windows Server Core (microsoft/windowsservercore)。因此它只适用于镜像的位置,比如Windows 10 Professional/Enterprise(周年纪念版)或Windows Server 2016。
辅助工具
Docker使用辅助工具汇总
中找到它:
你好看友,欢迎关注博主微信公众号哟! ❤
这将是我持续更新文章的动力源泉,谢谢支持!(๑′ᴗ‵๑)
温馨提示: 未解锁的用户不能粘贴复制文章内容哟!
方式1.请访问本博主的B站【WeiyiGeek】首页关注UP主,
将自动随机获取解锁验证码。
Method 2.Please visit 【My Twitter】. There is an article verification code in the homepage.
方式3.扫一扫下方二维码,关注本站官方公众号
回复:验证码
将获取解锁(有效期7天)本站所有技术文章哟!
@WeiyiGeek - 为了能到远方,脚下的每一步都不能少
欢迎各位志同道合的朋友一起学习交流,如文章有误请在下方留下您宝贵的经验知识,个人邮箱地址【master#weiyigeek.top】
或者个人公众号【WeiyiGeek】
联系我。
更多文章来源于【WeiyiGeek Blog - 为了能到远方,脚下的每一步都不能少】, 个人首页地址( https://weiyigeek.top )
专栏书写不易,如果您觉得这个专栏还不错的,请给这篇专栏 【点个赞、投个币、收个藏、关个注、转个发、赞个助】,这将对我的肯定,我将持续整理发布更多优质原创文章!。
最后更新时间:
文章原始路径:_posts/虚拟云容/云容器/Docker/奇技淫巧/1.Docker容器镜像体积缩小技巧.md
转载注明出处,原文地址:https://blog.weiyigeek.top/2020/5-3-467.html
本站文章内容遵循 知识共享 署名 - 非商业性 - 相同方式共享 4.0 国际协议