dockre用了估计也有一年了,今天来总结下
近年来容器技术火热,docker公司表示很强势,周围很多朋友一说到容器,就想到docker,那么多人用,docker就一定好吗,未必。
学之前,读者必须打量docker的当下与未来发展,首先docker是国内采用的容器引擎中应该是最多的,但是,容器概念早有了,容器引擎技术并不是docker公司专利,docker的发展也必然受到google(容器编排技术标准的龙头老大)等老牌企业的打压,近来kubernetes放弃docker作为容器引擎,containerd等逐渐火热,crictl等命令行工具层出,都是打压迹象。
所以你得考虑是否要花时间学docker还是直接去学其他的容器引擎,或者说你是否应该将docker作为你主要掌握的一门容器技术。
这里讲docker,主要是因为他的用户存量还有,另外就是容器引擎技术间还是有比较大的相同之处
这里对于容器的历史发展状况,比如linux的LXC就不说了,直接走容器以及实操还有填报上一些坑
docker 开源容器引擎技术,虚拟化技术之一,go开发
传统的开发和运维间环境不同,一直是个痛点(尽管现在有了devops等),容器技术的出现,比如我们用Docker 可以将程序运行的环境也一起打包到版本控制去了,这样就排除了因为环境不同造成的各种麻烦事情了
用官方的话来说,Docker 受欢迎,是因为以下几个特点:
灵活性:即使是最复杂的应用也可以集装箱化
轻量级:容器利用并共享主机内核
可互换:您可以即时部署更新和升级
便携式:您可以在本地构建,部署到云,并在任何地方运行
可扩展:您可以增加并自动分发容器副本
可堆叠:您可以垂直和即时堆叠服务
开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 服务器。容器是一个沙箱机制,相互之间不会有影响(类似于我们手机上运行的 app),并且容器开销是很低的
Docker 几个重要概念¶
在了解了 Docker 是什么之后,我们需要先了解下 Docker 中最重要的3个概念:镜像、容器和仓库。
镜像 是一个只读模板,带有创建 Docker 容器的说明,一般来说的,镜像会基于另外的一些基础镜像并加上一些额外的自定义功能来组成。比如,你可以构建一个基于 Centos 的镜像,然后在这个基础镜像上面安装一个 Nginx 服务器,这样就可以构成一个属于我们自己的镜像了。
容器 是一个镜像的可运行的实例,可以使用 Docker REST API 或者 CLI 命令行工具来操作容器,容器的本质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。
registry 是用来存储 Docker 镜像的仓库,Docker Hub 是 Docker 官方提供的一个公共仓库,而且 Docker 默认也是从 Docker Hub 上查找镜像的,当然你也可以很方便的运行一个私有仓库,当我们使用 docker pull 或者 docker run 命令时,就会从我们配置的 Docker 镜像仓库中去拉取镜像,使用 docker push 命令时,会将我们构建的镜像推送到对应的镜像仓库中,registry 可以理解为用于镜像的 github 这样的托管服务。
镜像其实也是个文件,有基础镜像也有已经设置好功能的提供服务的镜像,下载下来即可运行成容器
容器,本质就是操作系统上的一个进程,通过namespace对网络,进程,pid,用户id的那个虚拟资源惊醒隔离,用cgroup对cpu和内存等实体资源进行隔离限制,生成一个具有自己操作系统的独立的特殊进程,与其他容器共享主机的内核(与虚拟机的较大区别),它运行一个独立的进程,不占用其他任何可执行文件的内存,非常轻量。
registry:仓库,存放镜像的地方,你可以在官网或者阿里上注册个你的账号,账号名就是一个仓库,分公有和私有,同样你可以自己搭建个仓库服务器(harbor)
Docker 在 Linux 上使用以下几个命名空间(上面说的各个方面):
pid namespace:用于进程隔离(PID:进程ID)
net namespace:管理网络接口(NET:网络)
ipc namespace:管理对 IPC 资源的访问(IPC:进程间通信(信号量、消息队列和共享内存))
mnt namespace:管理文件系统挂载点(MNT:挂载)
uts namespace:隔离主机名和域名
user namespace:隔离用户和用户组(3.8以后的内核才支持)
cgroup很有意思,linux将它简化成文件,容易配置,可以自己去玩下
附上一张官网docker架构图:
这张图放现在来看,并不完善了,少了shim等东西,这些放在containerd中讲
以上的基本概念够用了
centos7,docker-ce(社区),使用 overlay2 存储驱动程序
ee企业版
旧版本的 Docker 叫 docker 或者 docker-engine,先卸载旧版本的docker:
yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine
用yum安装docker,使用阿里的开源镜像源,详情可以上阿里看:
# step 1: 安装必要的一些系统工具
yum install -y yum-utils device-mapper-persistent-data lvm2
# Step 2: 添加软件源信息
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# Step 3
sed -i 's+download.docker.com+mirrors.aliyun.com/docker-ce+' /etc/yum.repos.d/docker-ce.repo
#Step 4
yum makecache fast或yum repolist
#Step 5查看所有版本选择指定版本下载
yum list docker-ce --showduplicates
yum install docker-ce-18.09.9 docker-ce-cli-18.09.9 containerd.io -y
配置阿里的加速镜像
就类似是服务站点,帮我们去拉去镜像,速度更快
mkdir -p /etc/docker
vim /etc/docker/daemon.json
{
"registry-mirrors" : [
"https://xxxxxxxxxxxx.mirror.aliyuncs.com"
],
"graph": "/data/docker"
}
daemon.json也算是docker的配置文件,注意这个文件为空的话一般都会报错
docker的根目录一般是/var/lib/docker,这里把它改成了/data/docker
注意别漏了逗号
systemctl daemon-reload
systemctl enable docker --now
Docker 官方提供了一个公共的镜像仓库:Docker Hub,我们就可以从这上面获取镜像,获取镜像的命令:docker pull,格式为:
docker pull [选项] [Docker Registry 地址[:端口]/]仓库名[:标签]
仓库名有细分为用户名/软件名
镜像地址是缺省的,用户名默认是library,docker hub公开的用户
查看指定标签的镜像,可以用命令行过滤出来,但建议去dockerhub查看(注册个账号)
不指定标签则默认latest
操作镜像用镜像名或id都行:
[root@localhost ~]# docker pull nginx 拉取镜像
Using default tag: latest
latest: Pulling from library/nginx
a2abf6c4d29d: Pull complete
a9edb18cadd1: Pull complete
589b7251471a: Pull complete
186b1aaa4aa6: Pull complete
b4df32aa5a72: Pull complete
a0bcbecc962e: Pull complete
Digest: sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31
Status: Downloaded newer image for nginx:latest
[root@localhost ~]# docker images 列出镜像
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 605c77e624dd 3 months ago 141MB
列表包含了仓库名、标签、镜像 ID、创建时间以及所占用的空间。镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个标签。
[root@localhost ~]# docker tag nginx:latest nginx:test 给镜像打标签,也可以用来给镜像改名,一般是用来该标签,latest可以不写,默认latest
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 605c77e624dd 3 months ago 141MB
nginx test 605c77e624dd 3 months ago 141MB
[root@localhost ~]# docker save nginx > nginx.tar.gz 将镜像导出成压缩包,一般都是tar.gz
[root@localhost ~]# ls
anaconda-ks.cfg initial-setup-ks.cfg nginx.tar.gz test 公共 模板 视频 图片 文档 下载 音乐 桌面
[root@localhost ~]# docker rmi nginx 删除镜像
Untagged: nginx:latest
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx test 605c77e624dd 3 months ago 141MB
[root@localhost ~]# docker load <~/nginx.tar.gz 将镜像压缩包导入成镜像
Loaded image: nginx:latest
Loaded image: nginx:test
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 605c77e624dd 3 months ago 141MB
nginx test 605c77e624dd 3 months ago 141MB
导入镜像是不用指定标签,用的就是压缩时的标签
任何时候,不写标签就默认latest
从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的sha256 的摘要,以确保下载一致性。
以这个镜像为基础运行一个容器
[root@localhost ~]# docker run -d --name=nginx1 nginx:test -d表示后台运行,--name制定容器名称,不指定就自动生成一窗字符串作为容器名称
5c400ee504773f05bbc9812868342de871729dc01d80b2eb87cbdd67889ffc3a
[root@localhost ~]# docker ps 查看容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5c400ee50477 nginx:test "/docker-entrypoint.…" 5 seconds ago Up 4 seconds 80/tcp nginx1
[root@localhost ~]# docker run -it --name=nginx2 nginx:test /bin/bash -it表示分配交互式(i)终端(t) /bin/bash是终端类型,其实他这个位置是[command],也就是说这里是写命令的,可以直接在这里写ls
root@d40d5aabdc94:/# ls
bin dev docker-entrypoint.sh home lib64 mnt proc run srv tmp var
boot docker-entrypoint.d etc lib media opt root sbin sys usr
[root@localhost ~]# docker run -it --name=nginx3 nginx:test ls
bin docker-entrypoint.d home media proc sbin tmp
boot docker-entrypoint.sh lib mnt root srv usr
dev etc lib64 opt run sys var
当利用docker run来创建容器时,Docker 在后台运行的流程如下所示:
检查本地是否存在指定的镜像,不存在就从公有仓库下载
利用镜像创建并启动一个容器
分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去 (docker网桥,有时候看功能有点像交换机)
从地址池配置一个 ip 地址给容器
执行用户指定的应用程序
执行完毕后容器被终止(比如执行完某条命令,如果该命令是常驻进程,那就一直执行)
容器管理的核心是容器执行的应用程序这个进程,所以如果这个进程不是常驻前台的话则执行后容器就会退出了,比如上面我们是执行的 /bin/bash 这个程序,这个程序会常驻前台,所以容器会一直存在,而且这个这个程序在容器中的进程PID=1,即主进程
docker ps:查看运行中的容器
docker ps -a:查看所有容器包括退出的(exited)
docker ps -q:显示容器的id
[root@localhost ~]# docker ps -q
5c400ee50477
[root@localhost ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f1e06abcda96 nginx:test "/docker-entrypoint.…" 7 minutes ago Exited (0) 7 minutes ago nginx3
d40d5aabdc94 nginx:test "/docker-entrypoint.…" 10 minutes ago Exited (130) 7 minutes ago nginx2
5c400ee50477 nginx:test "/docker-entrypoint.…" 11 minutes ago Up 11 minutes 80/tcp nginx1
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5c400ee50477 nginx:test "/docker-entrypoint.…" 11 minutes ago Up 11 minutes 80/tcp nginx1
[root@localhost ~]# docker ps -aq
f1e06abcda96
d40d5aabdc94
5c400ee50477
docker rm -f 容器名或id
-f:强制
批量删除可以 docker rm $(docker ps -q)
默认运行中的容器不能删除,可以先docker stop $(docker ps -aq),再删除
更多的时候,我们需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加-d参数来实现
[root@localhost ~]# docker run nginx /bin/sh -c "while true;do echo 'hello';sleep 1; done;"
hello
hello
hello
hello
hello
^C[root@localhost ~]docker run -d nginx /bin/sh -c "while true;do echo 'hello';sleep 1; done;"
874c5da228d88a3866e0baf537e91efd9c0e99b44c42609bb3b7ea115690bb1b
返回的是容器id
后面的格式其实是command arg arg ...
-c:将后面的参数当成命令执行
输出的结果可以用docker logs [container name or id]查看,执行的命令输出的内容归进日志中,容器的日志以标准输出的形式输出到主机上的容器的根目录下
注:容器是否会长久运行,是和 docker run 指定的命令有关,和 -d 参数无关。
可以使用docker stop [container ID or NAMES]来终止一个运行中的容器。此外,当 Docker 容器中指定的应用终结时,容器也自动终止。例如前面只启动了一个终端的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止
docker stop [containerd name or id]
启动容器那就是换成start
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
874c5da228d8 nginx "/docker-entrypoint.…" 7 minutes ago Up 7 minutes 80/tcp youthful_jang
5c400ee50477 nginx:test "/docker-entrypoint.…" 25 minutes ago Up 25 minutes 80/tcp nginx1
[root@localhost ~]# docker stop 874c5da228d8
874c5da228d8
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5c400ee50477 nginx:test "/docker-entrypoint.…" 25 minutes ago Up 25 minutes 80/tcp nginx1
[root@localhost ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
874c5da228d8 nginx "/docker-entrypoint.…" 8 minutes ago Exited (137) 10 seconds ago youthful_jang
cb1eec66ace6 nginx "/docker-entrypoint.…" 8 minutes ago Exited (130) 8 minutes ago vigorous_perlman
f1e06abcda96 nginx:test "/docker-entrypoint.…" 21 minutes ago Exited (0) 21 minutes ago nginx3
d40d5aabdc94 nginx:test "/docker-entrypoint.…" 24 minutes ago Exited (130) 21 minutes ago nginx2
5c400ee50477 nginx:test "/docker-entrypoint.…" 25 minutes ago Up 25 minutes 80/tcp nginx1
注意容器状态的变化
有个运行中的熔器,进入它用exec
docker exec -it 容器名或id /bin/bash
exit退出
docker container rename 旧容器名 新容器名
前面的run等命令一样,container是缺省选项,可以不写
docker inspect 容器名或id
一般是用来传命令文件,因为容器内默认不少命令不具备
docker container cp 容器id:文件路径 本地路径
docker container cp 本地路径 容器id:文件路径
容器,业务层面看,就是让你将运行在宿主机上的应用或成运行在容器中
你可以在容器上部署wodpress,wecenter,设置nginx的各种负载均衡,总之,用容器,就转变自己的看待应用或服务器的方式,将他们部署到容器上,直接下载已经设置好功能的镜像也好,还是下载基础镜像自己来操作也一样。
(实际上,生成环境并不会直接操作容器来部署服务比如kubernetes是操控它的最小单位pod来部署服务,一个pod中有多个容器,这就是容器编排技术)
建议大家看完容器后去学kubernetes,你会发现容器只是冰山一角,很少说手动去设置容器,都是通过上层配置去自动设置容器
注:容器默认将容器内的第一进程作为容器是否在运行的依据
分析下:docker run -it nginx ls
其实第一进程是shell,它运行ls,执行完ls,shell的任务也就完成了,退出了,那么容器也退出,如果是/bin/bash,这是个交互式,常驻的,自然不会退出
这里有个坑,nginx,更多是不是应该作为一个常驻进程一直提供服务,但有些版本就是一致性往就退出,解决方法,一是将nginx变成守护进程,还有一种也是我们常用的就是写个死循环:
docker run -d nginx /bin/sh -c "while true;do echo 'hello';sleep 10;done"
这样容器就不会退出,nginx进程就继续提供服务
建议用nginx用新版的镜像,新版本的镜像默认解决了这个问题,不用我们手动解决
(docker help)
Docker 容器更多情况下是用来运行 Web 应用的,所以要如何访问到容器中的 Web 服务呢?
访问web,跟平时我们用浏览器一样,无非就是ip:port和域名两种,对于容器一般用ip:port
[root@localhost ~]# docker run -d --name web nginx
7f151d5df4ea700f82efd3070140dfc0212822aa5b01d0061fa593532a6b44bd
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7f151d5df4ea nginx "/docker-entrypoint.…" 3 seconds ago Up 2 seconds 80/tcp web
[root@localhost ~]# docker inspect web | grep IPAddress
"SecondaryIPAddresses": null,
"IPAddress": "172.17.0.2",
"IPAddress": "172.17.0.2",
访问nginx服务
[root@localhost ~]# curl 172.17.0.2
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
宿主机ip是192.168.23.174,没有做域名解析,域名访问不了还能理解,为什么ip能访问,因为有docker网桥,所以运行着宿主机和容器能通信
但是这样不够方便,因为启动容器的代价很小,一重启容器的 IP 就变了,是而且你这样只是运行着该容器的宿主技能访问,应该让外部的主机也能访问,否能够通过宿主机的方式去访问,这样只要/etc/hosts解析,能访问宿主机,就能访问容器的服务
就要用到端口暴露
-P:随机端口暴露
-p 8080:80将宿主机的8080端口绑定到容器的80端口,就可以通过宿主机的8080端口访问容器了
注:格式要注意,docker最好用这个格式写
docker run 等参数 镜像名 command arg arg...
[root@localhost ~]# docker run -d --name web2 -p 8080:80 nginx
18fcba88a2744762aa4f4a23213d71aa66cb6d231984eaac4c770c58d99b2f83
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
18fcba88a274 nginx "/docker-entrypoint.…" 4 seconds ago Up 3 seconds 0.0.0.0:8080->80/tcp web2
7f151d5df4ea nginx "/docker-entrypoint.…" 14 minutes ago Up 14 minutes 80/tcp web
[root@localhost ~]# curl http://localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
也可以在其他主机用这个主机的ip:8080来访问
当 Docker 进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的 Docker 容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中(同一个网段,可互相通信,即使每个容器有自己的namespace,有自己的ip,但大家连在同一个交换机上,网段相同的,视docker0网桥设备为网关,所以大家可以通过ip访问)。从 docker0 子网中分配一个 IP 给容器使用,并设置 docker0 的 IP 地址为容器的默认网关(进出口路由)。在主机上创建一对虚拟网卡veth pair设备,Docker 将 veth pair 设备的一端放在新创建的容器中,并命名为 eth0(容器的网卡),另一端放在主机中,以vethxxx这样类似的名字命名,并将这个网络设备加入到 docker0 网桥中。可以通过brctl show命令查看:
brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024286df8f39 no veth1040b0a
veth5a2ba56
veth7aa7e71
bridge 模式是 docker 的默认网络模式,使用docker run -p时,实际上是通过 iptables 做了DNAT规则,实现端口转发功能。可以使用iptables -t nat -vnL查看。bridge模式如下图所示:
docker0网桥和veth在宿主机中
访问宿主机的时,不论是内网还是外网网卡接收,最后都会有张网卡或者说网络接口将强求容器的流量转发给docker0,在有veth转发给容器的网卡(注意veth pair设备是虚拟网卡,所以理解是最好看成容器连接到docker0网桥)
自定义网络一般是用桥接网络来做
一个新的容器想要和已经存在的容器建立互连关系,可以使用 --link 命令
docker run -itd --link 已经存在的容器的名称 镜像名 command
–link是单方面的,就是新容器可以访问被连接的就容器,但反过来不行
这个时候我们可以通过自定义网络的方式来实现互联互通
先自定义一个网络
docker network create -d bridge my-net
使用该网络运行一个容器
docker run -itd --rm --name busybox1 --network my-test busybox sh
--rm容器用完,退出就删除
--network指定网络格式
同样的命令在建立容器,它们在同一个网络下,进入任意一个容器,ping ip或容器名称,比如ping busybox1,会发现怎样ping都行,它们是互通的
注意:这里细分出了一个网络,不同网络间的容器默认不能互相访问的
如果启动容器的时候使用 host 模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个 Network Namespace。容器将不会虚拟出自己的网卡,配置自己的 IP 等,而是使用宿主机的 IP 和端口。但是,容器的其他方面,如文件系统、进程列表等还是和宿主机隔离的。 Host模式如下图所示:
使用 host 模式也很简单,只需要在运行容器的时候指定 --net=host 即可
注意这时候容器的端口暴露设置就要小心了,引用用的是宿主机的端口,小心端口冲突,最好就不要设置端口
这也正是pod中的容器的网络模式
这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。 Container 模式如下图所示:
在运行容器的时候指定 --net=container:目标容器名 即可。实际上我们后面要学习的 Kubernetes 里面的 Pod 中容器之间就是通过 Container 模式链接到 pause(infrana) 容器上面的,所以容器之间直接可以通过 localhost 来进行访问。
注意这里的先存在的容器一般是bridge模式
使用 non e模式,Docker 容器拥有自己的 Network Namespace,但是并不为Docker 容器进行任何网络配置。也就是说这个 Docker 容器没有网卡、IP、路由等信息。需要我们自己为 Docker 容器添加网卡、配置 IP 等。 None模式示意图如下所示:
容器管理数据的两种方式:
1.数据卷(Data Volumes)
2.挂载主机目录 (Bind mounts)
数据卷可以在容器之间共享和重用
对数据卷的修改会立马生效
对数据卷的更新,不会影响镜像
数据卷默认会一直存在,即使容器被删除
注意:数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,显示的是挂载的 数据卷。卸载数据卷,就又可以看到原本的文件了。挂载过程目录就像进入文件系统的大门,卷也可以看成一种文件系统,只要是挂载,都可以看成文件系统
[root@localhost ~]# docker volume create v1
v1
[root@localhost ~]# docker volume ls
DRIVER VOLUME NAME
local v1
docker volume inspect v1
启动一个挂载数据卷的容器:在用docker run命令的时候,使用–mount或者-v标记来将数据卷挂载到容器里。下面创建一个名为 web 的容器,并加载一个数据卷到容器的 /usr/share/nginx/html 目录:
docker run -d -p 8080:80 --name web -v my-vol:/usr/share/nginx/html nginx
一般是用来挂载服务需要的配置文件和数据文件
数据卷是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用docker rm -v这个命令。 无主的数据卷可能会占据很多空间,要清理请使用以下命令:
docker volume prune 清理无用的volume
也可以是container 清理停止运行的容器
也可以是悬挂的镜像(image),就是没有被任意一个容器当做是镜像层来用的镜像 清理所有悬挂的镜像
prune是批量清理,那自然要慎用的,尽量别用这个
清理掉自然回收了空间
注意是目录
Docker 同样支持把宿主机上的目录挂载到容器中,同样可以使用 -v 或者 --mount 参数来进行挂载
命令形式跟挂载数据卷一样,就是将数据卷的名称换成主机的绝对路径
docker run -it -v /tmp:/usr/tmp busybox /bin/sh
默认挂载的路径权限为读写。如果指定为只读可以用:ro,如:-v /tmp:/usr/tmp:ro。
– 容器目录不可以为相对路径
– 宿主机目录如果不存在,则会自动生成
– 挂载宿主机已存在目录后,在容器内对其进行操作,报“Permission denied”。可通过两种方式解决:
1> 关闭selinux。
临时关闭:# setenforce 0
永久关闭:修改/etc/sysconfig/selinux
文件,将 SELINUX 的值设置为disabled。
2> 以特权方式启动容器
指定--privileged
参数,如:
# docker run -it --privileged=true -v /test:/soft centos /bin/bash
挂载主机目录进容器最好开启–privileged特权模式
bind mount 和 volume 其实都是利用宿主机的文件系统,不同之处在于 volume 是 docker 自身管理的目录中的子目录,所以不存在权限引发的挂载的问题,并且目录路径是 docker 自身管理的,所以也不需要在不同的服务器上指定不同的路径,你不需要关心路径。它们之间的主要区别有如下几点:
volume 会引起 docker 目录膨胀,因为既要存镜像,又要存 volume,最好不要放在系统盘,将 docker 的安装目录配置到其他更大的挂载盘
两者有一个不同的行为:当容器外的对应目录是空的,volume 会先将容器内的内容拷贝到容器外目录,而 mount 会将外部的目录覆盖容器内部目录
**volume 还有一个不如 bind mount 的地方,不能直接挂载文件,**例如挂载 nginx 容器的配置文件:nginx.conf
镜像的定制实际上就是定制镜像的每一层所添加的配置、文件等信息,实际上当我们在一个容器中添加或者修改了一些文件后,我们可以通过docker commit命令来将容器生成一个新的镜像,但是这个方法不够直观,没办法追溯我们镜像里面到底有哪些内容,所以实际定制镜像的过程我们很少采用这种方式。而是使用一个名为 Dockerfile 的文本文件来进行镜像定制,我们可以把镜像的每一层修改、安装、构建、操作的命令都写入到这个文件中,一行就对应镜像的一层,这种方法显然要更高级,因为我们有一个文件来直观反映我们的镜像内容,还可以作为版本记录进行跟踪。
docker commit -a="aaa" -m="nginxok" nginx1 nginx:ok
-a:作者信息
-p:commit时将容器暂停
-m:镜像说明
以我们之前的 nginx 镜像为例,我们现在来定制一个 nginx 镜像,要求默认的应用页面访问的内容是 Hello Docker。
首先我们新建一个目录 testnginx,然后在该目录下面新建一个名为Dockerfile的空白文本文件:
mkdir -p testnginx && cd testnginx && touch Dockerfile
然后向Dockerfile文件中添加如下内容:
FROM nginx
RUN echo 'Hello Docker!' > /user/share/nginx/html/index.html
这个文件很简单,一共就两行,其中包含两条指令,FROM 和 RUN。两层镜像。
/usr/share/nginx/html是nginx默认的站点目录
定制镜像,那么肯定是一个镜像为基础,在其基础上进行定制,而 FROM 这个指令就是来指定基础镜像的,所以在一个 Dockerfile 文件中 FROM 指令是必备的,并且必须是第一条指令。
在 Docker Hub 上有非常多的高质量的官方镜像,有一些可以直接拿来使用的服务类的镜像,如 nginx、redis、mongo、mysql、httpd、php、tomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node、openjdk、python、ruby、golang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。(除非是很特殊的要求,不然不会去定制镜像,因为网上的镜像功能很完好)
如果没有找到合适的基础镜像,则可以使用官方提供的一些更为基础的操作系统镜像,比如 ubuntu、debian、centos、alpine 等,这些基础镜像为我们提供了更大的扩展空间,就类似于平时我们在操作系统上面部署自己的服务一样的操作。(建议使用Alpine映像,因为它受到严格控制且较小(当前小于5 MB),同时仍是完整的 Linux 发行版)
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch,这个镜像是一个虚拟的镜像,并不实际存在,表示一个空白的镜像:
FROM scratch
...
如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。有的同学可能感觉很奇怪,没有任何基础镜像,我怎么去执行我的程序呢,其实对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接FROM scratch会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有很多观点认为 Go 是特别适合容器微服务架构的语言的原因之一。
你可以给镜像添加标签来帮助组织镜像、记录许可信息、辅助自动化构建等。每个标签一行,由 LABEL 开头加上一个或多个标签对。
下面的示例展示了各种不同的可能格式。#开头的行是注释内容。
注意
如果你的字符串包含空格,那么它必须被引用或者空格必须被转义。如果您的字符串包含内部引号字符("),则也可以将其转义。
#Set one or more individual labels
LABEL com.example.version="0.0.1-beta"
LABEL vendor="ACME Incorporated"
LABEL com.example.release-date="2015-02-12"
LABEL com.example.version.is-production=""
一个镜像可以包含多个标签,当然以上内容也可以写成下面这样,但是不是必须的:
#Set multiple labels at once, using line-continuation characters to break long lines
LABEL vendor=ACME\ Incorporated \
com.example.is-production="" \
com.example.version="0.0.1-beta" \
com.example.release-date="2015-02-12"
为了保持 Dockerfile 文件的可读性,以及可维护性,建议将长的或复杂的 RUN 指令用反斜杠\分割成多行。
RUN 指令是用来执行命令行命令的。由于命令行的强大能力,所以 RUN 指令是定制镜像时是最常用的指令之一。其格式有两种:
shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式:
RUN echo 'Hello, Docker!' > /usr/share/nginx/html/index.html
exec 格式:RUN [“可执行文件”, “参数1”, “参数2”],就像是[“/bin/bash”,“-c”,“echo 'hello ’ > test”],这更像是函数调用中的格式。
既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:
FROM debian:jessie
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-3.2.5.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 的人常犯的一个错误。
UnionFS 层限制
UnionFS 实际上是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。
上面的 Dockerfile 正确的写法应该是这样:
FROM debian:jessie
RUN 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-3.2.5.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 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。
WORKDIR 指令设置 Dockerfile 中的任何 RUN,CMD,ENTRPOINT,COPY 和 ADD 指令的工作目录。如果 WORKDIR 指定的目录不存在,即使随后的指令没有用到这个目录,都会创建该目录。
格式: WORKDIR /path/to/workdir
为了清晰性和可靠性,你应该总是在 WORKDIR 中使用绝对路径,而且单个 Dockerfile 可以使用多次WORKDIR。另外,我们应该使用 WORKDIR 来替代类似于 RUN cd … && do-something 的指令,后者难以阅读、排错和维护。
(切换的是镜像内的路径)
Dockerfile 中的 COPY 指令和 ADD 指令都可以将主机上的资源复制或加入到容器镜像中,都是在构建镜像的过程中完成的。
COPY 指令和 ADD 指令的唯一区别在于是否支持从远程 URL 获取资源。COPY 指令只能从执行docker build所在的主机上读取资源并复制到镜像中。而 ADD 指令还支持通过 URL 从远程服务器读取资源并复制到镜像中。
一般来说满足同等功能的情况下,推荐使用COPY指令。ADD 指令更擅长读取本地 tar 文件并解压缩。
COPY 指令能够将构建命令所在的主机本地的文件或目录,复制到镜像文件系统。COPY 指令同样也支持 exec 和 shell 两种格式:
exec 格式用法:COPY ["" ,... "" ],特别适合路径中带有空格的情况。
shell 格式用法:COPY <src>... <dest>
(cp 多个源 目标路径)
ADD 指令不仅能够将构建命令所在的主机本地的文件或目录,而且能够将远程 URL 所对应的文件或目录,作为资源复制到镜像文件系统。所以,可以认为 ADD 是增强版的 COPY,支持将远程 URL 的资源加入到镜像的文件系统。同样也支持 exec 和 shell 两种格式用法: * exec 格式用法:ADD [“”,… “”],特别适合路径中带有空格的情况
shell 格式用法:ADD <src>... <dest>
从远程 URL 获取资源,比如:
ADD http://foo.com/bar.go /tmp/main.go
不过需要注意的是对于从远程 URL 获取资源的情况,由于 ADD 指令不支持认证,如果从远程获取资源需要认证,则只能使用RUN wget 或 RUN curl 替代了。
(优先考虑wget和curl)
有能力自动解压文件,比如:
ADD ./foo.tar.gz /tmp/
上述指令会使 foo.tar.gz 压缩文件解压到容器的 /tmp 目录。
不过一般来说虽然 ADD 指令支持从远程获取资源,但是并不推荐使用,而是建议使用 RUN 指令去执行 wget 或 curl 命令。
比如前面我们定制的 nginx 镜像,可以改成下面的形式:
echo 'Hello Docker!' > index.html
然后修改 Dockerfile:
FROM nginx
# COPY或者ADD指令都可以
COPY index.html /user/share/nginx/html/index.html
COPY 指令和 ADD指令的用法非常相似,具体注意事项如下:
源路径可以有多个
源路径是相对于执行 build 的相对路径
源路径如果是本地路径,必须是构建上下文中的路径
源路径如果是一个目录,则该目录下的所有内容都将被加入到容器,但是该目录本身不会
目标路径必须是绝对路径,或相对于 WORKDIR 的相对路径
目标路径如果不存在,则会创建相应的完整路径
目标路径如果不是一个文件,则必须使用/结束
路径中可以使用通配符
上下文路径,就是你构建镜像时的指定的目录,一般就是要用到的资源的目录
EXPOSE 指令用于指定容器将要监听的端口。因此,你应该为你的应用程序使用常见的端口。
例如,提供 Apache web 服务的镜像应该使用 EXPOSE 80,而提供 MongoDB 服务的镜像使用 EXPOSE 27017。
对于外部访问,用户可以在执行 docker run 时使用一个 -p 参数来指示如何将指定的端口映射到所选择的端口。
为了方便新程序运行,你可以使用 ENV 指令来为容器中安装的程序更新 PATH 环境变量。例如使用ENV PATH /usr/local/nginx/bin:$PATH 来确保CMD [“nginx”]能正确运行。
ENV 指令也可用于为你想要容器化的服务提供必要的环境变量,比如 Postgres 需要的 PGDATA。 最后,ENV 也能用于设置常见的版本号,比如下面的示例:
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
类似于程序中的常量,这种方法可以让你只需改变 ENV 指令来自动的改变容器中的软件版本。
VOLUME 指令用于暴露任何数据库存储文件,配置文件,或容器创建的文件和目录。强烈建议使用 VOLUME 来管理镜像中的可变部分和用户可以改变的部分。
USER 用户名
如果某个服务不需要特权执行,建议使用 USER 指令切换到非 root 用户。先在 Dockerfile 中使用类似
RUN groupadd -r postgres && useradd -r -g postgres postgres
的指令创建用户和用户组。
注意
在镜像中,用户和用户组每次被分配的 UID/GID 都是不确定的,下次重新构建镜像时被分配到的 UID/GID
可能会不一样。如果要依赖确定的 UID/GID,你应该显示的指定一个 UID/GID。
你应该避免使用 sudo,因为它不可预期的 TTY 和信号转发行为可能造成的问题比它能解决的问题还多。如果你真的需要和 sudo 类似的功能(例如,以 root 权限初始化某个守护进程,以非 root 权限执行它),你可以使用 gosu。我们可以去查看官方的一些镜像,很多都是使用的 gosu。
为了减少层数和复杂度,避免频繁地使用 USER 来回切换用户。
尽管 ENTRYPOINT 和 CMD 都是在容器里执行一条命令, 但是他们有一些微妙的区别,在绝大多数情况下, 你只要在这2者之间选择一个调用就可以,但是我们还是非常有必要来认真了解下二者的区别。
CMD 指令是容器启动以后,默认的执行命令,需要重点理解下这个默认的含义,意思就是如果我们执行 docker run 没有指定任何的执行命令或者 Dockerfile 里面也没有指定 ENTRYPOINT,那么就会使用 CMD 指定的执行命令执行了。这也说明了 ENTRYPOINT 才是容器启动以后真正要执行的命令。
所以我们经常遇到 CMD 会被覆盖 的情况,为什么会被覆盖呢?主要还是因为 CMD 的定位就是默认,如果不额外指定,那么才会执行 CMD 命令,但是如果我们指定了的话那就不会执行 CMD 命令了,也就是说 CMD 会被覆盖。
CMD 总共有三种用法:
CMD ["executable", "param1", "param2"] # exec 形式
CMD ["param1", "param2"] # 作为 ENTRYPOINT 的默认参数
CMD command param1 param2 # shell 形式
其中 shell 形式,就是没有中括号的形式,命令 command 默认是在/bin/sh -c下执行的,比如:
FROM busybox
CMD echo "hello cmd shell form!"
我们将上面的 Dockerfile 打包成 cmdshell 镜像,然后直接启动一个容器:
$ docker build -t cmdshell .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM busybox
---> 020584afccce
Step 2/2 : CMD echo "hello cmd shell form!"
---> Running in 651afaddb83d
Removing intermediate container 651afaddb83d
---> d26e4d6d9cdf
Successfully built d26e4d6d9cdf
Successfully tagged cmdshell:latest
$ docker run cmdshell
hello cmd shell form!
对于带有中括号的 exec 形式,命令没有在任何 shell 终端环境下,如果我们要执行 shell,必须把 shell 加入到中括号的参数中。将上面的例子修改为:
FROM busybox
CMD ["/bin/sh", "-c", "echo 'hello cmd exec form!'"]
同样将上面的 Dockerfile 打包成 cmdexec 镜像,然后直接启动一个容器:
$ docker build -t cmdexec .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM busybox
---> 020584afccce
Step 2/2 : CMD ["/bin/sh", "-c", "echo 'hello cmd exec form!'"]
---> Running in 8c72d5e2ce35
Removing intermediate container 8c72d5e2ce35
---> c393bd1ab3c1
Successfully built c393bd1ab3c1
Successfully tagged cmdexec:latest
$ docker run cmdexec
hello cmd exec form!
需要注意,采用 exec 形式,第一个参数必须是命令的全路径才行。一个 Dockerfile 如果有多个 CMD,只有最后一个生效,官网推荐采用这种方式。
当然,以上都是体现了 CMD 的 默认 行为。如果我们在 run 时指定了命令或者有 ENTRYPOINT CMD 就会被覆盖。比如同样用上面两个镜像,在运行的时候指定一个命令:
$ docker run cmdexec echo 'hello docker'
hello docker
$ docker run cmdshell echo 'hello docker'
hello docker
可以看到,最终容器里面执行的是 run 命令后面的命令,而不是 CMD 里面定义的。
根据官方定义来说 ENTRYPOINT 才是用于定义容器启动以后的执行程序的,允许将镜像当成命令本身来运行(用 CMD 提供默认选项),从名字也可以理解,是容器的入口。ENTRYPOINT 一共有两种用法:
ENTRYPOINT ["executable", "param1", "param2"] (exec 形式)
ENTRYPOINT command param1 param2 (shell 形式)
对应命令行 exec 模式,也就是带中括号的。和 CMD 的中括号形式是一致的,但是这里貌似是在shell的环境下执行的,与cmd有区别。如果 run 命令后面有执行命令,那么后面的全部都会作为 ENTRYPOINT 的参数。如果 run 后面没有额外的命令,但是定义了 CMD,那么 CMD 的全部内容就会作为 ENTRYPOINT 的参数,这同时是上面我们提到的 CMD 的第二种用法。所以说 ENTRYPOINT 不会被覆盖。当然如果要在 run 里面覆盖,也是有办法的,使用–entrypoint参数即可。(就是如果用entrypoint,cmd和run会成为它的参数)
比如我们定义如下的 Dockerfile:
FROM busybox
CMD ["I am in cmd exec form"]
ENTRYPOINT ["echo"]
将上面的 Dockerfile 打包成镜像 entrypointest,然后直接运行,不带任何参数:
$ docker build -t entrypointest .
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM busybox
---> 020584afccce
Step 2/3 : CMD ["I am in cmd exec form"]
---> Running in 2d7b13b0dfe7
Removing intermediate container 2d7b13b0dfe7
---> 903d739ead9a
Step 3/3 : ENTRYPOINT ["echo"]
---> Running in c61682ea476e
Removing intermediate container c61682ea476e
---> 00b09a578d48
Successfully built 00b09a578d48
Successfully tagged entrypointest:latest
$ docker run entrypointest
I am in cmd exec form
我们可以看到打印的结果是 CMD 里面指定的内容,也就是默认情况将 CMD 部分作为 ENTRYPOINT 的参数了。但是如果我们在运行容器的时候如果指定了运行参数呢:
$ docker run entrypointest I am in run section
I am in run section
我们可以看到运行时指定的参数会覆盖掉 CMD 提供的默认参数,但是默认都是执行的 ENTRYPOINT 里面的命令。
对于 shell 模式的,任何 run 和 CMD 的参数都无法被传入到 ENTRYPOINT 里。官网推荐用上面一种用法。比如我们我们这里定义一个 Dockerfile 如下:
FROM busybox
CMD ["I am in cmd exec form and entrypoint shell form"]
ENTRYPOINT echo
将上面 Dockerfile 打包成镜像 entrypointshell,然后直接运行:
$ docker build -t entrypointshell .
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM busybox
---> 020584afccce
Step 2/3 : CMD ["I am in cmd exec form and entrypoint shell form"]
---> Running in 2aee7326f4cd
Removing intermediate container 2aee7326f4cd
---> e89cadeeecd3
Step 3/3 : ENTRYPOINT echo
---> Running in f359c8bb5025
Removing intermediate container f359c8bb5025
---> f9d4e1d1b0a0
Successfully built f9d4e1d1b0a0
Successfully tagged entrypointshell:latest
$ docker run entrypointshell
我们可以发现 CMD 的参数并没有被打印出来,如果在运行的时候添加上参数呢:
$ docker run entrypointshell I am in run section
$
我们可以发现也没有将 run 命令后面的参数打印出来。所以一般情况下对于 ENTRYPOINT 来说使用中括号的 exec 形式更好。
总结
一般会用 ENTRYPOINT 的中括号形式作为 Docker 容器启动以后的默认执行命令,里面放的是不变的部分,可变部分比如命令参数可以使用 CMD 的形式提供默认版本,也就是 run 里面没有任何参数时使用的默认参数。如果我们想用默认参数,就直接 run,否则想用其他参数,就 run 里面加上参数。
整体习惯,用[]参数形式,不用shell形式
如果我们仔细观察的话会看到 docker build 命令最后有一个 .。. 表示当前目录,而 Dockerfile 就在当前目录,因此不少初学者以为这个路径是在指定 Dockerfile 所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定上下文路径。那么什么是上下文呢?
首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker Daemon 和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。(就记住一点,我们执行的命令是在客户端,详细的操作是docker的服务端,只是一般服务端和客户端在同一台机器上,不容易区分)
当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD 指令等。而 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?
这就引入了上下文的概念。当构建的时候,**用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。**如果在 Dockerfile 中这么写:
COPY ./package.json /app/
这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json。
因此,COPY 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY …/package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。
现在就可以理解刚才的命令docker build -t nginx:v1 .中的这个.,实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。
如果观察 docker build 输出,我们其实已经看到了这个发送上下文的过程:
docker build -t nginx:v1 .
Sending build context to Docker daemon 2.048 kB
理解构建上下文对于镜像构建是很重要的,可以避免犯一些不应该的错误。比如有些初学者在发现 COPY /opt/xxxx /app 不工作后,于是干脆将 Dockerfile 放到了硬盘根目录去构建,结果发现 docker build 执行时发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build 打包整个硬盘,这显然是使用错误。
一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎(dockerd守护进程)的。
那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用-f …/Dockerfile.Dev参数指定某个文件作为 Dockerfile。
当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。
下载和推送镜像,默认都是对docker hub
首先需要 https://cloud.docker.com 免费注册一个 Docker 账号。
登录
执行docker login命令交互式的输入用户名及密码来完成在命令行界面登录 Docker Hub。
docker login -u xxx -p xxx
[root@localhost tmp]# docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: 010101010007
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
默认登录docker hub
docker logout就是退出登录
用户在登录后可以通过docker push命令来将自己的镜像推送到 Docker Hub
推送前要对镜像打标签
以下命令中的 username 请替换为你的 Docker 账号用户名。
[root@localhost tmp]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 605c77e624dd 3 months ago 141MB
nginx test 605c77e624dd 3 months ago 141MB
[root@localhost tmp]# docker tag nginx:latest 010101010007/nginx:ok
[root@localhost tmp]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
010101010007/nginx ok 605c77e624dd 3 months ago 141MB
nginx latest 605c77e624dd 3 months ago 141MB
nginx test 605c77e624dd 3 months ago 141MB
[root@localhost tmp]# docker push 010101010007/nginx:ok
The push refers to repository [docker.io/010101010007/nginx]
d874fd2bc83b: Layer already exists
32ce5f6a5106: Layer already exists
f1db227348d0: Layer already exists
b8d6e692a25e: Layer already exists
e379e8aedd4d: Layer already exists
2edcec3590a4: Layer already exists
ok: digest: sha256:ee89b00528ff4f02f2405e4ee221743ebc3f8e8dd0bfd5c4c20a2fa2aaa7ede3 size: 1570
你的用户名加软件名就是个仓库
镜像打标签时,除了dockerhub的情况,其他的仓库,如果镜像含有ip:port或者域名,足以判断出push或pull操作对应的仓库是哪个
创建个镜像仓库
可以创建多种类型的仓库
创建仓库是明明还是正常点比如nginx
阿里的仓库的命名逻辑还是有点不一样的,想这个命名空间,其实就像是dockerhub的用户名
一样分公有私有
[root@localhost tmp]# docker login --username=德德乒 registry.cn-hangzhou.aliyuncs.com
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
[root@localhost tmp]# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 605c77e624dd 3 months ago 141MB
nginx test 605c77e624dd 3 months ago 141MB
010101010007/nginx ok 605c77e624dd 3 months ago 141MB
[root@localhost tmp]# docker search nginx
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
nginx Official build of Nginx. 16667 [OK]
bitnami/nginx Bitnami nginx Docker Image 122 [OK]
ubuntu/nginx Nginx, a high-performance reverse proxy & we… 43
bitnami/nginx-ingress-controller Bitnami Docker Image for NGINX Ingress Contr… 17 [OK]
rancher/nginx-ingress-controller 10
ibmcom/nginx-ingress-controller Docker Image for IBM Cloud Private-CE (Commu… 4
bitnami/nginx-ldap-auth-daemon 3
bitnami/nginx-exporter 2
rancher/nginx-ingress-controller-defaultbackend 2
circleci/nginx This image is for internal use 2
vmware/nginx 2
vmware/nginx-photon 1
rancher/nginx 1
bitnami/nginx-intel 1
wallarm/nginx-ingress-controller Kubernetes Ingress Controller with Wallarm e… 1
rancher/nginx-conf 0
rancher/nginx-ssl 0
continuumio/nginx-ingress-ws 0
ibmcom/nginx-ppc64le Docker image for nginx-ppc64le 0
rancher/nginx-ingress-controller-amd64 0
ibmcom/nginx-ingress-controller-ppc64le Docker Image for IBM Cloud Private-CE (Commu… 0
rancher/nginx-proxy 0
kasmweb/nginx An Nginx image based off nginx:alpine and in… 0
wallarm/nginx-ingress-controller-amd64 Kubernetes Ingress Controller with Wallarm e… 0
注意,虽然登录了仓库但是个操作如果不知定仓库,还是跟原来的一样,就想search是找dockerhub的
有时候我们可能希望我们的镜像只在局域网范围内使用,不希望推送到 Docker Hub 这样的公共仓库,那么这个时候我们可以创建一个本地仓库供私人使用。
docker-registry 就是是官方提供的一个私有仓库工具,可以用于存储私有的镜像仓库。同样的我们可以通过获取官方 registry 镜像来直接运行:
docker run -d -p 5000:5000 --name registry registry:2
上面的命令会使用官方的 registry 镜像来启动私有仓库容器。默认情况下,仓库会被创建在容器的/var/lib/registry目录下。你可以通过 -v 参数来将镜像文件存放在本地的指定路径。例如下面的例子将上传的镜像放到本地的 /opt/data/registry 目录:
docker run -d \
-p 5000:5000 \
-v /opt/data/registry:/var/lib/registry \
registry:2
这样我们就运行了一个数据持久化的私有镜像仓库。
创建好私有仓库之后,然后可以使用docker tag来标记一个镜像,然后推送它到仓库。比如私有仓库地址为127.0.0.1:5000。先在本机查看已有的镜像。
docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu latest ba5877dc9bec 6 weeks ago 192.7 MB
使用docker tag将 ubuntu:latest 这个镜像标记为 127.0.0.1:5000/ubuntu:latest:
$ docker tag ubuntu:latest 127.0.0.1:5000/ubuntu:latest
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu latest ba5877dc9bec 6 weeks ago 192.7 MB
127.0.0.1:5000/ubuntu:latest latest ba5877dc9bec 6 weeks ago 192.7 MB
然后就可以使用docker push上传标记的镜像:
docker push 127.0.0.1:5000/ubuntu:latest
The push refers to repository [127.0.0.1:5000/ubuntu]
373a30c24545: Pushed
a9148f5200b0: Pushed
cdd3de0940ab: Pushedfc56279bbb33: Pushed
b38367233d37: Pushed
2aebd096e0e2: Pushed
latest: digest: sha256:fe4277621f10b5026266932ddf760f5a756d2facd505a94d2da12f4f52f71f5a size: 1568
docker push 仓库地址 镜像
此外,我们还可以使用 registry 仓库提供的 API 来查看仓库中的镜像:
curl 127.0.0.1:5000/v2/_catalog
{"repositories":["ubuntu"]}
这里可以看到 {“repositories”:[“ubuntu”]},表明镜像已经被成功上传了。
先删除已有镜像,再尝试从私有仓库中下载这个镜像。
$ docker image rm 127.0.0.1:5000/ubuntu:latest
$ docker pull 127.0.0.1:5000/ubuntu:latest
Pulling repository 127.0.0.1:5000/ubuntu:latest
ba5877dc9bec: Download complete
511136ea3c5a: Download complete
9bad880da3d2: Download complete
25f11f5fb0cb: Download complete
ebc34468f71d: Download complete
2318d26665ef: Download complete
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
127.0.0.1:5000/ubuntu:latest latest ba5877dc9bec 6 weeks ago 192.7 MB
到这里我们就完成了把镜像上传到了私有仓库中的完整过程。
如果你不想使用 127.0.0.1:5000 作为仓库地址,比如想让本网段的其他主机也能把镜像推送到私有仓库。你就得把例如 192.168.199.100:5000 这样的内网地址作为私有仓库地址,这时你会发现无法成功推送镜像。(私有仓库用ip:port表示,push和pull时就会知道仓库地址了)
这是因为 Docker 默认不允许非 HTTPS 方式推送镜像。我们可以通过 Docker 的配置选项来取消这个限制,我们这里是 CentOS 7 系统,同样还是编辑文件/etc/docker/daemon.json,添加如下内容:
{
"registry-mirror": [
"https://registry.docker-cn.com"
],
"insecure-registries": [
"192.168.199.100:5000"
]
}
其中的insecure-registries就是我们添加的内容,然后重启 Docker 之后就可以在局域网内使用我们的私有镜像仓库了。
其实生产环境很少用registry容器来做私有仓库,有个方案就harbor,后续会专门实践harbor的部署配置和使用
在镜像的构建过程中,Docker 根据 Dockerfile 指定的顺序执行每个指令。在执行每条指令之前,Docker 都会在缓存中查找是否已经存在可重用的镜像,如果有就使用现存的镜像,不再重复创建。当然如果你不想在构建过程中使用缓存,你可以在 docker build 命令中使用 --no-cache=true 选项。Docker 中构建缓存遵循的基本规则如下:
从一个基础镜像开始(FROM 指令指定),下一条指令将和该基础镜像的所有子镜像进行匹配,检查这些子镜像被创建时使用的指令是否和被检查的指令完全一样。如果不是,则缓存失效。
对于 ADD 和 COPY 指令,镜像中对应文件的内容也会被检查,每个文件都会计算出一个校验值。在缓存的查找过程中,会将这些校验和已存在镜像中的文件校验值进行对比。如果文件有任何改变,则缓存失效。
除了 ADD 和 COPY 指令,缓存匹配过程不会查看临时容器中的文件来决定缓存是否匹配。例如,当执行完 RUN apt-get -y update 指令后,容器中一些文件被更新,但 Docker 不会检查这些文件。这种情况下,只有指令字符串本身被用来匹配缓存。
一旦缓存失效,所有后续的 Dockerfile 指令都将产生新的镜像,缓存不会被使用。
多阶段构建可以让我们大幅度减小最终的镜像大小,而不需要去想办法减少中间层和文件的数量。因为镜像是在生成过程的最后阶段生成的,所以可以利用生成缓存来最小化镜像层。
例如,如果你的构建包含多个层,则可以将它们从变化频率较低(以确保生成缓存可重用)到变化频率较高的顺序排序:
安装构建应用程序所需的依赖工具
安装或更新依赖项
构建你的应用
比如我们构建一个 Go 应用程序的 Dockerfile 可能类似于这样:
FROM golang:1.11-alpine AS build
#安装项目需要的工具
#运行 `docker build --no-cache .` 来更新依赖
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
#通过 Gopkg.toml 和 Gopkg.lock 获取项目的依赖
#仅在更新 Gopkg 文件时才重新构建这些层
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
#安装依赖库
RUN dep ensure -vendor-only
#拷贝整个项目进行构建
#当项目下面有文件变化的时候该层才会重新构建
COPY . /go/src/project/
RUN go build -o /bin/project
#将打包后的二进制文件拷贝到 scratch 镜像下面,将镜像大小降到最低
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
为了降低复杂性、减少依赖、减小文件大小和构建时间,应该避免安装额外的或者不必要的软件包。例如,不要在数据库镜像中包含一个文本编辑器。
每个容器应用只关心一个方面的事情(lnmp拆分)。将多个应用解耦到不同容器中,可以更轻松地保证容器的横向扩展和复用。例如一个 web 应用程序可能包含三个独立的容器:web应用、数据库、缓存,每个容器都是独立的镜像,分开运行。但这并不是说一个容器就只能跑一个进程,因为有的程序可能会自行产生其他进程,比如 Celery 就可以有很多个工作进程。虽然每个容器跑一个进程是一条很好的法则,但这并不是一条硬性的规定。我们主要是希望一个容器只关注一件事情,尽量保持干净和模块化。
如果容器互相依赖,你可以使用 Docker 容器网络 来把这些容器连接起来,我们前面已经跟大家讲解过 Docker 的容器网络模式。
在很早之前的版本中尽量减少镜像层数是非常重要的,不过现在的版本已经有了一定的改善了:
只有 RUN、COPY 和 ADD 指令会创建层,其他指令会创建临时的中间镜像,但是不会直接增加构建的镜像大小了。(FROM应该也会)
多阶段构建的支持,允许我们把需要的数据直接复制到最终的镜像中,这就允许我们在中间阶段包含一些工具或者调试信息了,而且不会增加最终的镜像大小。
只要有可能,就将多行参数按字母顺序排序。这可以帮助你避免重复包含同一个包,更新包列表时也更容易,也更容易阅读和审查。建议在反斜杠符号 \ 之前添加一个空格,可以增加可读性。比如下面的例子:
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion
不要使用 RUN yum upgrade 或 dist-upgrade,如果基础镜像中的某个包过时了,你应该联系它的维护者。如果你确定某个特定的包,比如 foo,需要升级,使用 yum install -y foo 就行,该指令会自动升级 foo 包。
永远将 RUN apt-get update 和 apt-get install 组合成一条 RUN 声明,例如:
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo
将 apt-get update 放在一条单独的 RUN 声明中会导致缓存问题以及后续的 apt-get install 失败。比如,假设你有一个 Dockerfile 文件:
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl
构建镜像后,所有的层都在 Docker 的缓存中。假设你后来又修改了其中的 apt-get install 添加了一个包:
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl nginx
Docker 发现修改后的 RUN apt-get update 指令和之前的完全一样。所以,apt-get update 不会执行,而是使用之前的缓存镜像。因为 apt-get update 没有运行,后面的 apt-get install 可能安装的是过时的 curl 和 nginx 版本。
使用RUN apt-get update && apt-get install -y可以确保你的 Dockerfiles 每次安装的都是包的最新的版本,而且这个过程不需要进一步的编码或额外干预。这项技术叫作cache busting(缓存破坏)。
插入一个点:
yum update和yum upgrade的区别,有人说他们更新软件包,但一个更新内核一个不更新,其实不对,都有更新内核,只是yum upgrade会删除旧包,yum update不会,总之习惯用yum update最好