大家好,我是张晋涛。
本周 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
文件 github.com/debuerreoty… ,看下详细内容:
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】