1. Docker 镜像
镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码、运行时、库、环境变量和配置文件。
1.1 UnionFS 联合文件系统
Union 文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(Unite Several Directories into A Single Virtual FileSystem)。Union 文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录。
1.2 Docker 镜像加载原理
Docker 的镜像实际上由一层一层的文件系统组成,这种层级的文件系统就是 UnionFS。
bootfs(boot file system)主要包含 bootLoader 和 Kernel,bootLoader 主要是引导加载 Kernel。Linux 刚启动时会加载 bootfs 文件系统,在 Docker 镜像的最底层是 bootfs。这一层与我们典型的 Linux/Unix 系统是一样的,包含 boot 加载器和内核。当 boot 加载完成之后整个内核就都在内存中了,此时内存的使用权已由 bootfs 转交给内核,此时系统也会卸载 bootfs。
rootfs(root file system)在 bootfs 之上,包含的就是典型 Linux 系统中的 /dev、/proc、/bin、/etc 等标准目录和文件。rootfs 就是各种不同的操作系统发行版,比如 Ubuntu,CentOS 等等。
Q1:平时我们安装进虚拟机的 CentOS 都是好几个 G,为什么 Docker 这里才 215M?
对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了,因为底层直接用宿主机的 Kernel,自己只需要提供 rootfs 就行了。由此可见对于不同的 Linux 发行版,bootfs 基本是一致的,只是 rootfs 会有差别,因此不同的发行版可以共用 bootfs。
Q2:为什么 tomcat 这么大?→ “分层”的镜像
在下载的过程中我们可以看到 Docker 的镜像好像是在一层一层的下载 ...
Q3:为什么 Docker 镜像要采用这种分层结构呢?
最大的一个好处就是共享资源。比如:有多个镜像都从相同的 base 镜像构建而来,那么宿主机只需在磁盘上保存一份 base 镜像,同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享!
1.3 commit 操作
1.3.1 访问 tomcat 报 404
Docker 方式启动 tomcat,访问首页出现 404 错误。
docker run -it -p 8888:8080 tomcat # 分别代表 <docker容器端口>:<tomcat端口>
docker run -it -P tomcat # 随机分配 docker 容器端口,但是 tomcat 会是默认的 8080
通过 netstat -anp | more
查看端口占用情况,docker 的确在监听 8888 端口。
进入正在运行的容器,一探究竟:
这时候再访问,成了!
但这么做,只在当前有效。容器停止后,下一次再使用镜像生成新的容器时,这个错误还是存在,即实际上问题的根源是在生成容器的镜像上,只有将镜像修改了,再生成的容器才不会再出现这个问题。
1.3.2 容器 → 镜像
由镜像可以生成容器实例,也可以根据当前正在运行的容器实例的实际状况又生成新的镜像(有点像反射)。通过 docker commit
提交容器副本使之成为一个新的镜像,命令格式如下:
docker commit -m="提交的描述信息" -a="作者" 容器ID 要创建的目标镜像名:[标签名]
上一小节修改过 webapps 的 tomcat 运行实例算是一个标准的 tomcat 容器了,故以它为模板 commit 一个新的 tomcat 镜像 nuist/mytomcat。这样一来,以后再创建 tomcat 容器时,使用我们自己生成的镜像即可(它跟阿里云拉下来的没什么差别,只是保存了我们之前对容器做的修改)。
docker commit -a="ljq" -m="tomcat update the folder-webapps" c9e9af3087aa nuist/mytomcat:1.1
测试:
顺便 review 下“守护方式”启动容器:
1.4 Re: Docker
Docker 镜像都是只读的。当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
a. Image Definition
镜像(Image)就是一堆只读层(Read-Only Layer)的统一视角。
从左边我们看到了多个只读层,它们重叠在一起。除了最下面一层,其它层都会有一个指针指向下一层。这些层是 Docker 内部的实现细节,并且能够在主机(运行 Docker 的机器)的文件系统上访问到。统一文件系统(union file system)技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。我们可以在上图的右边看到这个视角的形式。
root@liujiaqi /]# tree -L 1 /var/lib/docker/
/var/lib/docker/
├── buildkit
├── containers
├── image
├── network
├── overlay2
├── plugins
├── runtimes
├── swarm
├── tmp
├── trust
└── volumes
11 directories, 0 files
b. Container Definition
容器(Container)的定义和镜像(Image)几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。
细心的读者可能会发现,容器的定义并没有提及容器是否在运行,没错,这是故意的。正是这个发现帮助我理解了很多困惑。
要点:容器 = 镜像 + 读写层,并且容器的定义并没有提及是否要运行容器。
c. Running Container Definition
一个运行态容器(Running Container)被定义为一个可读写的统一文件系统加上隔离的进程空间和包含其中的进程。下面这张图片展示了一个运行中的容器。
正是“文件系统隔离技术”使得 Docker 成为了一个前途无量的技术。一个容器中的进程可能会对文件进行修改、删除、创建,这些改变都将作用于可读写层(Read-Write Layer)。下面这张图展示了这个行为:
我们可以通过运行 docker run centos touch liujiaqi.txt
来验证我们上面所说的;即便这个 centos 容器不再运行,我们依旧能够在宿主机的文件系统上找到这个新文件。
d. Image Layer Definition
为了将零星的数据整合起来,我们提出了镜像层(Image Layer)这个概念。下面的这张图描述了一个镜像层,通过图片我们能够发现一个层并不仅仅包含文件系统的改变,它还能包含了其他重要信息。
元数据(metadata)就是关于这个层的额外信息,它不仅能够让 Docker 获取运行和构建时的信息,还包括父层的层次信息。需要注意,只读层(Read-Only Layer)和读写层(Read-Write Layer)都包含元数据。
除此之外,每一层都包括了一个指向父层的指针。如果一个层没有这个指针,说明它处于最底层。
「Metadata Location」我发现在我自己的主机上,镜像层(Image Layer)的元数据被保存在名为“json”的文件中,比如说:
一个容器的元数据好像是被分成了很多文件,但或多或少能够在 /var/lib/docker/containers/<id>
目录下找到,<id>
就是一个可读层的 id。这个目录下的文件大多是运行时的数据,比如说网络、日志等等。
e. Tying It All Together!
docker create <Image-ID>
为指定的镜像(Image)添加了一个可读写层(Read-Write Layer),构成了一个新的容器。注意,这个容器并没有运行。
docker start <Container-ID>
为容器文件系统创建了一个进程隔离空间。注意,每一个容器只能够有一个进程隔离空间。
docker run <Image-ID>
先是利用镜像创建了一个容器,然后运行这个容器。这个命令非常地方便,并且隐藏了两个命令的细节(类似于 git pull
命令,它就是 git fetch
和 git merge
两个命令的组合;同样地,docker run
就是 docker create
和 docker start
两个命令的组合)。
docker ps
会列出所有运行中的容器。这隐藏了非运行态容器的存在,如果想要找出这些容器,我们需要使用下面这个命令。
docker ps -a
会列出所有的容器,不管是运行的,还是停止的。
docker images
会列出了所有顶层(Top-Level)镜像。实际上,在这里我们没有办法区分一个镜像(Image)和一个只读层(Read-Only Layer),所以我们提出了 Top-Level 镜像。只有创建容器时使用的镜像或者是直接 pull 下来的镜像能被称为顶层(Top-Level)镜像,并且每一个顶层镜像下面都隐藏了多个镜像层。
docker images -a
列出了所有的镜像,也可以说是列出了所有的只读层。如果你想要查看某一个 Image-ID 下的所有层,可以使用 docker history
来查看。
docker stop <Container-ID>
会向运行中的容器发送一个 SIGTERM 的信号,然后停止所有的进程。
docker kill <Container-ID>
向所有运行在容器中的进程发送了一个不友好的 SIGKILL 信号。
stop 和 kill 命令会发送 UNIX 的信号给运行中的进程,docker pause <Container-ID>
则不一样,它利用了 cgroups 的特性将运行中的进程空间暂停。
docker rm <Container-ID>
会移除构成容器的可读写层(Read-Write Layer)。注意,这个命令只能对非运行态容器执行。
docker rmi <Image-ID>
会移除构成镜像的一个只读层。你只能够使用 docker rmi
来移除最顶层(Top Level Layer,也可以说是镜像),你也可以使用 -f
参数来强制删除中间的只读层(Read-Only Layer)。
docker commit <Container-ID>
将容器的可读写层转换为一个只读层,这样就把一个容器转换成了不可变的镜像。
docker build
命令非常有趣,它会反复的执行多个命令。从下图可以看到,build 命令根据 Dockerfile 文件中的 FROM 指令获取到镜像,然后重复地 ① run(create & start)、② 修改、③ commit。在循环中的每一步都会生成一个新的层,因此许多新的层会被创建。
docker exec <Running-Container-ID>
会在运行中的容器执行一个新进程。
docker inspect <Container-ID> | <Image-ID>
会提取出容器或者镜像最顶层的元数据。
docker save <Image-ID>
会创建一个镜像的压缩文件,这个文件能够在另外一个主机的 Docker 上使用。和 export 命令不同,这个命令为每一个层都保存了它们的元数据。这个命令只能对镜像生效。
docker export <Container-ID>
会创建一个 tar 文件,并且移除了元数据和不必要的层,将多个层整合成了一个层,只保存了当前统一视角看到的内容(expoxt 后的容器再 import 到 Docker 中,通过 docker images -tree
命令只能看到一个镜像;而 save 后的镜像则不同,它能够看到这个镜像的历史镜像)。
docker history <Image-ID>
会递归地输出指定镜像的历史镜像。
2. Docker 容器数据卷
2.1 概述
Q1:是什么?
先来看看 Docker 的理念:
- 将运用与运行的环境打包形成容器运行 ,运行可以伴随着容器,但是我们对数据的要求希望是持久化的。
- 容器之间希望有可能共享数据
Docker 容器产生的数据,如果不通过 docker commit
生成新的镜像,使得数据做为镜像的一部分保存下来,那么当容器删除后,数据自然也就没有了。
为了能保存数据,在 Docker 中我们使用“容器数据卷”// 有点类似我们 Redis 里面的 rdb 和 aof 文件!
Q2:作用
① 容器的持久化;② 容器间继承+共享数据;③ 容器和宿主机共享数据
“卷”就是目录或文件,存在于一个或多个容器中,由 Docker 挂载到容器,但不属于联合文件系统,因此能够绕过 Union File System 提供一些用于持续存储或共享数据的特性:
- 数据卷可在容器之间共享或重用数据
- 卷中的更改可以直接生效
- 数据卷中的更改不会包含在镜像的更新中
- 数据卷的生命周期一直持续到没有容器使用它为止
“卷”的设计目的就是数据的持久化,完全独立于容器的生存周期,因此 Docker 不会在容器删除时删除其挂载的数据卷。
2.2 容器内添加数据卷
2.2.1 直接命令添加
命令格式(-v 即 volume):
docker run -it -v </宿主机绝对路径目录:/容器内目录>[:ro] <镜像名>
# ro即readonly,一旦添加这个选项,容器只能读宿主机往目录里添的内容,自己不能做增删改操作。
Dockers 自动创建对应的文件夹:
查看数据卷是否挂载成功:
容器和宿主机之间共享数据:
容器停止退出后,主机修改后数据是否同步?同步!
2.2.2 DockerFile 添加
[Java] Hello.java → Hello.class
[Docker] Images → DockFile
- 根目录下新建 mydocker 文件夹并进入
- 可在 DockerFile 中使用
VOLUME
指令来给镜像添加一个或多个数据卷(出于可移植和分享的考虑,用-v 主机目录:容器目录
这种方法不能够直接在 DockerFile 中实现。由于宿主机目录是依赖于特定宿主机的,并不能够保证在所有的宿主机上都存在这样的特定目录)。 - 构建 DockerFile
# volume test FROM centos VOLUME ["/dataVolumeContainer1","/dataVolumeContainer2"] CMD echo "finished, success!" CMD /bin/bash
- build 后生成镜像,通过该镜像 run 容器
docker build -f /mydocker/dockerFile -t mycentos . docker run -it mycentos
通过上述步骤,容器内的卷目录地址已经知道,那么对应的主机目录地址在哪呢?→ 通过 docker inspect <Container-ID>
查看:
若 Docker 挂载主机目录,访问时出现 cannot open directory .: Permission denied
,解决办法是在挂载目录后多加一个 --privileged=true
参数。
2.3 数据卷容器
某个命名的容器挂载数据卷,其它容器通过挂载这个“父”容器实现数据共享,则这个挂载数据卷的容器,就称之为“数据卷容器”。
测试容器间传递共享 --volumes-from <Container-Name>
:
3. Dockerfile
3.1 概述
Dockerfile 是用来构建 Docker 镜像的构建文件,是由一系列命令和参数构成的脚本。
构建三步骤:dockerfile(编写) → docker build(构建) → docker run(运行)
3.2 构建过程解析
Dockerfile 内容基础知识:
- 每条保留字指令都必须为大写字母且后面要跟随至少一个参数
- 指令按照从上到下,顺序执行
#
表示注释- 每条指令都会创建一个新的镜像层,并对镜像进行提交
Docker 执行 Dockerfile 的大致流程:
- Docker 从基础镜像(scratch)运行一个容器
- 执行一条指令并对容器作出修改
- 执行类似
docker commit
的操作提交一个新的镜像层 - Docker 再基于刚提交的镜像运行一个新容器
- 执行 Dockerfile 中的下一条指令直到所有指令都执行完成
Docker Hub 中 99% 的镜像都是通过在 base 镜像(scratch)中安装和配置需要的软件构建出来的。
小结:
从应用软件的角度来看,Dockerfile、Docker 镜像与 Docker 容器分别代表软件的 3 个不同阶段:
- Dockerfile 是软件的原材料
- Docker 镜像是软件的交付品
- Docker 容器则可以认为是软件的运行态
Dockerfile 面向开发,Docker 镜像成为交付标准,Docker 容器则涉及部署与运维,三者缺一不可,合力充当 Docker 体系的基石。
- Dockerfile,需要定义一个 Dockerfile,Dockerfile 定义了进程需要的一切东西。Dockerfile 涉及的内容包括执行代码或者是文件、环境变量、依赖包、运行时环境、动态链接库、操作系统的发行版、服务进程和内核进程(当应用进程需要和系统服务和内核进程打交道,这时需要考虑如何设计 namespace 的权限控制)等。
- Docker 镜像,在用 Dockerfile 定义一个文件之后,
docker build
时会产生一个 Docker 镜像,当运行 Docker 镜像时,会真正开始提供服务。 - Docker 容器,容器是直接提供服务的。
3.3 体系结构
3.4 案例
3.4.1 自定义 centos
自定义 mycentos 使我们自己的镜像具备如下:
- 登陆后的落脚点为 /usr/local/
- vim 编辑器
- 查看网络配置 ifconfig 支持
1. 编写 Dockfile 文件:dockFile2
2. 构建命令:docker build -f /mydocker/dockFile2 -t mycentos:1.2 .
3. 运行:docker run -it mycentos:1.2
4. 列出镜像的变更历史:docker history mycentos:1.2
3.4.2 CMD/ENTRYPOINT
CMD、ENTRYPOINT 都是指定一个容器启动时要运行的命令。不同点是,一个会被 run 后的参数覆盖,另一个被追加组合。
1. Dockerfile 中可以有多个 CMD
指令,但只有最后一个生效,CMD
会被 docker run
之后的参数替换。
2. docker run
之后的参数会被当做参数传递给 ENTRYPOINT
,之后形成新的命令组合。
【crul 命令解释】curl
命令可以用来执行下载、发送各种 HTTP 请求,指定 HTTP 头部等操作。如果系统没有 curl
可以使用 yum install curl
安装,也可以下载安装。curl
是将下载文件输出到 stdout
。使用命令:curl http://www.baidu.com
,执行后,www.baidu.com 的 html 就会显示在屏幕上了。如果我们希望显示 HTTP 头信息,就需要加上 -i
参数。
我们可以看到 myip 报可执行文件找不到的报错(executable file not found)。之前我们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i
替换了原来的 CMD 后面的命令,而不是添加在原来的 curl -s http://ip.cn
后面。而 -i
根本不是命令,所以自然找不到。
那么如果我们希望加入 -i
这参数,我们就必须重新完整的输入这个命令:docker run myip curl -s http://ip.cn -i
。但这也太麻烦了,故当遇到这种组合命令,就该用 ENTRYPOINT
来指定一个容器启动时要运行的命令。
3.4.3 ONBUILD
当构建一个被继承的 Dockerfile 时运行命令,父镜像在被子继承后父镜像的 onbuild 被触发。
3.4.4 自定义镜像 tomcat9
1. mkdir -p /zzyyuse/mydockerfile/tomcat9
2. 在上述目录下touch c.txt
3. 将 jdk 和 tomcat 安装的压缩包拷贝进上一步目录
4. 在 /zzyyuse/mydockerfile/tomcat9 目录下新建 Dockerfile 文件
FROM centos
MAINTAINER zzyy<zzyybs@126.com>
# 把宿主机当前上下文的 c.txt 拷贝到容器 /usr/local/ 路径下
COPY c.txt /usr/local/cincontainer.txt
# 把 jdk 与 tomcat 添加到容器中(ADD是带解压功能的COPY)
ADD jdk-8u171-linux-x64.tar.gz /usr/local/
ADD apache-tomcat-9.0.8.tar.gz /usr/local/
# 安装 vim 编辑器
RUN yum -y install vim
# 设置工作访问时候的 WORKDIR 路径,登录落脚点
ENV MYPATH /usr/local
WORKDIR $MYPATH
# 配置 jdk 与 tomcat 环境变量
ENV JAVA_HOME /usr/local/jdk1.8.0_171
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /usr/local/apache-tomcat-9.0.8
ENV CATALINA_BASE /usr/local/apache-tomcat-9.0.8
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
# 容器运行时监听的端口
EXPOSE 8080
# 启动时运行 tomcat
# ENTRYPOINT ["/usr/local/apache-tomcat-9.0.8/bin/startup.sh" ]
# CMD ["/usr/local/apache-tomcat-9.0.8/bin/catalina.sh","run"]
CMD /usr/local/apache-tomcat-9.0.8/bin/startup.sh (不要换行,我这里是为了赏心悦目)
&& tail -F /usr/local/apache-tomcat-9.0.8/bin/logs/catalina.out
5. 构建 docker build -t zzyytomcat9 .
6. run
docker run -d -p 9080:8080 --name myt9 (没有换行,我这里纯粹是为了赏心悦目)
-v /zzyyuse/mydockerfile/tomcat9/test:/usr/local/apache-tomcat-9.0.8/webapps/test
-v /zzyyuse/mydockerfile/tomcat9/tomcat9logs/:/usr/local/apache-tomcat-9.0.8/logs
--privileged=true zzyytomcat9
7. 验证
![](_v_images/20201113212250547_14690.png =600x)
8. 结合前述的容器卷将测试的 web 服务 test 发布
3.5 小结
4. Docker 常用安装
4.1 安装 MySQL
从 Docker Hub 上(阿里云加速器)拉取 MySQL 镜像(标签为 5.7)到本地:
docker run -p 12345:3306 --name mysql
-v /mydocker/mysql/conf:/etc/mysql/conf.d
-v /mydocker/mysql/logs:/logs
-v /mydocker/mysql/data:/var/lib/mysql
-e MYSQL_ROOT_PASSWORD=123456
-d mysql:5.7
外部 Win10 也来连接运行在 Dokcer 上的 MySQL 服务:
数据备份小测试:
docker exec <Container-ID> sh -c ' exec mysqldump --all-databases -uroot -p"123456" ' > /dbs.sql
4.2 安装 Redis
从 Docker Hub 上(阿里云加速器)拉取 Redis 镜像(标签为 3.2)到本地:
docker run -p 6379:6379
-v /mydocker/myredis/data:/data
-v /mydocker/myredis/conf/redis.conf:/usr/local/etc/redis/redis.conf
-d redis:3.2 redis-server /usr/local/etc/redis/redis.conf
--appendonly yes
5. 本地镜像发布到阿里云
本地镜像发布到阿里云流程:
1. 创建仓库镜像:命名空间、仓库名称
2. 将镜像推送到 Registry,其中 [ImageId]、[镜像版本号] 根据自己的镜像信息进行填写:↓
3. 将阿里云上的镜像下载到本地