共计 18972 个字符,预计需要花费 48 分钟才能阅读完成。
大家好,我是张晋涛。
本周 Docker 就公布 10 周年了,为了庆贺这个里程碑,我将会公布一系列文章,波及 Docker,CI/CD, 容器等各个方面。
Docker 堪称是开启了容器化技术的新时代,当初无论大中小公司基本上都对容器化技术有不同水平的尝试,或是曾经进行了大量容器化的革新。随同着 Kubernetes 和 Cloud Native 等技术和理念的遍及,也大大增加了业务容器化需要。而这所有的推动,不可避免的技术之一便是构建容器镜像。
Docker 镜像是什么
在真正实际之前,咱们须要先搞明确几个问题:
- Docker 镜像是什么
- Docker 镜像的作用
- 容器和镜像的区别及分割
Docker 镜像是什么
这里,咱们以一个 Debian 零碎的镜像为例。通过 docker run --it debian
能够启动一个 debian
的容器,终端会有如下输入:
/ # docker run -it debian | |
Unable to find image 'debian:latest' locally | |
latest: Pulling from library/debian | |
c5e155d5a1d1: Pull complete | |
Digest: sha256:f81bf5a8b57d6aa1824e4edb9aea6bd5ef6240bcc7d86f303f197a2eb77c430f | |
Status: Downloaded newer image for debian:latest | |
root@860f21595fb6:/# cat /etc/os-release | |
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)" | |
NAME="Debian GNU/Linux" | |
VERSION_ID="11" | |
VERSION="11 (bullseye)" | |
VERSION_CODENAME=bullseye | |
ID=debian | |
HOME_URL="https://www.debian.org/" | |
SUPPORT_URL="https://www.debian.org/support" | |
BUG_REPORT_URL="https://bugs.debian.org/" |
看终端的日志,Docker CLI 首先会查找本地是否有 debian
的镜像,如果没有则从镜像仓库(若不指定,默认是 DockerHub)进行 pull;
将镜像 pull 到本地后,再以此镜像来启动容器。
咱们能够先退出此容器,来看看 Docker 镜像到底是什么。用 docker image ls
来查看已下载好的镜像:
(MoeLove) ➜ docker image ls debian | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
debian latest 72b624312240 2 weeks ago 124MB |
用 docker image save
命令将镜像保留成一个 tar 文件:
(MoeLove) ➜ mkdir debian-image | |
(MoeLove) ➜ docker image save -o debian-image/debian.tar debian | |
(MoeLove) ➜ ls debian-image/ | |
debian.tar |
将镜像文件进行解压:
(MoeLove) ➜ tar -C debian-image/ -xf debian-image/debian.tar | |
(MoeLove) ➜ tree -I debian.tar debian-image/ | |
debian-image/ | |
├── 72b6243122405be2c5c5e7e20d410f4c8fe301e1ce84cc60ea591b63167750e6.json | |
├── 7a66e59f40fd03d0e7bfaebe419af6a2c409ef8f513d037e3b1ebb8cbc803ec2 | |
│ ├── VERSION | |
│ ├── json | |
│ └── layer.tar | |
├── manifest.json | |
└── repositories | |
1 directory, 6 files |
能够看到将镜像文件解压后,蕴含的内容次要是一些配置文件和 tar 包。
接下来咱们来具体看看其中的内容,并通过这些内容来了解镜像的组成。
manifest.json
(MoeLove) ➜ cd debian-image/ | |
(MoeLove) ➜ cat manifest.json | jq | |
[ | |
{ | |
"Config": "72b6243122405be2c5c5e7e20d410f4c8fe301e1ce84cc60ea591b63167750e6.json", | |
"RepoTags": ["debian:latest"], | |
"Layers": ["7a66e59f40fd03d0e7bfaebe419af6a2c409ef8f513d037e3b1ebb8cbc803ec2/layer.tar"] | |
} | |
] |
留神:在理论存储时,是不蕴含换行的,这里为了便于展现所以应用了 jq
工具进行格式化。
manifest.json
蕴含了镜像的顶层配置,它是一系列配置按程序组织而成的;以当初咱们的 debian
镜像为例,它至蕴含了一组配置,这组配置中蕴含了 3 个次要的信息,咱们由简到繁进行阐明。
RepoTags
RepoTags
示意镜像的名称和 tag,这里简要的对此进行阐明:RepoTags
其实分为两局部:
Repo
: Docker 镜像能够存储在本地或者远端镜像仓库内,Repo 其实就是镜像的名称。Docker 默认提供了大量的官网镜像存储在 Docker Hub 上,对于咱们当初在用的这个 Docker 官网的 debian 镜像而言,残缺的存储模式其实是docker.io/library/debian
,只不过 docker 主动帮咱们省略掉了前缀。Tag
: 咱们能够通过repo:tag
的形式来援用一个镜像,默认状况下,如果没有指定 tag(像咱们下面操作的那样),则会 pull 下来最新的镜像(即:latest)
Config
Config
字段蕴含的内容是镜像的全局配置。咱们来看看具体内容:
(MoeLove) ➜ cat 72b6243122405be2c5c5e7e20d410f4c8fe301e1ce84cc60ea591b63167750e6.json | jq | |
{ | |
"architecture": "amd64", | |
"config": { | |
"Hostname": "","Domainname":"", | |
"User": "","AttachStdin": false,"AttachStdout": false,"AttachStderr": false,"Tty": false,"OpenStdin": false,"StdinOnce": false,"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd": ["bash"],"Image":"sha256:f8f185aa88c5b07710b327c1c8fd02c8d264bdcce11877d337b9d5c739015cea","Volumes": null,"WorkingDir":"", | |
"Entrypoint": null, | |
"OnBuild": null, | |
"Labels": null | |
}, | |
"container": "f41eadbc246cbece89086679da07f3b0d1508234aab4932acab7cbdc8ae63a9c", | |
"container_config": { | |
"Hostname": "f41eadbc246c", | |
"Domainname": "","User":"", | |
"AttachStdin": false, | |
"AttachStdout": false, | |
"AttachStderr": false, | |
"Tty": false, | |
"OpenStdin": false, | |
"StdinOnce": false, | |
"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"], | |
"Cmd": [ | |
"/bin/sh", | |
"-c", | |
"#(nop)", | |
"CMD [\"bash\"]" | |
], | |
"Image": "sha256:f8f185aa88c5b07710b327c1c8fd02c8d264bdcce11877d337b9d5c739015cea", | |
"Volumes": null, | |
"WorkingDir": "","Entrypoint": null,"OnBuild": null,"Labels": {}}, | |
"created": "2023-03-01T04:09:46.527045822Z", | |
"docker_version": "20.10.23", | |
"history": [ | |
{ | |
"created": "2023-03-01T04:09:45.982020208Z", | |
"created_by": "/bin/sh -c #(nop) ADD file:513c5d5e501279c21a05c1d8b66e5f0b02ee4b27f0b928706d92fd9ce11c1be6 in /" | |
}, | |
{ | |
"created": "2023-03-01T04:09:46.527045822Z", | |
"created_by": "/bin/sh -c #(nop) CMD [\"bash\"]", | |
"empty_layer": true | |
} | |
], | |
"os": "linux", | |
"rootfs": { | |
"type": "layers", | |
"diff_ids": ["sha256:cf2e8433dbf248a87d49abe6aa4368bb100969be2267db02015aa9c38d7225ed"] | |
} | |
} |
以上是配置文件的全部内容。其含意如下:
architecture
和os
: 示意架构及零碎不再开展;docker_version
: 构建镜像时所用 docker 的版本;created
:镜像构建实现的工夫;history
: 镜像构建的历史记录,前面内容中再具体介绍;rootfs
: 镜像的根文件系统;
重点介绍下 rootfs
:咱们晓得 rootfs
其实是指 /
下一系列文件目录的组织构造;尽管 Docker 容器与咱们的主机(或者称之为宿主机)共享同一个 Linux 内核,但它也有本人残缺的 rootfs
;
如果咱们应用 debian:latest
启动一个容器则能够看到如下内容:
/# tree -L 1 / | |
/ | |
|-- bin | |
|-- boot | |
|-- dev | |
|-- etc | |
|-- home | |
|-- lib | |
|-- lib64 | |
|-- media | |
|-- mnt | |
|-- opt | |
|-- proc | |
|-- root | |
|-- run | |
|-- sbin | |
|-- srv | |
|-- sys | |
|-- tmp | |
|-- usr | |
`-- var | |
19 directories, 0 files |
能够看到与咱们失常 Linux 零碎的 /
下目录雷同。
回到这个例子当中,咱们来看看这段配置的具体含意。因为一开始在 manifest.json
中曾经定义了 layer 的内容,咱们来看看该 layer 的 sha256sum
值:
(MoeLove) ➜ ls 7a66e59f40fd03d0e7bfaebe419af6a2c409ef8f513d037e3b1ebb8cbc803ec2 | |
VERSION json layer.tar | |
(MoeLove) ➜ sha256sum 7a66e59f40fd03d0e7bfaebe419af6a2c409ef8f513d037e3b1ebb8cbc803ec2/layer.tar | |
cf2e8433dbf248a87d49abe6aa4368bb100969be2267db02015aa9c38d7225ed 7a66e59f40fd03d0e7bfaebe419af6a2c409ef8f513d037e3b1ebb8cbc803ec2/layer.tar |
能够看到与 Config 字段配置文件中相符,示意 7a66e59f40fd03d0e7bfaebe419af6a2c409ef8f513d037e3b1ebb8cbc803ec2/layer.tar
便是 debian 镜像的 rootfs
咱们将它进行解压,看看它的内容。
(MoeLove) ➜ mkdir 7a66e59f40fd03d0e7bfaebe419af6a2c409ef8f513d037e3b1ebb8cbc803ec2/layer | |
(MoeLove) ➜ tar -C 7a66e59f40fd03d0e7bfaebe419af6a2c409ef8f513d037e3b1ebb8cbc803ec2/layer -xf 7a66e59f40fd03d0e7bfaebe419af6a2c409ef8f513d037e3b1ebb8cbc803ec2/layer.tar | |
(MoeLove) ➜ ls 7a66e59f40fd03d0e7bfaebe419af6a2c409ef8f513d037e3b1ebb8cbc803ec2/layer | |
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var |
能够看到它的内容的确是 rootfs
应该有的内容。同时,下面操作中也蕴含了一个知识点:
Docker 镜像相干的配置中,所用的 id 或者文件名 / 目录名大多是采纳 sha256sum 计算得出的
对于配置的局部咱们先谈这些,咱们持续看配置中尚未解释的 Layers
。
Layers
其实依据后面的介绍,咱们曾经大抵看到,Docker 镜像是分层的模式,将一系列层按程序组织起来加上配置文件等独特形成残缺的镜像。这样做的益处次要有:
- 雷同内容能够复用, 加重存储累赘;
- 能够比拟容易的失去各层所做操作 / 操作后后果的记录;
- 后续操作不影响前一层的内容;
通过 manifest.json
的内容,和后面对 rootfs
的解释,不难看出此镜像只蕴含了一层,即 7a66e59f40fd03d0e7bfaebe419af6a2c409ef8f513d037e3b1ebb8cbc803ec2/layer.tar
。
Docker 提供了一个命令能够更加直观的看到构建记录:
(MoeLove) ➜ docker image history debian | |
IMAGE CREATED CREATED BY SIZE COMMENT | |
72b624312240 2 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B | |
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:513c5d5e501279c21… 124MB |
它的输入相比咱们下面配置文件中的内容,多了一列 SIZE
,示意该构建步骤所占空间大小。能够看到第二步(输入是逆序的) /bin/sh -c #(nop) CMD ["bash"]
所占空间为 0。
咱们首先合成这些步骤所示意的内容:
/bin/sh -c #(nop) ADD file:caf91edab64f988bc…
: 应用ADD
命令增加文件;/bin/sh -c #(nop) CMD ["bash"]
:应用CMD
配置默认执行的程序是bash
;
从后面 Config
的配置中,咱们也能够看到第二步其实是批改了 Config
的配置,所以占用空间为 0,并没有使镜像变大。
从 Docker Hub 上咱们也能够找到此镜像的 Dockerfile
文件 https://github.com/debuerreotype/docker-debian-artifacts/blob/fe5738569aad49a97cf73183a8a6b2732fe57840/bullseye/Dockerfile,看下具体内容:
FROM scratch | |
ADD rootfs.tar.xz / | |
CMD ["bash"] |
步骤与咱们下面提到的完全符合, 不再进行开展了。
以上便具体解释了 Docker 镜像是什么:它其实是一组依照标准进行组织的分层文件,各层互不影响,并且每层的操作都将记录在 history
中。
Docker 镜像的作用
从后面的讲述中,咱们能够看到镜像中蕴含了一个残缺的 rootfs
,在咱们应用 docker run
命令时,便将指定镜像中的各层和配置组织起来独特启动一个新的容器;而在容器中,咱们能够随便进行操作(包含读写)。
所以 Docker 镜像的次要作用是:
- 为启动容器提供必要的文件;
- 记录了各层的操作和配置等;
容器和镜像的区别及分割
这里能够间接得出一个很直观的论断了。
镜像就是一系列文件和配置的组合,它是动态的,只读的,不可批改的。
而容器是镜像的实例化,它是可操作的,是动静的,可批改的。
Docker 镜像惯例治理操作
Docker 因为一直减少新性能,为了不便,在后续版本中便对命令进行了分组。对镜像相干的命令都放到了 docker image
组内:
(MoeLove) ➜ docker image | |
Usage: docker image COMMAND | |
Manage images | |
Commands: | |
build Build an image from a Dockerfile | |
history Show the history of an image | |
import Import the contents from a tarball to create a filesystem image | |
inspect Display detailed information on one or more images | |
load Load an image from a tar archive or STDIN | |
ls List images | |
prune Remove unused images | |
pull Download an image from a registry | |
push Upload an image to a registry | |
rm Remove one or more images | |
save Save one or more images to a tar archive (streamed to STDOUT by default) | |
tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE | |
Run 'docker image COMMAND --help' for more information on a command. |
对于咱们开始时对镜像进行剖析的操作,咱们能够间接通过 docker image inspect debian
间接拿到它的配置信息。
pull
, push
, tag
这三个子命令与和镜像仓库的交互比拟相干,能够联合后面 RepoTags
了解。
save
和 load
是将镜像保留到文件系统上及从文件系统中导入 Docker 中。
build
命令会在接下来具体阐明,残余命令都比较简单直观了。
如何构建 Docker 镜像
后面具体讲述了 Docker 镜像是什么,以及简略介绍了罕用的 Docker 镜像治理命令。那如何构建一个 Docker 镜像呢?通常状况下,有两种方法能够用于构建镜像(但并不只有这两种方法,后续再写文章来独自讲 flag++)
从容器创立
还是以 debian 镜像为例,应用官网的 debian 镜像,启动一个容器:
(MoeLove) ➜ docker run --rm -it debian | |
root@642741c96f0c:/# toilet | |
bash: toilet: command not found |
容器启动后,咱们输出 toilet
来查看以后是否有 toilet
这个命令。这是一个能将输出的字符串以更大的文本输入的命令行工具。
看下面的输出,以后的 PATH 中并没有该命令。咱们应用 apt
进行装置。
root@642741c96f0c:/# apt-get update -qq && apt-get install toilet -y -qq | |
debconf: delaying package configuration, since apt-utils is not installed | |
Selecting previously unselected package libncursesw6:amd64. | |
(Reading database ... 6661 files and directories currently installed.) | |
Preparing to unpack .../0-libncursesw6_6.2+20201114-2_amd64.deb ... | |
Unpacking libncursesw6:amd64 (6.2+20201114-2) ... | |
Selecting previously unselected package libslang2:amd64. | |
Preparing to unpack .../1-libslang2_2.3.2-5_amd64.deb ... | |
Unpacking libslang2:amd64 (2.3.2-5) ... | |
Selecting previously unselected package libcaca0:amd64. | |
Preparing to unpack .../2-libcaca0_0.99.beta19-2.2_amd64.deb ... | |
Unpacking libcaca0:amd64 (0.99.beta19-2.2) ... | |
Selecting previously unselected package libgpm2:amd64. | |
Preparing to unpack .../3-libgpm2_1.20.7-8_amd64.deb ... | |
Unpacking libgpm2:amd64 (1.20.7-8) ... | |
Selecting previously unselected package toilet-fonts. | |
Preparing to unpack .../4-toilet-fonts_0.3-1.3_all.deb ... | |
Unpacking toilet-fonts (0.3-1.3) ... | |
Selecting previously unselected package toilet. | |
Preparing to unpack .../5-toilet_0.3-1.3_amd64.deb ... | |
Unpacking toilet (0.3-1.3) ... | |
Setting up toilet-fonts (0.3-1.3) ... | |
Setting up libgpm2:amd64 (1.20.7-8) ... | |
Setting up libslang2:amd64 (2.3.2-5) ... | |
Setting up libncursesw6:amd64 (6.2+20201114-2) ... | |
Setting up libcaca0:amd64 (0.99.beta19-2.2) ... | |
Setting up toilet (0.3-1.3) ... | |
update-alternatives: using /usr/bin/figlet-toilet to provide /usr/bin/figlet (figlet) in auto mode | |
Processing triggers for libc-bin (2.31-13+deb11u5) ... |
能够看到,装置曾经实现,咱们在终端下输出 toilet MoeLove
来查看下成果:
root@642741c96f0c:/# toilet MoeLove | |
m m m | |
## ## mmm mmm # mmm m m mmm | |
# ## # #""# #" # # #""#"m m"#" # | |
# ""# # # #"""" # # # #m# #""""# #"#m#""#mm" #mmmmm "#m#" # "#mm" |
该命令曾经装置实现,并工作良好。当初咱们应用以后容器来创立一个蕴含 toilet
命令的 Docker 镜像。
Docker 提供了一个命令 docker container commit
用于从容器创立一个镜像。
(MoeLove) ➜ docker ps | |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES | |
642741c96f0c debian "bash" 2 minutes ago Up 2 minutes exciting_wu | |
(MoeLove) ➜ | |
(MoeLove) ➜ docker container commit -m "install toilet" 642741c96f0c local/debian:toilet | |
sha256:214051a092243edfbeb0c6ef8855646aac404425eb81d44c2bce5260b2bc5ce4 | |
(MoeLove) ➜ docker image ls local/debian:toilet | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
local/debian toilet 214051a09224 7 seconds ago 146MB |
间接将以后容器的 ID 传递给 docker container commit
作为参数,并提供一个新的镜像名称便可创立一个新的镜像(传递名称是为了方便使用,即便不传递名称也能够创立镜像)
应用新的镜像来启动一个容器进行验证:
(MoeLove) ➜ docker run --rm -it local/debian:toilet | |
root@9968f2a887f1:/# toilet debian | |
# # " | |
mmm# mmm #mmm mmm mmm m mm | |
#""# #" # #""# #" # #" # | |
# # #""""# # # m"""# # # | |
"#m##"#mm"##m#" mm#mm "mm"# # # | |
能够看到 toilet
曾经存在。从容器创立镜像的目标达成。
从 Dockerfile 创立
Docker 提供了一种可依据配置文件构建镜像的形式,该配置文件通常命名为 Dockerfile
。咱们将方才创立镜像的过程以 Dockerfile 进行形容。
/ # mkdir toilet | |
/ # cd toilet/ | |
/toilet # vi Dockerfile | |
/toilet # cat Dockerfile | |
FROM debian | |
RUN apt-get update -qq && apt-get install toilet -y -qq |
Dockerfile 语法是固定的,但本篇不会对全副语法一一解释,如有趣味可查阅官网文档。接下来应用该 Dockerfile 构建镜像。
(MoeLove) ➜ docker image build -t local/debian:toilet-using-dockerfile . | |
[+] Building 4.6s (6/6) FINISHED | |
=> [internal] load build definition from Dockerfile 0.0s | |
=> => transferring dockerfile: 106B 0.0s | |
=> [internal] load .dockerignore 0.0s | |
=> => transferring context: 2B 0.0s | |
=> [internal] load metadata for docker.io/library/debian:latest 0.0s | |
=> [1/2] FROM docker.io/library/debian 0.0s | |
=> [2/2] RUN apt-get update -qq && apt-get install toilet -y -qq 4.1s | |
=> exporting to image 0.5s | |
=> => exporting layers 0.5s | |
=> => writing image sha256:247bdcfbeb4dd0ef62732040edd3de36b72aa46f8f0392462db1a82276bb23db 0.0s | |
=> => naming to docker.io/local/debian:toilet-using-dockerfile 0.0s | |
(MoeLove) ➜ docker image ls local/debian | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
local/debian toilet-using-dockerfile 247bdcfbeb4d 30 seconds ago 146MB | |
local/debian toilet 214051a09224 4 minutes ago 146MB |
应用 -t
参数来指定新生成镜像的名称,并且咱们也能够看到该镜像曾经构建胜利。同样的应用该镜像创立容器进行测试:
/toilet # docker run --rm -it local/debian:toilet-using-dockerfile | |
root@d4f191b8d653:/# toilet debian | |
# # " | |
mmm# mmm #mmm mmm mmm m mm | |
#""# #" # #""# #" # #" # | |
# # #""""# # # m"""# # # | |
"#m##"#mm"##m#" mm#mm "mm"# # # | |
也都验证胜利。如果你反复执行 docker build
命令的话,会看到有 cache
字样的输入,这是因为 Docker 为了进步构建镜像的效率,对曾经构建过的每层进行了缓存,前面的内容会再讲到缓存相干的内容。
以上便是两种最常见构建容器镜像的办法了。其余方法之后写文章独自再聊。
逐渐合成构建 Docker 镜像的最佳实际
从容器构建 VS 从 Dockerfile 构建
通过下面的介绍也能够看到,从容器构建很简略很间接,从 Dockerfile 构建则须要你形容进去每一步所做内容。
然而,如果对构建过程会有批改,或者是想要可保护,可记录,可追溯,那还是抉择 Dockerfile 更为失当。
以一个 Spring Boot 的我的项目为例
(MoeLove) ➜ spring-boot-hello-world git:(master) ✗ ls -l | |
总用量 20 | |
-rw-rw-r--. 1 tao tao 0 3 月 15 06:52 Dockerfile | |
drwxrwxr-x. 2 tao tao 4096 3 月 15 06:54 docs | |
-rw-rw-r--. 1 tao tao 1992 3 月 15 06:33 pom.xml | |
-rw-rw-r--. 1 tao tao 89 3 月 15 06:50 README.md | |
drwxrwxr-x. 4 tao tao 4096 3 月 15 06:33 src | |
drwxrwxr-x. 9 tao tao 4096 3 月 15 06:52 target |
这里尽管以 Spring Boot 我的项目为例,但你如果对 Spring Boot 不相熟的话也齐全不影响后续内容,这里并不波及 Spring Boot 的任何常识。你只须要晓得对于这个我的项目而言,须要先装依赖,构建,能力运行。
那咱们来看看个别状况下,对于这样的我的项目 Dockerfile
的内容是什么样的。
利用缓存
FROM debian | |
COPY . /app | |
RUN apt update | |
RUN apt install -y openjdk-17-jdk | |
CMD ["java", "-jar", "/app/target/gs-spring-boot-0.1.0.jar"] |
这是一种比拟典型的,在本地先构建好之后,再复制到容器镜像中。留神,因为 debian
镜像默认没有 Java 环境,所以还须要有 apt
/apt-get
来装置 Java 环境。
那这样的 Dockerfile
有问题吗?有。
后面咱们提到了,如果你对同样内容的 Dockerfile
执行两次 docker build
命令的话,会看到有 cache
字样的输入,这是因为 Docker 的 build 零碎内置了缓存的逻辑,在构建时,会查看以后要构建的内容是否曾经被缓存,如果被缓存则间接应用,否则从新构建,并且后续的缓存也将生效。
对于一个失常的我的项目而言,源代码的更新是最为频繁的。所以看下面的 Dockerfile
你会发现 COPY . /app
这一行,很容易就会让缓存生效,从而导致前面的缓存也都生效。
对此 Dockerfile
进行改良:
FROM debian | |
RUN apt update | |
RUN apt install -y openjdk-17-jdk | |
COPY . /app | |
CMD ["java", "-jar", "/app/target/gs-spring-boot-0.1.0.jar"] |
第一个实际指南:为了更无效的利用构建缓存,将更新最频繁的步骤放在最初面 这样在之后的构建中,前三步都能够利用缓存。你能够运行屡次 docker build
以进行验证。
局部拷贝
在我的项目变大,或者是我的项目中其余目录,比方 docs
目录内容很大时,依据后面对镜像相干的阐明,间接应用 COPY . /app
会把所有内容拷贝至镜像中,导致镜像变大。
而对于咱们要构建的镜像而言,那些文件是不必要的,所以咱们能够将 Dockerfile
改成这样:
FROM debian | |
RUN apt update | |
RUN apt install -y openjdk-17-jdk | |
COPY target/gs-spring-boot-0.1.0.jar /app/ | |
CMD ["java", "-jar", "/app/gs-spring-boot-0.1.0.jar"] |
第二个实际指南:防止将全部内容拷贝至镜像中, 至保留须要的内容即可。当然除去批改 Dockerfile
文件外,也能够通过批改 .dockerignore
文件来实现相似的事件。
docker build
的过程是先加载 .dockerignore
文件,而后才依照 Dockerfile
进行构建,.dockerignore
的用法与 .gitignore
相似,排除掉你不想要的文件即可。
避免包缓存过期
下面咱们曾经提到了,docker build
能够利用缓存,但你有没有思考到,如果应用咱们后面的 Dockerfile
,当你机器上须要构建多个不同我的项目的镜像,或者是须要装置的依赖发生变化的时候,缓存可能就不是咱们想要的了。
比如说,我想装置一个最新版的 vim
在镜像中,能够简略的批改第三行为 RUN apt install -y openjdk-17-jdk vim
,但因为 RUN apt update
是被缓存的,所以我无奈装置到最新版本的 vim
。
FROM debian | |
RUN apt update && apt install -y openjdk-17-jdk | |
COPY target/gs-spring-boot-0.1.0.jar /app/ | |
CMD ["java", "-jar", "/app/gs-spring-boot-0.1.0.jar"] |
第三个实际指南:将包管理器的缓存生成与安装包的命令写到一起可避免包缓存过期
审慎应用包管理器
理解 apt
/apt-get
的敌人应该晓得,在应用 apt
/apt-get
安装包的时候,它会主动减少一些举荐装置的包,并且一起下载。但那些包对咱们镜像中跑应用程序而言无关紧要。它有一个 --no-install-recommends
的选项能够防止装置那些举荐的包。
咱们先来看下是否应用此选项的区别,我启动一个 debian
的容器进行测试:
root@5a23eb858163:/# apt install --no-install-recommends openjdk-17-jdk | grep 'additional disk space will be used' | |
... | |
After this operation, 344 MB of additional disk space will be used. | |
^C | |
root@5a23eb858163:/# apt install openjdk-17-jdk | grep 'additional disk space will be used' | |
... | |
After this operation, 548 MB of additional disk space will be used. | |
^C |
能够看到如果减少了 --no-install-recommends
选项的话,能够缩小 200M 左右磁盘占用。
所以 Dockerfile
能够批改为:
FROM debian | |
RUN apt update && apt install -y --no-install-recommends openjdk-17-jdk | |
COPY target/gs-spring-boot-0.1.0.jar /app/ | |
CMD ["java", "-jar", "/app/gs-spring-boot-0.1.0.jar"] |
此时构建镜像,咱们来与之前的镜像做下比照:
(MoeLove) ➜ docker image ls local/spring-boot | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
local/spring-boot 4 716523c83a26 3 minutes ago 497MB | |
local/spring-boot 2 178dacdaf015 9 hours ago 600MB |
能够很显著看到镜像显著变小了。
接下来还有个值得注意的中央。咱们一开始执行了 apt update
这个命令,它次要是在缓存源信息。而对于咱们构建所需镜像时,这没有必要。咱们抉择将这些缓存文件删掉。
启动一个新的容器验证下:
(MoeLove) ➜ docker run --rm -it debian | |
root@cd857c3ab882:/# apt -qq update | |
All packages are up to date. | |
root@cd857c3ab882:/# du -sh /var/lib/apt/lists/ | |
16M /var/lib/apt/lists/ | |
root@cd857c3ab882:/# |
能够看到有 16M 左右的大小,咱们批改 Dockerfile
减少删除操作:
FROM debian | |
RUN apt update && apt install -y --no-install-recommends openjdk-17-jdk \ | |
&& rm -rf /var/lib/apt/lists/* | |
COPY target/gs-spring-boot-0.1.0.jar /app/ | |
CMD ["java", "-jar", "/app/gs-spring-boot-0.1.0.jar"] |
比照应用这个 Dockerfile
构建镜像的镜像大小
(MoeLove) ➜ docker image ls local/spring-boot | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
local/spring-boot 4-2 ac272f3dcac2 24 seconds ago 481MB | |
local/spring-boot 4 716523c83a26 37 minutes ago 497MB | |
local/spring-boot 2 178dacdaf015 10 hours ago 600MB |
能够看到小了 16M 左右。
第四个实际指南:审慎应用包管理器,不装置非必要的包,留神清理包管理器缓存文件
抉择适合的根底镜像
Docker Hub 上提供了很多 官网镜像 这些镜像的构建基本上都通过了大量的优化,尽可能放大镜像体积,缩小镜像层数。
当咱们构建镜像的时候,无妨先查看官网镜像是否有满足需要的镜像能够作为根底镜像。Java 运行环境官网镜像是有提前提供的 openjdk 咱们能够在 GitHub 上找到它构建镜像的 Dockerfile 能够看到其中的一些构建过程与咱们后面所说的实际形式相符。
咱们抉择 Docker 官网 openjdk
镜像来作为根底镜像,Dockerfile
能够改写为:
FROM openjdk:17-jdk-bullseye | |
COPY target/gs-spring-boot-0.1.0.jar /app/ | |
CMD ["java", "-jar", "/app/gs-spring-boot-0.1.0.jar"] |
openjdk
有很多不同的 tag 比方 8-jdk-stretch
8-jre-stretch
以及 8-jre-alpine
之类的,具体的能够在 openjdk 的 tag 页面查看。
咱们其实只想要一个 Java 的运行环境,所以能够抉择一个体积绝对较小的镜像 openjdk:17-jdk-slim-bullseye
这样 Dockerfile
能够改写为:
FROM openjdk:17-jdk-slim-bullseye | |
COPY target/gs-spring-boot-0.1.0.jar /app/ | |
CMD ["java", "-jar", "/app/gs-spring-boot-0.1.0.jar"] |
别离用下面的 Dockerfile
构建镜像,能够看到镜像大小
(MoeLove) ➜ docker image ls local/spring-boot | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
local/spring-boot 5-1 b423dfc8d995 23 minutes ago 303MB | |
local/spring-boot 5 7158d42a6a87 25 minutes ago 643MB | |
local/spring-boot 4-2 ac272f3dcac2 4 hours ago 481MB | |
local/spring-boot 4 716523c83a26 5 hours ago 497MB | |
local/spring-boot 2 178dacdaf015 14 hours ago 600MB |
很显著,应用 openjdk:17-jdk-slim-bullseye
后,镜像大小只有 303M 比之前的镜像小了很多。
第五个实际指南:尽可能抉择官网镜像,看理论需要进行最终抉择 这样说的起因,次要是因为有些镜像是基于 Alpine Linux 的,Alpine 并非基于 glibc 的,而是基于 musl 的,如果是 Python 的我的项目,请理论测试下性能损失再决定是否抉择 Alpine Linux(这里是我做的一份对于 Python 各镜像次要的性能比照,有须要能够参考)
放弃构建环境统一
在后面的实际中,咱们都是先本地构建好之后,才 COPY
进去的,这容易导致不同用户构建出的镜像可能不同。所以咱们将构建过程写入到 Dockerfile
:
FROM maven:3.8.7-openjdk-18-slim | |
WORKDIR /app | |
COPY pom.xml /app/ | |
COPY src /app/src | |
RUN mvn -e -B package | |
CMD ["java", "-jar", "/app/target/gs-spring-boot-0.1.0.jar"] |
这样所有人都能够应用雷同的 Dockerfile
构建出雷同的镜像了。
但咱们也会发现一个问题,在 mvn -e -B package
这一步消耗的工夫特地长,因为它须要先拉取依赖能力进行构建。而对于我的项目开发而言,代码变更比依赖变更更加频繁,为了能放慢构建速度,无效的利用缓存,咱们将解决依赖与构建分成两步。
FROM maven:3.8.7-openjdk-18-slim | |
WORKDIR /app | |
COPY pom.xml /app/ | |
RUN mvn dependency:go-offline | |
COPY src /app/src | |
RUN mvn -e -B package | |
CMD ["java", "-jar", "/app/target/gs-spring-boot-0.1.0.jar"] |
这样,即便业务代码产生扭转,也不须要从新解决依赖,可无效的利用了缓存,放慢构建的速度。
当然,当初咱们构建的镜像中,还是蕴含着我的项目的源代码,这其实并非咱们所须要的。那么咱们能够应用 多阶段构建 来解决这个问题。Dockerfile
能够批改为:
FROM maven:3.8.7-openjdk-18-slim AS builder | |
WORKDIR /app | |
COPY pom.xml /app/ | |
RUN mvn dependency:go-offline | |
COPY src /app/src | |
RUN mvn -e -B package | |
FROM openjdk:17-jdk-slim-bullseye | |
COPY --from=builder /app/target/gs-spring-boot-0.1.0.jar / | |
CMD ["java", "-jar", "/gs-spring-boot-0.1.0.jar"] |
当然,多阶段构建也并不只是为了放大镜像体积;咱们能够应用指定构建阶段,以满足多种不同的镜像需要。
Dockerfile
能够批改为:
FROM maven:3.8.7-openjdk-18-slim AS builder | |
WORKDIR /app | |
COPY pom.xml /app/ | |
RUN mvn dependency:go-offline | |
COPY src /app/src | |
RUN mvn -e -B package | |
FROM builder AS dev | |
RUN apt-get update -y && apt-get install -y vim | |
FROM openjdk:17-jdk-slim-bullseye | |
COPY --from=builder /app/target/gs-spring-boot-0.1.0.jar / | |
CMD ["java", "-jar", "/gs-spring-boot-0.1.0.jar"] |
咱们能够应用如下的命令来构建不同阶段的镜像;
# 构建用于开发的镜像 | |
(MoeLove) ➜ docker build --target dev -t local/spring-boot:6-4-dev . | |
# 构建用于生产部署的镜像 | |
(MoeLove) ➜ docker build -t local/spring-boot:6-4 . |
咱们来看看在这个过程中镜像大小的变动:
(MoeLove) ➜ docker image ls local/spring-boot | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
local/spring-boot 6-4-dev f47a322c9de3 6 seconds ago 450MB | |
local/spring-boot 6-4 2ab6215ff05e 3 minutes ago 303MB | |
local/spring-boot 6-3 2ab6215ff05e 3 minutes ago 303MB | |
local/spring-boot 6-2 2b3d3f923e05 4 minutes ago 325MB | |
local/spring-boot 6 f96bea38825f 2 hours ago 388MB |
第六个实际指南:能够利用多阶段构建放弃构建和运行环境的统一 ,也能够利用多阶段构建来管制构建的指标阶段。
这对于保护绝对大型的我的项目是十分有帮忙的,比方 Docker 我的项目本身的 Dockerfile 就充沛的利用了多阶段构建的个性。
如何晋升构建效率
在构建 Docker 镜像的最佳实际局部中,咱们提到了很多办法,比方利用缓存;缩小装置依赖等,这些都能够晋升构建效率。
咱们还提到了多阶段构建,这是一种很不便而且很灵便的形式。但多阶段构建,在默认状况下是程序构建;
对于 18.09+ 版本,能够通过配置启动 Buildkit。对于新版本 v23.0.0 及 Docker Desktop 中都默认启用了 Buildkit。
我在之前的文章 万字长文:彻底搞懂容器镜像构建 | MoeLove
中也介绍了 Buildkit 和 Docker 原有的 builder 的区别及分割。
除此之外,还有很多其余的伎俩能够用于晋升镜像构建,或者说 CI/CD pipeline 的效率,我会在后续文章中持续分享相干的教训。
总结
本文深刻介绍了 Docker 镜像是什么,容器和镜像的区别,如何构建镜像,以及 6 个构建镜像的最佳实际。
事实上对于 Docker 镜像构建在生产环境中的利用,我还有很多教训能够分享,
咱们下篇文章见!
欢送订阅我的文章公众号【MoeLove】