Docker镜像创建方式

Docker 创建镜像主要有三种:

  1. 基于本地模板来导入;

  2. 基于已有的镜像创建;

  3. 基于 Dockerfile 来创建;

基于本地模板导入(了解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#通过导入操作系统模板文件可以生成镜像,模板可以从OPENVZ开源项目下载,下载地址为 http://openvz.org/Download/template/precreated
# 下载镜像
[root@localhost opt]# wget http://download.openvz.org/template/precreated/debian-7.0-x86-minimal.tar.gz
--2022-05-05 12:05:03-- http://download.openvz.org/template/precreated/debian-7.0-x86-minimal.tar.gz
正在解析主机 download.openvz.org (download.openvz.org)... 130.117.225.97
正在连接 download.openvz.org (download.openvz.org)|130.117.225.97|:80... 已连接。
已发出 HTTP 请求,正在等待回应... 200 OK
长度:88436521 (84M) [application/x-gzip]
正在保存至: “debian-7.0-x86-minimal.tar.gz”

100%[=================================================================================================================>] 88,436,521 1.32MB/s 用时 49s
2022-05-05 12:05:53 (1.70 MB/s) - 已保存 “debian-7.0-x86-minimal.tar.gz” [88436521/88436521])
# 导入
[root@localhost opt]# cat debian-7.0-x86-minimal.tar.gz | docker import - debian:1.0
sha256:dc9713284f84bcdab70c18baafb73c5327add3776dd1c175f92f48e416ee9a86
# 查看
[root@localhost opt]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
debian 1.0 dc9713284f84 19 seconds ago 215MB

基于已有的镜像创建(commit方式)

docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

常用参数

**-a :**提交的镜像作者;

**-c :**使用Dockerfile指令来创建镜像;

**-m :**提交时的说明文字;

**-p :**在commit时,将容器暂停。

我们所使用的一般都来自于 Docker Hub 的镜像。直接使用这些镜像是可以满足一定的需求,而当这些镜像无法直接满足需求时,我们就需要定制这些镜像。

镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。

自定义镜像我们一般使用Dockfile的方式,虽然commit方式也行,但是不推荐。

commit 可以方便我们理解镜像是如何构成的,下面我们以nginx为例,来介绍一下这个过程。

首先我们启动一个nginx容器:

1
2
[root@caojiarui0 ~]# docker run --name nginx -p 80:80 -d nginx 
ff968fb18a4c364f5334930eb192657e09763f47731ce9e4

此时我们就可以通过http://localhost去nginx首页了(服务器或虚拟机测试需要将localhost改为对应的ip地址)。下面我们修改一下这个首页的内容:

1
2
3
4
[root@caojiarui0 ~]# docker exec -it nginx bash
root@ff968fb18a4c:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@ff968fb18a4c:/# exit
exit

再次访问网页,我们可以看到如下内容:

image-20210819164302794

我们修改了容器的文件,也就是改动了容器的存储层。可以通过 docker diff 命令看到具体的改动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@caojiarui0 ~]# docker diff nginx
C /root
A /root/.bash_history
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
A /var/cache/nginx/client_temp
C /run
A /run/nginx.pid
C /etc
C /etc/nginx
C /etc/nginx/conf.d
C /etc/nginx/conf.d/default.conf
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html

现在我们定制好了变化,我们希望能将其保存下来形成镜像。

要知道,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。

docker commit 的语法格式为:

1
docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

我们可以用下面的命令将容器保存为镜像:

1
2
3
4
5
6
[root@caojiarui0 ~]# docker commit \
> --author "Cheng" \
> --message "修改了默认网页" \
> nginx \
> nginx:v2
sha256:d6728ce2859dbe154cfd35b8b174b8f2bfa378021440b2adfca

此时,我们就能通过docker image ls 查看到这个镜像了:

1
2
3
4
[root@caojiarui0 ~]# docker image ls nginx 
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx v2 d6728ce2859d 51 seconds ago 133MB
nginx latest dd34e67e3371 45 hours ago 133MB

我们还可以用 docker history 具体查看镜像内的历史记录,如果比较 docker history nginx:latest 的历史记录,会发现新增了我们刚刚提交的这一层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@caojiarui0 ~]# docker history nginx:v2
IMAGE CREATED CREATED BY SIZE COMMENT
d6728ce2859d About a minute ago nginx -g daemon off; 1.19kB 修改了默认网页
dd34e67e3371 45 hours ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
<missing> 45 hours ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
<missing> 45 hours ago /bin/sh -c #(nop) EXPOSE 80 0B
<missing> 45 hours ago /bin/sh -c #(nop) ENTRYPOINT ["/docker-entr… 0B
<missing> 45 hours ago /bin/sh -c #(nop) COPY file:09a214a3e07c919a… 4.61kB
<missing> 45 hours ago /bin/sh -c #(nop) COPY file:0fd5fca330dcd6a7… 1.04kB
<missing> 45 hours ago /bin/sh -c #(nop) COPY file:0b866ff3fc1ef5b0… 1.96kB
<missing> 45 hours ago /bin/sh -c #(nop) COPY file:65504f71f5855ca0… 1.2kB
<missing> 45 hours ago /bin/sh -c set -x && addgroup --system -… 63.9MB
<missing> 45 hours ago /bin/sh -c #(nop) ENV PKG_RELEASE=1~buster 0B
<missing> 45 hours ago /bin/sh -c #(nop) ENV NJS_VERSION=0.6.1 0B
<missing> 45 hours ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.21.1 0B
<missing> 45 hours ago /bin/sh -c #(nop) LABEL maintainer=NGINX Do… 0B
<missing> 2 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 days ago /bin/sh -c #(nop) ADD file:87b4e60fe3af680c6… 69.3MB

然后启动这个新的镜像,并访问http://localhost:81,可以发现nginx首页内容就是我们之前修改过的内容

1
docker run --name nginx2 -d -p 81:80 nginx:v2

所以我们使用 docker commit 命令来定制镜像是手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。

慎用 docker commit

使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。

首先,如果仔细观察之前的 docker diff webserver 的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html 文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,将会导致镜像极为臃肿。

此外,使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为 黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体的操作。这种黑箱镜像的维护工作是非常痛苦的。

而且,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。这会让镜像更加臃肿。

Dockerfile

Dockerfile简介

工作环境中的需求是不可控的,所以网络上的镜像有时候很难满足实际的需求,对于一些个性化的需求,我们一般需要个性化定制Docker镜像。又因为docker commit命令有一些缺陷,所以我们有必要了解如何使用当前主流的Dockerfile方式定制镜像。

Dockerfile其实就是我们用来构建Docker镜像的源码,当然这不是所谓的编程源码,而是一些命令和参数构成的脚本,只要理解它的逻辑和语法格式,就可以很容易的编写Dockerfile。

它的构建步骤如下:

1、编写Dockerfile文件

2、docker build 构建镜像

3、docker run 运行镜像

Dockerfile的基本结构

Dockerfile是一个包含用于组合映像的命令的文本文档。可以使用在命令行中调用任何命令。 Docker通过读取Dockerfile中的指令自动生成映像。

docker build命令用于从Dockerfile构建映像。可以在docker build命令中使用 -f 标志指向文件系统中任何位置的Dockerfile。

Dockerfile由一行行命令语句组成,并且支持以#开头的注释行Dockerfile分为四部分:基础镜像信息、维护者信息、 镜像操作指令和容器启动时执行指令

Dockerfile命令介绍

Docker以从上到下的顺序运行Dockerfile的指令。为了指定基本映像,第一条指令必须是FROM。一个声明以 # 字符开头则被视为注释。可以在Docker文件中使用 RUN , CMD , FROM , EXPOSE , ENV 等指令。

命令 说明
FROM 指定基础镜像,必须为第一个命令
MAINTAINER 维护者(作者)信息
ENV 设置环境变量
RUN 构建镜像时执行的命令
CMD 构建容器后调用,也就是在容器启动时才进行调用。
ENTRYPOINT 指定运行容器启动过程执行命令,覆盖CMD参数ENTRYPOINT与CMD非常类似,不同的是通过docker run执行的命令不会覆盖ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给ENTRYPOINT。Dockerfile中只允许有一个ENTRYPOINT命令,多指定时会覆盖前面的设置,而只执行最后的ENTRYPOINT指令。
ADD 将本地文件添加到容器中,tar类型文件会自动解压(网络压缩资源不会被解压),可以访问网络资源,类似wget
COPY 功能类似ADD,但是是不会自动解压文件,也不能访问网络资源
WORKDIR 工作目录,类似于cd命令
ARG 用于指定传递给构建运行时的变量
VOLUMN 用于指定持久化目录
EXPOSE 指定于外界交互的端口
USER 指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。使用USER指定用户时,可以使用用户名、UID或GID,或是两者的组合。当服务不需要管理员权限时,可以通过该命令指定运行用户。并且可以在之前创建所需要的用户

需要注意的几点

FROM命令

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。而 FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

在 Docker Store 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如nginx 、 redis 、 mongo 、mysql 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node 、 openjdk 、 python 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如ubuntu 、 debian 、 centos 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch 。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

1
2
FROM scratch
...

如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarm 、 coreos/etcd 。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go是特别适合容器微服务架构的语言的原因之一。

RUN 命令

RUN 指令是用来执行命令行命令的。由于命令行的强大能力, RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:

shell 格式: RUN <命令> ,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。

1
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

exec 格式: RUN [“可执行文件”, “参数1”, “参数2”],这更像是函数调用中的格式。
既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:

1
2
3
4
5
6
7
8
9
FROM debian:stretch

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install

之前说过,Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。

Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过127 层。

上面的 Dockerfile 正确的写法应该是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM debian:stretch

RUN set -x; buildDeps='gcc libc6-dev make wget' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
&& rm -rf /var/lib/apt/lists/* \
&& rm redis.tar.gz \
&& rm -r /usr/src/redis \
&& apt-get purge -y --auto-remove $buildDeps

首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN 一一对应不同的命令,而是仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

Dockerfile实战

Docker Hub里的nginx没有ping已经ip addr两个命令,下面我们用Dockerfile定制一个镜像,让其支持ping及ip addr的命令。

  1. 编写一个Dockerfile文件(无后缀名)
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
[root@localhost ~]# cd /opt/dockerfile/nginx
[root@localhost nginx]# ls
Dockerfile
[root@localhost nginx]# docker build -t cheng-nginx:1.0 .
Sending build context to Docker daemon 2.048kB
Step 1/5 : FROM nginx:latest
---> 670dcc86b69d
Step 2/5 : MAINTAINER cheng<chave_z@163.com>
---> Running in 20c3ab3f0da1
Removing intermediate container 20c3ab3f0da1
---> 2a913ad1a5fc
Step 3/5 : RUN apt-get update
---> Running in d2c54691ac48
Get:1 http://deb.debian.org/debian bullseye InRelease [116 kB]
---> 5ef39efc718a
Step 4/5 : RUN apt -y install iputils-ping
---> Running in 573723109c34
Step 5/5 : RUN apt -y install iproute2
---> Running in d021ada4b6b4
// ....
Successfully built a9450932c5c8
Successfully tagged cheng-nginx:1.0
[root@localhost nginx]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cheng-nginx 1.0 a9450932c5c8 9 seconds ago 162MB
nginx latest 670dcc86b69d 38 hours ago 142MB
mysql latest 33037edcac9b 8 days ago 444MB
tomcat latest 451d25ef4583 3 weeks ago 483MB

上面/opt/dockerfile/nginx的内容为:

1
2
3
4
5
6
7
8
9
# 文件内容如下
FROM nginx:latest

MAINTAINER cheng<chave_z@163.com>

RUN apt-get update
RUN apt -y install iputils-ping
# 安装 ip addr命令
RUN apt -y install iproute2

参考:

Docker 从入门到实践