第三篇:Docker镜像制作

在制作镜像时我们会发现 dockerfile 镜像的第一行,都是 FROM 一个镜像名称,到目前为止这里的镜像一般都是官方镜像或者别人制作的镜像,虽然在大多数情况下这已经可以满足我们的需求,但是如果我们需要在一个可控的操作系统上做一些事情(比如我们的JDK需要按照公司标准安装在固定目录),这时自己制作一个基本的 Docker 基础镜像将会非常有必要。本节会主要介绍镜像制作的两种主要方式。镜像制作完毕后我们还会对镜像做压缩以方便交付和传递,本节在结束时也会介绍镜像的压缩的一些技巧以及镜像打包的方法。

通过 docker commit 的方式构建镜像

经第一节的介绍我们知道,Docker 镜像是分层存储的,每一层都是在上一层的基础上做了一些修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。如下:

docker镜像分层文件系统

docker容器分层文件系统

现在让我们以定制一个 Web 服务器为例子,来讲解镜像是如何构建的。

docker run --name web -d -p 80:80 nginx

这条命令会用 nginx 镜像启动一个容器,命名为 web,并且映射了 80 端口,这样我们可以用浏览器去访问这个 nginx 服务器。这时我们就会看到默认的 Nginx 欢迎页面。

假设我们需要更改这个欢迎页面的文案为"Hello WEATHER!",这时就可以使用 docker exec 命令进入容器,修改其内容。

$ docker exec -it web bash
root@124f4168be89:/# echo '<h1>Hello WEATHER!</h1>' > /usr/share/nginx/html/index.html
root@124f4168be89:/# exit

我们以交互式终端方式进入 web 容器,并执行了 bash 命令打开命令行终端。

然后,我们用 <h1>Hello WEATHER!</h1> 覆盖了 /usr/share/nginx/html/index.html 的内容。

现在我们再刷新浏览器的话,会发现内容被改变了。

整个过程,我们通过修改容器中的文件来改变了改动了容器的存储层。我们可以通过 docker diff 命令看到具体的改动。

$ docker diff web
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
C /root
A /root/.bash_history
C /run
A /run/nginx.pid
A /run/secrets
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp

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

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

docker commit -m "update ngxin index.html" web weather-nginx:1.0.1

docker commit 的语法和 git commit的语法类似,不在赘述。

提交之后会返回提交镜像的摘要信息,这时执行 docker images 就可以看到我们新制作的镜像信息了:

[root@ctum2-vmo-214168059 userapp]# docker images
REPOSITORY                        TAG                 IMAGE ID            CREATED              SIZE
weather-nginx                         1.0.1               6070317fe28c        About a minute ago   108.5 MB

通过 docker history 指令可以查看镜像内做的改动记录(第一行带注释的为我们的改动记录)。

[root@ctum2-vmo-214168059 userapp]# docker history 6070317fe28c
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
6070317fe28c        4 minutes ago       nginx -g daemon off;                            168 B               update ngxin index.html
9e7424e5dbae        8 days ago          /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon    0 B
<missing>           8 days ago          /bin/sh -c #(nop)  STOPSIGNAL [SIGTERM]         0 B
<missing>           8 days ago          /bin/sh -c #(nop)  EXPOSE 80/tcp                0 B
<missing>           8 days ago          /bin/sh -c ln -sf /dev/stdout /var/log/nginx/   0 B
<missing>           8 days ago          /bin/sh -c set -x  && apt-get update  && apt-   53.22 MB
<missing>           8 days ago          /bin/sh -c #(nop)  ENV NJS_VERSION=1.13.7.0.1   0 B
<missing>           8 days ago          /bin/sh -c #(nop)  ENV NGINX_VERSION=1.13.7-1   0 B
<missing>           3 weeks ago         /bin/sh -c #(nop)  LABEL maintainer=NGINX Doc   0 B
<missing>           3 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0 B
<missing>           3 weeks ago         /bin/sh -c #(nop) ADD file:45233d6b5c9b91e943   55.25 MB

如果还需要其他一些操作,都可以使用相同的方式,在容器里修改完成后通过 docker commit 提交。至此我们已经完成了自己的镜像制作,我们可以将自己的镜像推送到 Registry 中方便后面使用。但是一般在实际中我们不会通过这种方式来生成镜像,因为这样容易使容器的层级过多,使容器过于臃肿。我们一般会利用下面 docker build 的方式来构建镜像,这可以合并一些操作,减少分层。

通过 docker build 的方式构建镜像

通过 docker build 方式构建镜像是我们制作镜像的主要方式,这种方式中主要的操作都会定义在 dockerfile 文件中,里面包含了制作镜像的一条条指令,通过这种配置的方式生成了镜像,这样更方便做历史记录和目标文件存档。

首先介绍下 dockerfile 的基本语法:

| 指令 | 指令用途 | | – | :- | |FROM| 基于哪个镜像 | |RUN| 在镜像内通过docker引擎执行指令 | |MANTAINER|镜像的创建和维护者信息 | |CMD| container 启动时执行的命令,一个 container 只能有一个 CMD 指令,多条的话只有最后一个生效 | |ENTRYPOINT| container 启动时执行的命令,一个 container 只能有一个 ENTRYPOINT 指令,多条的话只有最后一个生效| |USER|使用哪个用户跑container | |EXPOSE|开放端口,可以有多个。开放的端口需要在 run 时指定 host-container 端口映射,run时格式为 ”-p <主机IP:>主机端口:容器端口“| |ENV|设置环境变量 | |ADD|将文件 src 拷贝到 container 的文件系统对应的路径 dest,所有拷贝到 container 中的文件和文件夹权限为0755,uid和gid为0,如果文件是可识别的压缩格式,则docker会帮忙解压缩,注意要ADD本地文件必须在构建时的构建上下文中 | |COPY|同ADD,但是压缩包不会自动解压 | |VOLUME|将本地文件夹或者其他container的文件夹挂载到container中 | |WORKDIR|切换目录用,可以多次切换(相当于cd命令),对RUN,CMD,ENTRYPOINT生效 | |ONBUILD|ONBUILD 指定的命令在构建镜像时并不执行,而是在它的子镜像中执行 |

这其中有几个注意点:

ADD 和 COPY 指令的使用

它们的格式都是: 指令 src dest ,使用 ADD 时如果是压缩包,docker 引擎会自动把压缩包解压,但是 COPY 指令会保存源文件的格式。而且 COPY 的 src 不支持使用url,所以在使用 docker build – < somefile 时该指令不能使用。

容器指令入口

指令入口可以通过 ENTRYPOINT 或 CMD 来指定,它们都是在镜像运行时执行。

CMD 的语法有三种:① CMD [“executable”, “param1”, “param2”],将会调用exec执行,首选方式;②CMD [“param1”, “param2”],当使用ENTRYPOINT指令时,为该指令传递默认参数;③ CMD command [ param1|param2 ] 将会调用/bin/sh -c执行。 ENTRYPOINT 的语法有两种:① ENTRYPOINT [“executable”, “param1”, “param2”] ,将会调用exec执行,这是首选方式;② ENTRYPOINT command param1 param2,将会调用/bin/sh -c执行。

它们的区别是① CMD 指令指定的容器启动时命令可以被 docker run 指定的命令覆盖,而ENTRYPOINT指令指定的命令不能被覆盖,而是将 docker run指定的参数当做 ENTRYPOINT 指定命令的参数;② CMD指令可以为 ENTRYPOINT 指令设置默认参数,而且可以被 docker run 指定的参数覆盖。

所以通过 docker run -l 指令启动的容器会把 -l 参数传递给 ENTRYPOINT 指令定义的命令会覆盖 CMD 指令中定义的默认参数(如果有的话),但不会覆盖ENTRYPOINT定义的参数,可以通过这种方法来指定默认参数:ENTRYPOINT 指定入口定义,CMD 指定默认参数。在制作 weather-base 镜像时我们就使用了这个技巧,通过 ENTRYPOINT 设置了启动脚本,通过 CMD 传递进去要启动的应用名称。

构建上下文

docker build 的格式如下:

docker build [选项] <上下文路径/URL/>

通过 dockerfile 构建时会指定 .,这个代表当前文件夹,通过这个路径我们可以设置构建的上下文。当 Docker 执行构建的时候,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的,在这种客户端/服务端的架构中,为了让服务端获得本地文件,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

所以,一般会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

下面以制作我们的 WEATHER 基本 java 环境镜像为例说明这个过程:

# 基于原始操作系统镜像构建
FROM centos:7.2.1511

# 切换为root用户执行以下指令
USER root
# 设置环境变量值
ENV TAG 20171130
ENV LANG en_US.UTF-8
ENV LC_ALL en_US.UTF-8
ENV JAVA_HOME /usr/java/jdk1.8.0_112
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH .:$JAVA_HOME/lib:$JRE_HOME/lib
ENV PATH $PATH:$JAVA_HOME/bin

# 挂载文件卷
VOLUME /tmp

# 添加本地文件到镜像,注意一般dockerfile放在专有的文件夹下,里面不要放置无关的文件,也可以放置一个文件名为 .dockerignore 的文件,里面列出需要忽略的文件。ADD被忽略的文件时会报错退出。
ADD wanda-centos-7.repo /etc/yum.repos.d/wanda-centos-7.repo
ADD i18n /etc/sysconfig/i18n
ADD docker-entrypoint.sh /docker-entrypoint.sh

# 执行目录创建、包安装等指令
RUN set -x \
    && mkdir -p /opt/idc/apps \
    && mkdir -p /opt/idc/soft \
    && mkdir -p /etc/yum.repos.d/backup \
    && mv /etc/yum.repos.d/CentOS-*.repo /etc/yum.repos.d/backup \
    && yum update -y \
    && curl -SL http://10.214.124.68/linux-softwares/jdk-8u112-linux-x64.rpm > /opt/idc/soft/jdk-8u112-linux-x64.rpm \
    && rpm -ivh /opt/idc/soft/jdk-8u112-linux-x64.rpm \
    && rm -rf /opt/idc/soft

# 设置容器入口指令
ENTRYPOINT ["/docker-entrypoint.sh"]

然后执行docker build -t weather/weather-base:0.0.1 . 即可构建出我们通过 dockerfile 生成的第一个镜像。

镜像压缩

一般我们使用默认指令构建出来的镜像会比较大,这就不方便镜像的分发和传输,我们需要想办法来精简镜像。一般镜像精简有以下方法:

使用精简的基础镜像

选择比较小的基础镜像可以在一开始就让我们的镜像保持精简。

| 操作系统 | 大小 | | – | :- | |busybox:latest|1.129 MB| |alpine:latest|4.139 MB| |debian:latest|55 MB| |ubuntu:latest|122.8 MB| |centos:7.2.1511|194.6 MB|

但是这里为了方便我们从原系统迁移,使用了和虚拟机上一样的操作系统 centos 7.2.

减少镜像分层

因为在镜像中的每次操作都会生成新的分层,所以我们可以通过减少指令和简化指令的方式来减少精简镜像。常用的一个方法是将需要执行的指令合并,比如我们制作 weather-base 镜像时在 RUN 指令中将需要执行的指令合并在一起执行。

减少镜像文件

首先我们可以通过减少镜像上下文中文件数量和大小来减少镜像文件,通过上文中镜像上下文的分析我们知道在构建目录下的文件,除非列在.dockerignore 中,否则都会同步到 docker 引擎中,所以我们尽量只放置需要的文件或文件夹到构建目录。另外,在执行完指令时我们要尽量把不需要的文件或压缩包删除,同时删除操作尽量和放置文件的操作放在同一层(比如尽量使用 wget 方式来下载安装包,通过 rpm -ivh rpm网络链接的方式安装软件,使用后再删除安装包。在制作 weather-base 制作时使用的 jdk 包就是通过这样的方式做了精简),从而防止添加文件和删除文件不在同一层时添加层镜像并没有精简的问题(不在同一层时上层添加了文件后不删除的话文件就一直存在,另一层做删除时只是把另外层的对应文件标识为了删除,本质上并不会改变其它层的文件系统)。

压缩镜像分层

可以通过镜像打包的方式进行常规压缩,可以使用 export 和 import 指令来打包,比如:

docker run -d weather/weather-base:0.0.3
# 记录上一步的 container_id
docker export ${contianer_id} | docker import - weather/weather-base:1.0.0

但是通过以上指令导入导出有两个问题:首先必须先将容器运行起来才可以打包,另外,打包后,容器中的一些基本信息会丢失,比如 导出端口,环境变量,默认指令 等。所以我们只能借助于其他方法。

在 Docker 1.13 以上版本中,Docker 的构建指令自带 --squash 参数来启动构建时镜像压缩,如果我们不能使用这个指令,就需要考虑其他办法。一般我们可以使用压缩工具来做到这一点。在 Docker 1.10 版本以前,我们可以使用 Go 语言版本的 [docker-squash-1][docker-equash-1] 这个工具来完成这一操作,指令为:

docker save weather/weather-base:0.0.1 | docker-squash -verbose -t weather/weather-base:latest | docker load

以上指令代表将 weather-base:0.0.1 版本镜像压缩,并将新镜像打tag为 weather/weather-base:latest

但是上面的工具在 Docker 1.10 以上版本失效了,主要原因是 Docker 1.10 之后的版本 docker 镜像的分层逻辑做了调整,而工具的作者一直没有对应作调整,具体可参考 Github 上讨论。之后找到了一个 python 版本的 [docker-squash-2][docker-equash-2],这个可以比较好的支持 Docker 1.10 以上版本。安装和使用指令如下:

pip install docker-squash
docker-squash -t weather/weather-base:latest weather/weather-base:0.0.1

压缩完镜像后,我们就可以把这个镜像提交到 Registry 中了。

镜像打包和交付

镜像打包一般有两种方式:导出后再导入(export-import)、保存后再加载(save-load)。

导出后再导入(export-import) 基本操作如下:

docker run -d weather-base:1.0.0
# export 容器到压缩包,类似快照功能
docker export <CONTAINER ID> > /tmp/weather-base.tar
# 导入为镜像
cat /tmp/weather-base.tar | sudo docker import - weather-base:latest

保存后再加载(save-load) 基本操作如下:

# save 镜像到压缩包
docker save weather-base:1.0.0 > /tmp/weather-base.tar
# 导入为镜像
docker load < /tmp/weather-base.tar

他们的主要区别是:导出后再导入(exported-imported)的镜像会丢失所有的历史,而保存后再加载(saveed-loaded)的镜像没有丢失历史和层(layer)。这意味着使用导出后再导入的方式,你将无法回滚到之前的层(layer),同时,使用保存后再加载的方式持久化整个镜像,就可以做到层回滚(可以执行docker tag LAYER_ID IMAGE_NAME 来回滚之前的层,通过 docker images --tree可以查看分层 )。

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注