镜像里的内容是按「层」来组织的,「层」能够复用,一个完整的镜像也能够看作是一个「层」。多个「层」叠加在一块儿就造成了一个新的镜像,这个镜像也能够做为别的镜像的基础「层」进行更加复杂的镜像构建。下图展现了一个镜像的内部结构。java
这个目标镜像使用 Debian 镜像做为基础镜像开始构建,也就是说 Debian 镜像是目标镜像的第一「层」;往上的两层分别使用了 ADD
指令将 emacs
和 apache
添加到了目标镜像中,每个 ADD
指令都将产生新的一个「层」,最后这个目标镜像就是一个拥有三「层」的镜像。每新增一「层」时,将要生成的这一「层」镜像都会默认使用上一步构建出的「层」做为本身的基础镜像,上图中的箭头表示的就是这种引用关系。mysql
因此,「层」和「镜像」是等价的,当这一「层」之上没有其余「层」时,咱们就能够将这一「层」及其下面的全部「层」合起来称做一个「镜像」。若是这一「层」只是在构建镜像过程当中生成的一个「中间层」,即这一「层」不会被用来启动容器,那么就能够称做「层」。总的来讲,能用来启动容器的就称做「镜像」,其余都称做「层」。nginx
制做镜像的过程和在操做系统上安装软件的过程几乎是彻底同样的,惟一的区别是制做镜像须要使用 Dockerfile 文件来编写要执行的操做。请注意,Dockerfile 里的全部指令,除了 CMD
和 ENTRYPOINT
,都是给 Docker 引擎执行的,目的是制做出目标镜像,这些指令不是启动容器的时候执行的。sql
下面的例子将一步步演示从 0 开始制做一个在 CentOS7.2 操做系统上安装了 openjdk
和 nginx
并运行一个 Java 应用程序的镜像,这个过程同时也将体现镜像分层复用的思想。docker
Dockerfile 中的每一个指令都会生成一个「层」,最终的目标镜像就是由多个「层」组成的。若是制做 B 镜像的 Dockerfile 中存在某个指令与制做 A 镜像的 Dockerfile 中的某个指令彻底一致,那么制做 B 镜像时就会复用制做 A 镜像时生成的「中间层」,而不会再去建立一个新的「层」,这就是「镜像分层复用」思想。shell
官方为咱们提供了 Linux 各类发行版的镜像,咱们平常的全部镜像构建都是基于这些镜像来完成的。因为官方的 CentOS 镜像并不支持中文字符集,因此咱们须要先制做一个支持中文的镜像出来数据库
FROM centos:7.2.1511 RUN yum install -y telnet kde-l10n-Chinese net-tools vim inetutils-ping unzip \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && localedef -c -f UTF-8 -i zh_CN zh_CN.utf8 \ && yum clean all ENV LC_ALL "zh_CN.UTF-8" CMD ["/bin/bash"]
FROM
指令表示咱们从官方提供的 CentOS 镜像开始构建咱们本身的镜像。centos
是镜像的名称,7.2.1511
是镜像的版本。apache
RUN
指令表示在构建镜像时咱们要执行的 shell 命令。以前的 FROM
指令至关于给了咱们一个干净的操做系统,咱们在这个系统上要执行的各类操做,如安装软件、建立目录等就都要书写在这个 RUN 指令以后。理论上你能够对每个要执行的 shell 命令都使用一个 RUN 指令,好比咱们将上面的 RUN 指令改写为下面的样子:vim
RUN yum install -y telnet kde-l10n-Chinese net-tools vim inetutils-ping unzip RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime RUN localedef -c -f UTF-8 -i zh_CN zh_CN.utf8 RUN yum clean all
这样编写出来的 Dockerfile 文件是没有任何问题的,镜像最终也可以制做成功,可是这并不切合镜像分层复用的思想,由于咱们几乎不会用到上面单个 RUN 指令生成的「中间层」。这样编写指令只会增长磁盘空间的占用,也让 Dockerfile 变得很是臃肿。centos
须要特别注意的是,若是 RUN 指令中有安装软件的操做,那就必定要在 RUN 指令的最后清除掉软件仓库的缓存,这样能够有效的瘦身镜像。
ENV
指令表示在构建镜像时要在操做系统中设置的环境变量。这个指令每次只能设置一个环境变量,若是须要设置多个环境变量,则须要编写多个 ENV 指令。
CMD
指令表示的是容器启动时要执行的操做,一般会设置为应用程序的启动脚本,这个指令必定是出如今 Dockerfile 的最后。被指定的操做必定是可以挂起一个进程的操做,不然容器启动并执行完这个操做后就会退出。
构建镜像时须要告诉 Docker 引擎 Dockerfile 的位置、镜像的名称和构建位置三个信息,下面是一个简单的镜像构建命令:
docker build -t myorg/centos:7.2 .
因为咱们没有使用 -f
参数指定 Dockerfile 文件的位置,Docker 引擎将默认使用当前目录下的 Dockerfile 文件进行构建。镜像的名称为 myorg/centos:7.2
,其中 myorg
是组织名,但不是必须的。若是你须要将镜像发布到公网去,或者尽量的避免和别人制做的镜像发生冲突,一般仍是建议加上组织名。最后的 .
表示构建位置在当前目录。一般建议将 Dockerfile 和构建所须要的文件放在一个目录下,而后在这个目录下执行构建。因为在构建开始前 Docker 引擎会读取构建目录下的全部文件,为了提升构建速度,请不要将构建中不须要的文件放到构建目录下。下面是执行上述构建命令后的输出,其中 shell 命令的输出内容被裁减掉了:
Sending build context to Docker daemon 2.048kB Step 1/4 : FROM centos:7.2.1511 ---> 4cbf48630b46 Step 2/4 : RUN yum install -y telnet kde-l10n-Chinese net-tools vim inetutils-ping unzip && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && localedef -c -f UTF-8 -i zh_CN zh_CN.utf8 && yum clean all ---> Running in 724ac4950fc9 // shell 命令执行的输出 ---> 2703f1dd2526 Removing intermediate container 724ac4950fc9 Step 3/4 : ENV LC_ALL "zh_CN.UTF-8" ---> Running in 2f49ec282e95 ---> f6919bceb45f Removing intermediate container 2f49ec282e95 Step 4/4 : CMD /bin/bash ---> Running in aea69f51eefd ---> e8e1d37c61a1 Removing intermediate container aea69f51eefd Successfully built e8e1d37c61a1 Successfully tagged myorg/centos:7.2
Sending build context to Docker daemon 2.048kB
表示在构建开始前,Docker 引擎读取到了构建目录下共有 2.048k 的文件。这里也印证了前文提到的不要将构建无关的文件放到构建目录下,不然会影响构建速度的结论。
Step 1/4 : FROM centos:7.2.1511
表示构建镜像的第一步是使用 centos:7.2.1511
镜像做为基础镜像,因为没有任何变动操做,因此下面输出的 4cbf48630b46
就是本来这个 CentOS 镜像的 ID。若是在构建时本地没有 centos:7.2.1511
这个镜像,那么这里还将输出 Docker 引擎从镜像仓库拉取这个镜像的信息。
Step 2/4 ...
表示构建镜像的第二步是执行这些 shell 命令。其下的 Running in 724ac4950fc9
表示 Docker 引擎启动了一个 ID 为 724ac4950fc9
的容器并在容器内部执行这些操做。接着 ---> 2703f1dd2526
表示这些 shell 命令执行完成后生成了 ID 为 2703f1dd2526
的中间「层」。最后的 Removing intermediate container 724ac4950fc9
表示当中间「层」生成完成后,删除了刚才使用的容器。
Step 3/4 ...
和 Step 4/4 …
表示的意义和 Step 2/4
相似,这里再也不赘述。
Successfully built e8e1d37c61a1
表示最终构建出来的镜像 ID 是 e8e1d37c61a1
。
Successfully tagged myorg/centos:7.2
表示把镜像的名称设置为了构建命令中指定的 myorg/centos:7.2
。
构建完成的镜像会直接被 Docker 管理,而不会给咱们生成一个文件。使用 docker images
命令能够查看到当前已有的镜像,以下所示:
REPOSITORY TAG IMAGE ID CREATED SIZE myorg/centos 7.2 e8e1d37c61a1 14 minutes ago 272MB centos 7.2.1511 4cbf48630b46 3 months ago 195MB
能够看到第一个镜像就是刚才建立的镜像,大小是 272MB,比本来官方的镜像多了 77MB。在制做这个镜像的过程当中还生成了 2 个中间「层」,咱们可使用 docker images -a
命令看到它们。
REPOSITORY TAG IMAGE ID CREATED SIZE myorg/centos 7.2 e8e1d37c61a1 18 minutes ago 272MB <none> <none> f6919bceb45f 18 minutes ago 272MB <none> <none> 2703f1dd2526 18 minutes ago 272MB centos 7.2.1511 4cbf48630b46 3 months ago 195MB
因为中间「层」没有名字,因此名称和 TAG 都显示为 <none>
。你能够尝试使用 docker rmi f6919bceb45f
命令来删除一个中间「层」,你会获得一个以下的错误提示:
Error response from daemon: conflict: unable to delete f6919bceb45f (cannot be forced) - image has dependent child images
从上面构建镜像的输出能够看出,f6919bceb45f
这一「层」,即 Step 3/4
这一步生成的「层」被 e8e1d37c61a1
所引用,因此这里不可以直接删除这个中间「层」。回想一下前文的那张镜像层次图中的引用箭头,这就是「层」与「层」直接的引用关系。
使用镜像就是利用制做好的镜像来启动容器,以下面的命令:
docker run --name mycontainer myorg/centos:7.2
docker run
是启动容器的命令,--name
用于指定容器的名称,最后面是启动容器所使用的镜像名称。命令执行完成后使用 docker ps
查看运行中的容器,这时你会发现并无任何容器出现;再使用 docker ps -a
查看全部容器将会有以下信息:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b4cae07cb40c myorg/centos:7.2 "/bin/bash" Less than a second ago Exited (0) 1 second ago mycontainer
能够看到刚才启动的 mycontainer
容器的状态(STATUS)为 Exited
,这表示容器已经退出了,即没有在运行状态,那为何容器启动后就会退出呢?前文已经提到过,Dockerfile 中 CMD
指令和 ENTRYPOINT
指令指定的是容器启动后的操做,这个操做必需要可以挂起一个进程,不然容器启动完成后就会退出。查看刚才编写的 Dockerfile 能够看到,CMD
指令指定的命令是 /bin/bash
,这个命令并不会挂起进程。
这个问题能够经过增长 docker run
命令的参数来解决,以下面的命令:
docker run -d -i --name mycontainer2 myorg/centos:7.2
命令执行完成后再次执行 docker ps
查看运行中的容器就能看到这个名为 mycontainer2
的容器了。其中 -d
参数表示让容器在后台运行,-i
参数表示保持标准输入打开,这样容器就不会在启动完成后当即退出了。
这一次的镜像构建使用咱们第一步构建出的镜像做为基础镜像。
FROM myorg/centos:7.2 RUN echo "[nginx]" >> /etc/yum.repos.d/nginx.repo \ && echo "name=nginx repo" >> /etc/yum.repos.d/nginx.repo \ && echo "baseurl=http://nginx.org/packages/centos/7/\$basearch/" >> /etc/yum.repos.d/nginx.repo \ && echo "gpgcheck=0" >> /etc/yum.repos.d/nginx.repo \ && echo "enabled=1" >> /etc/yum.repos.d/nginx.repo \ && yum makecache \ && rpm --rebuilddb \ && yum install -y java-1.8.0-openjdk-devel.x86_64 nginx \ && yum clean all ENV JAVA_HOME /usr CMD ["/bin/bash"]
这一份 Dockerfile 中的指令在上文中已经解释过了,这里再也不赘述。注意 RUN
指令后 shell 命令多行排版的方式是以 \
结尾,以 &&
开头。
这一次构建的镜像命名为 myorg/base:centos7.2.x64-ngx-java8
。在为镜像命名时,应当在名称和版本两个部分充分描述这个镜像,这样便于快速了解镜像的功能,构建命令以下:
docker build -t myorg/base:centos7.2.x64-ngx-java8 .
通过第二步的构建,咱们已经拥有了一个带有 Java 运行环境和 Nginx 的镜像,这一步就是要将应用系统也放入镜像中,并经过指令让容器启动后就去执行应用系统启动操做。
FROM myorg/base:centos7.2.x64-ngx-java8 COPY login-deploy-1.0 /home/admin/login/ COPY login-ui /home/admin/login-ui/ COPY nginx.conf /etc/nginx/nginx.conf COPY entrypoint.sh /home/admin/entrypoint.sh RUN chmod +x /home/admin/entrypoint.sh EXPOSE 80 VOLUME ["/home/admin/logs"] ENTRYPOINT ["sh", "/home/admin/entrypoint.sh"]
COPY
指令用于将文件或目录拷贝到镜像中指定的位置。若是拷贝的是一个目录,指定镜像中的位置时一般建议在最后加上 /
,以免将目录拷贝成文件的状况。与 COPY
类似的指令是 ADD
,后者能够将一个压缩文件拷贝到镜像中并自动解压。因为 ADD
的自动解压功能可能致使解压出来的文件的名称不可控,因此一般是推荐使用 COPY
命令来完成拷贝工做,压缩文件在拷贝前手动解压便可。
EXPOSE
指令用于指定使用这个镜像启动的容器能够经过哪一个端口和外界进行通讯。换言之,只有 EXPOSE 指令指定的端口才可以和宿主机上的端口作映射。好比这里 EXPOSE 了 80 端口,那么在启动容器的时候就能够将宿主机的 8888 端口映射到容器的 80 端口,这样外界访问宿主机的 8888 端口就至关于访问容器内部的 80 端口。
VOLUME
指定用于指定容器数据的挂载点。容器在运行时会产生各类数据,因为容器和宿主机自然是隔离的,因此在宿主机上并不能看到容器内的数据,当容器被销毁时,这些数据也会随之销毁,没法找回。为了将容器内产生的数据存放到宿主机上,咱们能够在制做镜像时指定某些目录为挂载点,而后将容器运行时产生的数据指定输出到这些目录中。当容器启动时,Docker 就会自动在宿主机上建立数据卷来映射挂载点,这样容器中产生的数据就会保存在宿主机上的这个数据卷内。数据卷有本身独立的生命周期,即便删掉了容器,数据卷也还会存在。
Docker 会使用随机 ID 给数据卷命名,这很是不便于管理。在启动 Docker 容器时可使用 -v
参数来指定数据卷的名称,如 -v myappdata:/home/admin/logs
。这样当咱们启动容器时,Docker 就会在宿主机上建立名为 myappdata
的数据卷。查看数据卷使用命令 docker volume ls
。
ENTRYPOINT
指令的做用和前文介绍的 CMD
指令的做用是基本一致的。区别在于前者指定的命令不会被覆盖,然后者指定的命令会被启动容器时附带的命令所覆盖。对于应用程序镜像来讲,一般建议使用 ENTRYPOINT 指令。在这份 Dockerfile 中,ENTRYPOINT 指令表示在容器启动后执行 /home/admin/entrypoint.sh
这份脚本。
这一次构建的镜像命名为 myorg/login:20190108
。
docker build -t myorg/login:20190108 .
通过上面的三个步骤,一个可使用的应用系统镜像就制做完成了,使用下面的命令来启动容器:
docker run -d --name loginService -p 8800:80 -p 9090:8080 -v myappdata:/home/admin/logs myorg/login:20190108
-d
参数表示让容器在后台运行。
--name
参数指定容器的名称。
-p
参数指定端口映射关系。命令中的关系为将宿主机 8800 端口映射到容器中的 80 端口,将宿主机 9090 端口映射到容器中的 8080 端口。
-v
参数指定挂载点对应数据卷的名称。须要特别说明的是,挂载点也能够指定一个宿主机目录去挂载,这样 Docker 将不会建立数据卷。好比使用宿主机的 /data/appdata
目录去挂载,参数值修改成 -v /data/appdata:/home/admin/logs
。挂载前宿主机目录必须存在,Docker 不会自动建立,而且要保证具备读写权限。
对于经常使用如 MySQL、Kafka、Redis 等中间件,官方已经为咱们提供了通过测试的镜像,咱们能够直接拿来使用。可是因为业务的具体需求等缘由,咱们一般须要对这些镜像进行修改。所谓修改镜像,其实就是基于这些官方镜像制做出新的镜像。在下面的这个例子中,咱们将一步步把官方的 mysql:5.7
镜像进行修改。
不少官方镜像都使用的零时区,这显然不符合国情,因此 一般咱们都须要把官方镜像的时区调整为东八区。调整时区须要安装 tzdata
这个软件,因此咱们须要事先肯定官方镜像是基于哪一种 Linux 发行版进行构建的,不然咱们将不知道该使用什么软件安装命令。你能够登陆 Docker Hub 搜索对应的镜像,而后查看官方放置在 GitHub 上的 Dockerfile 文件来肯定相关信息。
FROM mysql:5.7 ENV TZ=Asia/Shanghai COPY customer.cnf /etc/mysql/conf.d/ RUN apt-get update \ && apt-get install -y tzdata \ && ln -s -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && rm -rf /var/lib/apt/lists/*
修改时区的同时咱们经过增长自定义 MySQL 配置文件来调整 MySQL 字符集、时区、链接数等配置,内容以下:
[client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqld] character-set-client-handshake=FALSE character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci max_connections=1000 default-time_zone='+8:00'
须要注意的是咱们并无使用 CMD
指令或 ENTRYPOINT
指令来指点容器启动后要执行的操做,由于在不少状况下,除非咱们查阅官方镜像的 Dockerfile 文件,不然咱们没法获知本来的启动操做是什么。因此只要咱们变动的操做不影响启动流程,那么就能够不指定启动操做,让镜像默认使用基础镜像的启动操做。
这一步咱们将镜像命名为 myorg/mysql:5.7_bjtime_utf8mb4
,执行构建命令:
docker build -t myorg/mysql:5.7_bjtime_utf8mb4 .
在系统部署状况下,咱们系统 MySQL 容器启动后就能将所须要的数据库创建好,这样能够避免咱们再手动去建库。
FROM myorg/mysql:5.7_bjtime_utf8mb4 COPY db_login.sql /sqls/db_login.sql COPY privileges.sql /sqls/privileges.sql COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENV MYSQL_ALLOW_EMPTY_PASSWORD yes ENTRYPOINT ["sh", "/entrypoint.sh"]
本次构建基于上一步的镜像继续构建,db_login.sql
是建库建表的 SQL 脚本,privileges.sql
是添加数据库用户信息的脚本, entrypoint.sh
是容器启动后要执行的脚本。
privileges.sql
中主要是修改了 ROOT 用户的密码并容许远程登陆,内容以下:
update mysql.user set authentication_string=password("123456") where user = "root"; create user 'root'@'%' identified by '123456'; grant all privileges on *.* to 'root'@'%'; flush privileges;
entrypoint.sh
脚本负责启动 MySQL Server 并执行 db_login.sql
和 privileges.sql
,内容以下:
#! /bin/sh service mysql start sleep 3 mysql < /sqls/db_login.sql mysql < /sqls/privileges.sql tail -f /dev/null
最后的 tail -f /dev/null
是为了让进城挂起,禁止容器退出。
ENV MYSQL_ALLOW_EMPTY_PASSWORD yes
表示容许容器启动时 MySQL ROOT 用户没有密码。官方默认要求启动时必须设置 ROOT 用户密码,不然容器没法启动。
至此,一个启动即建库并支持 utf8mb4 和东八区的 MySQL 镜像就修改完成了,这个镜像中关于 MySQL 安装和配置部分彻底是复用的官方镜像,咱们只作了定制化的修改。最后,咱们将镜像命名为 myorg/mysql:5.7_login_20190108
。
docker build -t myorg/mysql:5.7_login_20190108 .
docker run -d --name login_db -p 3306:3306 -v /data/mysqldata:/var/lib/mysql myorg/mysql:5.7_login_20190108
当你须要制做一个镜像,尤为是中间件镜像时,最好的选择是先去 Docker Hub 搜索是否已有相关的官方镜像。基于官方镜像或者别人发布的镜像来进行定制化比本身从头作一个镜像更方便更可靠。
Docker Hub 是世界上最大的 Docker 镜像仓库,Docker 官方和世界各地的开发者都在这上面发布本身制做的镜像。在这里你能够找到各类镜像的使用说明,也能找到其 Dockerfile 来学习。好比咱们上面使用官方的 MySQL 镜像来定制化,那么 ROOT 密码该怎么设置,数据挂载点在哪里,开放了哪些端口这些问题,你都能在镜像文档中找到答案。
Docker 镜像的制做技术很是简单,难点在于你是否可以事先规划好镜像内容。当你编写 Dockerfile 时,你的脑海里应该具备镜像制做完成以后的一个全貌,这样你编写的 Dockerfile 才是可靠有效的。编写 Dockerfile 其实就像是给你一个干净的操做系统,让你去安装软件,设置目录,启动应用相似,明确了目的,流程就会很清晰。