第2次实践作业

利用commit理解镜像的构成

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

首先以一个nginx服务器为例子,看一下镜像大概是怎样构成的。

$ docker pull nginx
$ docker run --name webserver -d -p 8080:80 nginx

使用这两条命令先从docker hub中获取镜像,然后以nginx镜像为基础启动一个容器,命名为webserver,并且映射到8080端口。在浏览器上访问这个nginx服务器。

现在尝试修改nginx的主页,使用docker exec命令进入容器,并修改其内容。

$ docker exec -it webserver bash
root@d5da5a1d02d9:/# echo '<h1>Hello, FZU!</h1>' > /usr/share/nginx/html/index.html

我们以bash方式进入webserver容器,并使用\<h1>Hello, FZU!\</h1>覆盖了/usr/share/nginx/html/index.html的内容。再刷新浏览器,就能够看到主页内容已经被修改。

接着,通过docker diff命令可以看到我们对容器的存储层所做出的修改。

docker提供了 docker commit 命令,可以将容器的存储层保存,并制作成新的镜像。也就是在原有镜像的基础上,叠加上容器的存储层,构成新的镜像。语法为:

$ docker commit [option] <container ID or container name> [<Repository>[:<tag>]]

比如,保存上述nginx容器为新的镜像:

$ docker commit --author "czm <389214550@qq.com>" --message "change index" webserver nginx:v1

其中,--author 指定修改者,--message 记录修改的内容。使用 docker image ls可以看到该镜像。

还可以使用 docker history 查看镜像修改的历史记录。

成功保存镜像后,运行它,并在浏览器中观察效果。

$ docker run --name webserver2 -d -p 8090:80 nginx:v1

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

而且,由于镜像使用分层存储,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无需访问到,这会让镜像更加臃肿。使用docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用,因此,Dockerfile出现了。

使用Dockerfile修改镜像

镜像的定制实际上就是定制每一层所添加的配置、文件。我们可以利用Dockerfile把每一层修改、安装、构建、操作的命令都写入一个脚本文件,用这个脚本来构建、定制镜像。Dockerfile 内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

实现一个自定义的web容器服务

还以之前定制 nginx 镜像为例,这次我们使用 Dockerfile 来定制。首先需要知道的是nginx的配置文件是conf/nginx.conf文件,只要修改该配置文件,就能完成设定你自己的web存放目录,安全起见,请将默认的监听端口80更改为你自定义的端口的作业需求。我们先在本地修改该文件,再通过镜像构建上下文将该文件上传至docker引擎完成镜像定制。从nginx.conf文件中可知,nginx默认监听端口为80,默认web存放目录为html

...
listen       80;
server_name  localhost;
#charset koi8-r;
#access_log  logs/host.access.log  main;
location / {
	root   html;
	index  index.html index.htm;
}
...

假定本次实验的web存放目录为diyhtml,默认监听端口号为88,则只需要修改相应内容即可。

...
listen       88;
server_name  localhost;
#charset koi8-r;
#access_log  logs/host.access.log  main;
...

在一个空白目录中,建立一个文本文件,并命名为 Dockerfile

$ mkdir diynginx
$ touch Dockerfile

将nginx配置文件放在Dockerfile文件所在目录中。

由于在Dockerfile中进行配置镜像需要实现知道镜像的目录结构,所以我们进入镜像了解镜像的目录结构。

$ docker run -it nginx bash

启动并进入容器。然后使用命令:

$ whereis nginx

可以看到nginx在镜像中的位置。

面对多个目录我们似乎无从下手,但是还是有线索的。首先镜像是我们使用docker pull拉取下来的,那么在官方的docker hub中肯定会有对应于nginx的相关说明。我们打开docker hub中关于nginx的页面Official build of Nginx..

下拉到Complex configuration可以看到我们如果需要更改nginx相关配置信息,需要在/etc/nginx/nginx.conf中进行修改。

那么,我们得到第一句Dockerfile命令:COPY nginx.conf /etc/nginx/nginx.conf.到这里,我们完成了

请将默认的监听端口80更改为你自定义的端口需求。

由于新的web目录是我们自己自定义的,所以我们需要先创建一个我们自定义的目录。接下来的问题变成,nginx原来的web目录在哪里?我们可以用ls命令,将所有nginx目录列举一遍。

$ ls /usr/sbin/nginx 
$ ls /usr/lib/nginx
$ ls /etc/nginx 
$ ls /usr/share/nginx

可以看到只有/usr/share/nginx目录下有html文件。我们想到,在nginx.conf的文件中,原来的web目录就是html(root html;)显而易见,我们可以在/usr/share/nginx下建立我们自己web目录。因此Dockerfile的下一句代码就是RUN mkdir /usr/share/nginx/diyhtml .

同时需要在nginx.conf中修改root参数为/usr/share/nginx/diyhtml

...
listen       88;
server_name  localhost;
#charset koi8-r;
#access_log  logs/host.access.log  main;
location / {
	root   /usr/share/nginx/diyhtml;
	index  index.html index.htm;
}
...

为了验证我们的想法正确,我们需要有一个主页。看过了nginx.conf之后,我们知道,nginx的默认主页就是index.html,所以我们需要制作我们自己的主页。和制作nginx.conf一样,需要先制作好主页再将它上传至docker引擎。但是我这里为了方便,直接在Dockerfile中制作,并令主页只显示Hello,Nginx By You..所以,接下来的Dockerfile命令是:

touch /usr/share/nginx/diyhtml/index.html
echo '<h1>Hello,Nginx By You.</h1>' > /usr/share/nginx/diyhtml/index.html

根据需求容器启动时,能直接进入web代码的存放目录,因此我们使用WORKDIR 命令 指定工作目录为/usr/share/nginx/diyhtml。故需要在Dockerfile文件中添加命令:

WORKDIR /usr/share/nginx/diyhtml

因为我们修改了nginx镜像的监听端口,所以我们需要把新的端口告诉给docker,EXPOSE命令应运而生。我们Dockerfile的下一句命令就是:

EXPOSE 88

想要标明镜像作者信息,可以在FROM命令后面添加MAINTAINER auth <email>

综上所述,完整的Dockerfile内容为:

FROM nginx
MAINTAINER dockertrainee233 <389214550@qq.com>
COPY nginx.conf /etc/nginx/nginx.conf
RUN mkdir /usr/share/nginx/diyhtml \
	&& touch /usr/share/nginx/diyhtml/index.html \
	&& echo '<h1>Hello,Nginx By You.</h1>' > /usr/share/nginx/diyhtml/index.html
WORKDIR /usr/share/nginx/diyhtml
EXPOSE 88

完成Dockerfile文件的编写后,我们使用$ docker build来构建镜像。

$ docker build -t nginx:diy1 .

接下来我们以新定制的镜像新建一个容器并启动。

$ docker run -d -p 89:88 nginx:diy1

正常启动后,使用浏览器打开http://hostname:89/可以看到主页与之前设置的一样,并且端口是从docker容器的88端口映射到宿主机的89端口,由此可见web目录与端口均定制正确。

接下来,我们进入容器。使用命令:

$ docker exec -it containerID bash

可以看见直接进入我们自定义的web目录,查看index.html内容如下图。

到此,实现一个自定义的web容器服务小实验正确完成。

实现一个自定义的数据库容器服务

本次实验打算使用MySQL官方提供的最新镜像(MySQL:8.0.19),实验中涉及在环境变量中设置好数据库的root密码且不允许空密码登录,创建一个测试数据库,指定用户名和密码,那么先看看Official build of MySQL.中有什么环境变量吧。

图片中包含了部分MySQL支持的环境变量,想要观看更多,需要到MySQL Document中探索。以下是本次实验需要用到的环境变量:

MYSQL_ROOT_PASSWORD

强制设置MySQL中root超级用户的密码。

MYSQL_DATABASE

设置该环境变量后,在镜像启动时自动创建一个数据库。数据库名为该环境变量的key值。

MYSQL_USER, MYSQL_PASSWORD

为MYSQL_DATABASE环境变量创建的数据库添加一个拥有该数据库超级权限的用户。这两个环境变量需要同时连续的出现,分别为添加用户的用户名和口令。但是,这两个环境变量不是用来设置root用户的,因为root用户的密码设置有自己独立的环境变量。

MYSQL_ALLOW_EMPTY_PASSWORD

允许空密码登录则设置为yes,否则设置为no

了解完以上五个环境变量后,就能够写出相关的dockerfile命令了:

ENV MYSQL_ROOT_PASSWORD=123456 \ 
	MYSQL_ALLOW_EMPTY_PASSWORD=no \
	MYSQL_DATABASE=diydb \
	MYSQL_USER=trainee \
	MYSQL_PASSWORD=654321

这些命令的意思是:为root用户设置密码为123456;拒绝空密码登录;容器启动时创建名为diydb的数据库,并为该数据库添加一个登录名为trainee和密码为654321的超级权限用户。注意,该添加的用户只对diydb数据库拥有超级权限,后面能够验证这一点。

到此,作业的需求已经满足。如果此时想要在启动容器时,在创建的diydb数据库中自动建表并插入部分数据时,应该怎么处理呢?

分析此需求,能够将问题转为如何在容器启动时自动执行sql语句。同样的,当我们不知所措时,可以看看官方资料。我们来看看官方MySQL dockerfile中的玄机。

在该Dockerfile文件第70行开始:

70 COPY docker-entrypoint.sh /usr/local/bin/
71 RUN ln -s usr/local/bin/docker-entrypoint.sh /entrypoint.sh # backwards compat
72 ENTRYPOINT ["docker-entrypoint.sh"]

可以看到,它将上下文中的docker-entrypoint.sh复制到了/usr/local/bin/,执行了它。那么,我们再探索以下这个docker-entrypoint.sh。在该文件中177行开始:

可以看到,它将执行在/docker-entrypoint-initdb.d/下的所有sql文件。显而易见,我们只需要将我们编写的sql文件放入该目录即可。所以,在我们自己的dockerfile文件中添加如下代码:

COPY mytable.sql /docker-entrypoint-initdb.d

然后,按照自己的想法编写sql语句即可。比如,我的sql文件内容为:

use diydb;
create table student(
	id int(4) not null primary key,
	name char(8) not null
)DEFAULT CHARSET=latin1;
INSERT INTO student VALUES (1, 'Jimmy');

完整的dockerfile代码为:

FROM mysql
MAINTAINER dockertrainee233 <389214550@qq.com>
ENV MYSQL_ROOT_PASSWORD=123456 \ 
	MYSQL_ALLOW_EMPTY_PASSWORD=no \
	MYSQL_DATABASE=diydb \
	MYSQL_USER=trainee \
	MYSQL_PASSWORD=654321
COPY mytable.sql /docker-entrypoint-initdb.d
EXPOSE 3306

准备工作已经完成,那么我们尝试使用如下命令创建镜像吧。

$ docker build -t mysql:diy1

成功创建,我们以该镜像创建一个容器,启动并进入它。

$ docker run -d -p 3307:3306 mysql:diy1
$ docker exec --it containerID bash

在bash命令行中,使用命令:

mysql -uroot -p123456

登录MySQL。

可以看到,成功登录。接下来我们验证一下我们是否成功建立了名为diydb的数据库、数据库用户trainee、student表、以及student表中的数据。

使用命令select user,host from mysql.user;查看数据库中的用户:

成功创建用户trainee。使用命令show databases;查看MySQL中的所有数据库:

使用命令use diydb;进入diydb数据库,并使用命令show tables;查看数据库中存在的基本表:

使用命令desc tablename;可以查看基本表的基本结构:

使用命令select * from student;查看student表中的数据:

既然之前我们为diydb数据库创建了一个用户,那么我们登录它看看吧。

在trainee用户下,show databases;,并进入diydb看看:

可以看到,trainee用户只拥有diydb数据库的权限,和root是有差别的。

到此,可见定制MySQL镜像的思路完全正确。接下来使用下面的命令看看容器的IP地址信息吧。

$ docker inspect --format='{{.NetworkSettings.IPAddress}}' [NAME]/[CONTAINER ID]

实验中的Q&A

什么是镜像构建上下文?

如果注意,会看到 docker build 命令最后有一个 .. 表示当前目录,而 Dockerfile 就在当前目录,因此在刚开始时,我以为这个路径是在指定 Dockerfile 所在路径。在网上查询相关资料之后才知道这么理解其实是不准确的。资料中将这个.理解为是在指定上下文路径(context)。那么,什么是上下文呢?

首先需要理解 docker build 的工作原理。Docker 在运行时分为 Docker 引擎(服务端守护进程)和客户端工具(我们在命令行中输入相关命令进行交互)。Docker 引擎提供了一组 REST API,被称为 Docker Remote API。像 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。所以,表面上我们好像是在本机中执行各种 docker 功能,但实际上,一切都是使用远程过程调用(RPC)的形式在服务端(Docker 引擎)完成。也正是因为这种 C/S结构设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。

当我们使用Dockerfile进行定制镜像的时候,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令复制。而 docker build 命令构建镜像时,其实并非在本地构建,而是在Docker 引擎中进行构建的。那么在这种C/S的架构中,如何才能让服务端获得本地文件呢?

这就引入了上下文的概念。当构建镜像时,用户需要指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎就能收到构建镜像所需的一切文件。

如果在 Dockerfile 有这么一句命令:

COPY web.xml /webapps/

这并不是要复制执行 docker build 命令所在的目录下的 web.xml,也不是复制 Dockerfile 所在目录下的 web.xml,而是复制 上下文 目录下的 web.xml

因此,COPY 这类指令中的源文件的路径一般都是相对路径。现在就可以理解命令 docker build -t image:tag . 中的这个 .,实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些人将 Dockerfile 放到了硬盘根目录去构建,以便可以直接使用任何文件,结果发现 docker build 执行后,在发送一个几十 GB 的东西,这种做法是在让 docker build 打包整个硬盘,极为缓慢而且很容易构建失败。

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

posted @ 2020-04-22 20:52  rrmmoo  阅读(176)  评论(0编辑  收藏  举报