# 容器
# 启动
docker run <image> [command] # 根据一个镜像创建并启动一个容器,还可指定容器的启动命令
-i # --interactive ,保持打开容器的 stdin ,允许输入
-t # --tty ,创建一个伪终端,绑定到容器的 stdin ,供用户操作
-d # --detach ,以 daemon 方式运行,默认在当前终端的前台运行
--init # 使用 docker-init 进程作为容器内的 1 号进程
--rm # 当容器停止时,自动删除它
-u <uid>[:gid] # 指定在容器内使用的用户(默认为 root)
-w <path> # --workdir ,指定容器的工作目录,如果该目录不存在则会自动创建
-e <name>[=value] # --env ,设置容器内的环境变量(可重复使用该命令选项)如果省略 value ,则读取宿主机上的同名环境变量
-l <key>[=<value>] # --label ,给容器添加键值对格式的标签,比如 branch=dev 。如果不指定 value ,则默认赋值为 "" 。可以多次使用该选项
--name <name> # 设置容器的名称
--hostname <name> # 设置容器内的主机名,默认为容器 ID
--privileged # 特权模式,默认不启用。允许在容器内访问所有设备文件,比如挂载磁盘,甚至可以在容器内运行嵌套的容器
--entrypoint 'xx' # 覆盖 Dockerfile 中的 ENTRYPOINT
--pid <namespace> # 指定容器采用的 PID namespace 。比如 --pid=host 是采用宿主机的 namespace ,--pid=container:redis 是采用指定容器的 namespace ,共享进程列表
- 创建容器时,如果本机不存在指定名称的镜像,则会自动从镜像仓库拉取。
- 例:
docker run busybox # 运行一个镜像 docker run -it busybox sh # 创建容器,并进入该容器的终端 docker run -d busybox tail -f /dev/null # 创建一个容器,让它执行一个不会停止的启动命令
- 运行嵌套容器的示例:
docker run -d --name dind --privileged docker:dind # dind 镜像代表 docker in docker ,内置了 dockerd docker exec -it dind sh # 进入 dind 容器 docker run -d nginx # 在 dind 容器内运行嵌套的容器 ps auxf # 查看此时的进程树
# 启动命令
- 用户创建一个容器时,需要指定一条启动命令,否则默认采用镜像声明的启动命令。
- 容器启动之后,用户可以进入容器内终端,执行任意命令,启动其它进程。
- 启动命令执行后,默认会担任容器内 PID 为 1 的进程,即 1 号进程。
- 一旦容器内的 1 号进程退出,或者不在前台运行,dockerd 会自动停止该容器。
- 因此,为了让容器保持运行,容器的启动命令应该一直保持运行,并且在前台运行,比如
tail -f /dev/null
。
- 建议在容器内使用非 root 用户运行应用程序。
- 因为容器内的 root 用户虽然受到限制,没有宿主机的 root 用户那么多特权。但它也是作为 root 用户与内核交互,可能通过内核漏洞逃出容器,成为宿主机的 root 用户。
- 建议每个容器内只运行一个应用程序,使得启动、停止该容器相当于启动、停止该应用,这样方便管理。
- 例如一个容器内只运行一个 Nginx 服务器,它包含多个子进程。
- 如果容器内包含多个进程,建议用 tini、supervisord 等工具管理。
- 执行 docker stop 时,dockerd 只会发送 SIGTERM 信号给容器内的 1 号进程,然后等待 1 号进程清理容器内的其它进程。如果等待超时,则发送 SIGKILL 信号。
- 如果 1 号进程是 shell 解释器,则不会捕捉 SIGTERM 信号,也不会传递信号给子进程,因此 dockerd 只能等超时之后才杀死容器。
- 如果 1 号进程是用户自定义的程序,则可能不会捕捉 SIGTERM 信号、清理僵尸进程。
- 如果 1 号进程为 docker-init 进程(来自 tini (opens new window) 项目),则会以子进程的方式执行容器的启动命令,主要提供两种功能:
- 如果收到 SIGTERM 信号,则终止直接子进程,加上 -g 选项则终止子进程组。
- 如果直接子进程是僵尸进程,则自动清理。
- 执行 docker stop 时,dockerd 只会发送 SIGTERM 信号给容器内的 1 号进程,然后等待 1 号进程清理容器内的其它进程。如果等待超时,则发送 SIGKILL 信号。
- 无状态容器(Stateless)
- :指不需要保持连续运行的容器。其它容器称为有状态容器(Stateful)。
- 这种容器比较方便管理。可以随时重启,甚至随时销毁并从镜像重新创建,不会中断服务、不会丢失数据。
- 例如运行一个 Web 服务器时,如果容器把产生的数据存储到容器外的数据,且当前没有正在进行的 HTTP 通信,则可以重启。
# 重启策略
- 容器的重启策略(restart policy):当容器停止时,是否通过 docker start 重启。
- 如果在 10 秒内连续重启,则重启间隔从 100ms 开始,每次增加一倍,最多增加到 1min 。
- 设置重启策略:
docker run --restart no # 禁止自动重启(默认采用) --restart on-failure # 当容器异常停止时(不包括 dockerd 重启的情况),才会自动重启。该策略还可限制连续重启次数,比如 on-failure:3 --restart unless-stopped # 当容器停止时,就自动重启(除非容器是被 docker stop 了) --restart always # 当容器停止时,总是会自动重启(即使被 docker stop 了,当 dockerd 重启时又会自动重启该容器)
# 资源限制
- 可以限制容器占用的系统资源:
docker run -c 1024 # --cpu-shares ,与其它容器抢占 CPU 时的权重,取值范围为 1~1024 --cpus 1.5 # 限制同时占用 CPU 的核数(每秒的平均值)。默认为 0 即不限制 --cpuset-cpus 0-2,3 # 限制可用的 CPU 核的编号 --cpu-period 100000 # CPU 调度的 CFS 周期的长度。单位为 us ,取值范围为 1ms~1s ,默认为 100000us 即 100ms --cpu-quota 0 # 容器在每个 CFS 周期内占用的 CPU 最大时长。取值范围为 >1ms ,默认为 0 即不限制 -m 256m # --memory ,限制占用的 RAM 内存大小,单位可以是 b、k、m、g 。默认不限制 --memory-swap 0 # 限制占用的 RAM + swap 大小。默认取值为 0 ,相当于为 -m 的两倍。为 -1 时,不限制。与 -m 相等时,会禁用 swap --memory-swappiness # 用 swap 内存的推荐度,取值范围为 0~100 ,0 表示禁用 --oom-kill-disable false # 是否禁止 OOM 杀死进程 --kernel-memory 4m # 限制占用的内核态内存,比如 stack、slab、socket 。默认不限制。如果取值小于 --memory ,则属于后者的子集 --shm-size 64m # 限制挂载到 /dev/shm 的 tmpfs 文件系统的体积,默认为 64m --device-read-bps 1kb # 限制每秒读磁盘的数据量,默认不限制 --device-write-bps 1kb # 限制每秒写磁盘的数据量 --device-read-iops 10 # 限制每秒读磁盘的次数 --device-write-iops 10 # 限制每秒读磁盘的次数
# 查看
docker
ps # 显示所有 running 状态的容器
-a # 显示所有状态的容器
-n <int> # --last ,显示最后创建的几个容器(包括所有状态的)
--no-trunc # 不截断显示过长的内容
-q # --quiet ,只显示 ID
-s # --size ,增加显示容器占用的磁盘空间
-f status=running # --filter ,添加过滤条件,只显示部分容器
-f "label=branch" # 过滤具有 branch 标签的容器
-f "label=branch=dev" # 过滤具有 branch 标签且取值为 dev 的容器
--format '{{.Names}} {{.Status}}' # 自定义每个容器显示的字段信息,基于 Go 模板语法
diff <container> # 显示容器内 top layer 的变化,用 A、C、D 分别表示增加、更改、删除了文件
port <container> # 显示指定容器映射的所有端口
top <container> [options] # 显示指定容器内的进程列表,可加上 ps 命令的参数
stats [container]... # 显示容器的资源占用情况,包括单核 CPU 使用率、分配的内存使用率、网络 IO 量、磁盘 IO 量、创建的线程数
inspect <object> # 显示一个 docker 对象的详细信息
-f "{{json .HostConfig.Binds }}" # --format ,只按照 JSON 格式显示指定信息
- docker 的容器、镜像、数据卷、网络等对象可采用 ID 或 Name 作为标识符。
- ID :取自对象十六进制哈希值的开头 n 位。用户可自由指定 n 位 ID ,只需与其它对象不同即可。
- Name :只能包含字符
[0-9A-Za-z_.-]
,且以字母或数字开头。如果用户未指定 name ,则由 dockerd 自动生成。 - 每个对象在创建之后,不支持修改其 ID 或 Name 。
docker ps --format
可显示以下字段,区分大小写:.ID .Image .Command # 容器的启动命令 .CreatedAt # 容器的创建时间 .RunningFor # 容器从创建以来,存在的时长 .Ports # 镜像 EXPOSE 的端口、容器实际映射的端口 .Names .Labels # 容器的所有标签 .Label # 容器的指定标签的值,比如 '{{.Label "maintainer"}}' .State # 容器的运行状态,比如 created、running、exited .Status # 容器的运行状态,以及该状态的持续时间,例如: Up 2 minutes .Size # 容器占用的磁盘空间。例如:0B (virtual 206MB) 分别表示 top layer 所占磁盘空间、全部层 layer 所占虚拟磁盘空间,不包括日志驱动器、挂载卷、swap 占用的磁盘空间 .Mounts # 容器挂载的所有卷,例如:/etc/localtime, /data/mysql .Networks # 容器关联的网络名
# 管理
docker
# 运行容器
run # 运行容器,相当于先 create 再 start
create # 创建容器,命令行参数与 docker run 差不多。此时容器处于 created 状态,没有运行
start <container>... # 启动容器,容器会从 exited 状态变为 running 状态
restart <container>... # 重启容器,相当于先 stop 再 start
# 暂停容器
pause <container>... # 暂停容器内所有进程,基于 Cgroup 的 freezer
unpause <container>... # 解除暂停的容器
# 停止容器
stop <container>... # 停止容器。这会向容器内 1 号进程发送 SIGTERM 信号,然后等待容器内所有进程退出
-t <n> # 超时时间,默认为 10 秒。如果超时之后,容器内依然有进程未退出,则自动发送 SIGKILL 信号
kill <container>... # 杀死容器。这会向容器内 1 号进程发送 SIGKILL 信号
-s <signal> # 发送的信号,默认为 SIGKILL
wait <container>... # 阻塞等待容器停止,然后打印其退出码
rm <container>... # 删除容器(只能删除已停止的)
-f # 强制删除(可以删除正在运行的)
container prune # 删除所有已停止的容器
# 修改容器
rename <container> <new_name> # 重命名容器
update <container>... # 更改容器的配置
--cpus 2
-m 256m
--restart no
# 管理 docker 引擎
system
info # 显示宿主机、docker 的配置信息
df # 显示各种 docker 对象占用的磁盘空间
prune # 删除所有未被使用的 docker 对象
- 例:
docker restart `docker ps -aq` # 重启所有容器
- 容器的生命周期:
created # 已创建。此时容器被 dockerd 分配了 CPU 、内存等资源,创建了根目录文件系统 running、up # 运行中 paused # 暂停运行。此时容器内所有进程依然存在,只是不再被 CPU 执行 exited、stopped # 停止运行。此时容器内所有进程都退出,占用的 CPU、内存、文件描述符等资源被释放 restart # 重启。此时容器重新被分配资源,但依然使用之前的文件系统,重新执行启动命令 delete # 被删除。此时容器占用的资源被释放,文件系统也被删除。最终消失不见,在 dockerd 中不能查询到该容器
# 执行命令
docker exec [options] <container> <command> # 在容器内执行一条命令,该容器必须为 running 状态
- 例:
docker exec -it nginx bash # 在容器内创建终端并进入
# 拷贝文件
docker cp /root/f1 <container>:/root/ # 从宿主机拷贝文件到容器内
docker cp <container>:/root/f1 /root/ # 从容器内拷贝文件到宿主机
- 拷贝当前目录时不能使用
docker cp *
,要使用docker cp .
,默认会递归拷贝子目录、拷贝文件权限。
# 日志
docker logs <container> # 显示一个容器的日志
-f # 保持显示
--tail 10 # 只显示最后几行。默认从头开始显示
-t # 增加显示时间戳
- dockerd 会记录每个容器内的 1 号进程的 stdout、stderr ,作为该容器的日志,存储到宿主机的日志文件中。
- 可以将容器内其它进程的输出,重定向到 1 号进程的终端,从而一起记录到该容器的日志中。如下:
run.sh 1> /proc/1/fd/1 2> /proc/1/fd/2 # 直接重定向
ln -s /proc/1/fd/1 stdout.log # 创建一个软链接,然后再重定向 run.sh &> stdout.log
# 日志驱动器
日志驱动器(logging driver):用于保存容器的日志。
- 属于每个容器的独立配置。
docker 支持多种日志驱动器:
- none :不保存日志。
- local
- 将日志按文本格式保存在宿主机的
/var/lib/docker/containers/{ContainerId}/local-logs/container.log
文件中。 - 默认会自动进行日志轮换, max-size 为 10m ,max-file 为 5 。
- 将日志按文本格式保存在宿主机的
- json-file
- 默认启用这种。
- 将日志按 JSON 格式保存在宿主机的
/var/lib/docker/containers/{ContainerId}/{ContainerId}-json.log
文件中。如下:[root@CentOS ~]# tail -n 1 /var/lib/docker/containers/3256c21887f9b110e84f0f4a620a2bf01a8a7b9e3a5c857e5cae53b22c5436d4/3256c21887f9b110e84f0f4a620a2bf01a8a7b9e3a5c857e5cae53b22c5436d4-json.log {"log":"2021-02-22T03:16:15.807469Z 0 [Note] mysqld: ready for connections.\n","stream":"stderr","time":"2021-02-22T03:16:15.80758596Z"}
- 使用 docker logs 命令查看日志时,只会显示其 log 字段的值。
- 默认不会进行日志轮换, max-size 为 -1 即不限制大小,max-file 为 1 。
- syslog :将日志保存到宿主机的 syslog 。
- journald :将日志保存到宿主机的 journald 。
- fluentd :将日志发送给 fluentd 服务。
每个容器只能选用一种日志驱动器。
- 可以在 daemon.json 中配置日志驱动器。也可以在创建一个容器时,单独配置:
docker run -d \ --log-driver json-file \ --log-opt max-size=50m \ --log-opt max-file=2 \ nginx
- 可以在 daemon.json 中配置日志驱动器。也可以在创建一个容器时,单独配置:
# 网络
# CNM
- 2015 年,Docker 公司发布了容器网络模型(Container Network Model,CNM),定义了容器的网络规范。
- Docker 公司还开源了一个名为 libnetwork 的 Golang 库,作为 CNM 规范的标准实现。
- 后来 k8s 制定了 CNI 规范,与 CNM 不兼容。CNI 随着 k8s 推广之后,CNM 几乎只有 Docker 还在使用。
- CNM 规范的特点:
- 将关于网络的代码从容器运行时中剥离出来,以插件的形式工作。
- 为每个容器创建一个独立的 Sandbox 网络环境,包含虚拟网卡、路由表、DNS 配置等。
- 一个 Sandbox 可以接入多个 Network 网络(通常为 bridge 类型),接入点称为 endpoint (通常为 veth pair )。
# 网络驱动器
网络驱动器(Network driver):用于控制容器的网络连接。
- 属于每个容器的独立配置。
- 基于操作系统的底层网络工具来工作,比如 iptables 。
docker 支持多种网络驱动器:
- none
- :无网络。
- 此时容器内只有一个环回网口 lo ,因此不能访问到宿主机 ip 或其它容器 ip 。
- host
- :主机网络。
- 此时容器绑定了宿主机的全部网口,相当于直接部署在宿主机上,采用宿主机的 network namespace 。
- 当容器内进程监听端口时,是监听宿主机网口 eth0 上的 Socket 。
- bridge
- :桥接网络,是默认类型。
- 此时容器绑定了一个环回网口 lo 和一个虚拟网口 eth0@xxx 。
- 宿主机原本通过以太网口 eth0 进行内网通信。而 Docker 可以给每个容器分配虚拟 IP ,再通过 bridge 技术连通这些容器,组成虚拟网络。
- overlay
- :用于跨主机连通容器之间的虚拟网络。相比之下,bridge 只能连通同一个主机上的多个容器。
- 这是 docker swarm 特有的功能。
- none
dockerd 创建一个 bridge 网络时,会在宿主机上创建一个 network namespace ,并创建一个名为
br-******
的虚拟网口,管理一个虚拟子网,比如 172.17.1.0/24 。- 为了管理该虚拟子网的流量,还会自动修改宿主机、容器内的路由表,在宿主机上添加 iptables 规则。
当一个容器接入一个 bridge 网络时,会创建一对 veth pair 虚拟网口。一端位于宿主机上,名为
veth******
。另一端位于容器内,名为eth[0-9]
。从而在虚拟网络上连通两者,像用一根网线将一个主机接入一个交换机。- 同时,会分配一个该 bridge 网络的虚拟 IP ,绑定到容器内的虚拟网口
eth[0-9]
。 - 如果多个容器接入同一个 bridge 网络,则分别拥有一个该虚拟子网的 IP ,可以相互通信。
- 此时,每个容器拥有独立的虚拟网口,相当于独立主机。
- 假设容器 A 向容器 B 发送数据包,流程如下:
- 容器 A 从自己的 eth0 网口发出数据包,被传输到宿主机的 veth 网口。
- 宿主机收到数据包,根据 ARP 协议寻址,将数据包转发到容器 B 的 veth 网口,并通过 iptables 规则进行 NAT 转换。
- 容器 B 从自己的 eth0 网口收到数据包。
- 如果一个容器没有接入某个 bridge 网络,则不能访问该虚拟子网的任何 IP ,会报错:
No route to host
- 如果一个容器接入多个 bridge 网络,则拥有多个虚拟网口、多个虚拟子网 IP ,可以与各个子网的容器相互通信。
- 同时,会分配一个该 bridge 网络的虚拟 IP ,绑定到容器内的虚拟网口
bridge 网络的连通性:
- 从容器内可以访问到宿主机、其它主机,比如 ping 其它主机的 IP 。
- 从宿主机不能访问到容器内,比如 ping 容器的虚拟 IP 。
- 容器内进程监听端口时,是监听其虚拟网口上的 Socket ,因此默认不能从容器外访问到该端口。
- 访问容器内端口的几种方案:
- 创建容器时,映射端口到宿主机。
- 让容器采用 host 网络。
关于 DNS 。
- 创建容器时,容器内的 /etc/hosts 文件由 Docker 镜像决定,与宿主机的 /etc/hosts 文件无关。
- Docker 会在容器内的 /etc/hosts 文件中,自动添加一行规则
<当前容器IP> <当前容器名>
。
- Docker 会在容器内的 /etc/hosts 文件中,自动添加一行规则
- 创建容器时,容器内的 /etc/resolv.conf 文件不由 Docker 镜像决定,而是默认采用以下配置:
# cat /etc/resolv.conf nameserver 127.0.0.11 options ndots:0
- 这样是为了让容器内进程采用 libnetwork 内置的 DNS 服务器,实现两个功能:
- 如果 DNS 查询当前容器或其它容器的名称,则解析到该容器的 IP 。
- 如果 DNS 查询其它域名,则将 DNS 请求转发到上游 DNS 服务器,即宿主机的 /etc/resolv.conf 文件中指定的 DNS 服务器。
- 为了避免与容器内普通进程抢占端口,libnetwork 内置的 DNS 服务器不会使用 53 端口,而是监听一个随机的 TCP 端口、一个随机的 UDP 端口。
- 为了避免用户依然访问 53 端口,还会在容器内添加 iptables 规则:将访问
127.0.0.11:53
的流量反向代理到127.0.0.11:$random_port
。如下:# iptables-save | grep 127.0.0.11 -A OUTPUT -d 127.0.0.11/32 -j DOCKER_OUTPUT -A POSTROUTING -d 127.0.0.11/32 -j DOCKER_POSTROUTING -A DOCKER_OUTPUT -d 127.0.0.11/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:37173 -A DOCKER_OUTPUT -d 127.0.0.11/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:47570 -A DOCKER_POSTROUTING -s 127.0.0.11/32 -p tcp -m tcp --sport 37173 -j SNAT --to-source :53 -A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport 47570 -j SNAT --to-source :53
- 宿主机的 /etc/resolv.conf 文件没有采用 libnetwork 内置的 DNS 服务器,因此不支持 DNS 解析容器名。
- 如果容器使用初始的 bridge 网络,则会照搬宿主机的 /etc/resolv.conf 文件,因此不支持 DNS 解析容器名。
- 这样是为了让容器内进程采用 libnetwork 内置的 DNS 服务器,实现两个功能:
- 创建容器时,容器内的 /etc/hosts 文件由 Docker 镜像决定,与宿主机的 /etc/hosts 文件无关。
# 网络实例
docker network
ls # 显示所有的 docker 网络实例
inspect <network> # 查看一个网络的详细信息
create <network> # 创建一个网络
-d bridge # --dirver ,选择驱动器,默认为 bridge
--subnet 172.17.0.0/16 # 子网的范围。默认会给每个 network 创建一个独立子网
--gateway 172.17.0.1 # 子网中的网关,这会添加到本机的路由表,可用 route 命令查看
rm <network> # 删除一个网络
prune # 删除所有未被使用的网络
connect <network> <container> # 将一个网络连接到指定容器
--ip <ip> # 指定容器在该网络中的 IP 地址,默认会随机分配
--alias <name> # 给容器添加 DNS 名称,默认采用容器名
disconnect <network> <container> # 取消连接
- dockerd 安装之后会创建三个初始的 docker 网络实例,如下:
[root@CentOS ~]# docker network ls NETWORK ID NAME DRIVER SCOPE f12e6817d0e7 bridge bridge local 926afc8d908b host host local a77e846f1a82 none null local
- 初始的 bridge 网络会绑定一个名为 docker0 的虚拟网口,管理虚拟子网 172.17.0.0/16 。
- 新建一个容器时,默认的网络配置是
docker run --network bridge
,因此会接入初始的 bridge 网络。
# 网络配置
docker run
-p 80:8080 # 将宿主机的 80 端口映射到容器的 8000 端口(可重复使用该命令选项),默认是指 TCP 端口
-p 80:8080/udp # 映射 UDP 端口
-p 127.0.0.1:80:8080 # 映射宿主机指定 Socket 的端口
-P # 从宿主机上随机选取端口,映射到容器 EXPOSE 声明的所有端口
--network <network> # 让当前容器接入指定的 docker 网络(启用该命令选项时,-p 选项会失效)
--network container:<name> # 让当前容器共用指定容器的 network namespace
--link <container>[:alias] # 将当前容器通过网络连接到另一个容器,需要两个容器都接入初始的 bridge 网络。可选添加目标容器的别名,支持 DNS 解析
--dns <ip> # 设置容器内的 DNS 服务器
--mac-address <string> # 设置容器的 MAC 地址。默认会根据容器 IP 自动生成
- 映射端口时,dockerd 会自动添加 iptables 规则,将宿主机的 src_port 收到的网络包转发到容器的 dst_port 。
- 此时宿主机的防火墙会暴露 src_port 端口,允许被任意外部 IP 访问。
- 这样自动添加的 iptables 规则很复杂,建议不要手动修改,容易出错。
- 比如启动、停止 firewalld.service 时,会导致 dockerd 的 iptables 规则出错。
- 如果出错,可以尝试重启 dockerd ,让它重新配置 iptables 。
# 例
例:使用初始的 bridge 网络
创建一个容器,映射 80 端口到宿主机:
[root@CentOS ~]# docker run -it --rm --name test1 -p 80:80 nginx bash root@818dcf340ce3:/# ip addr # 查看容器内的网口 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 3609: eth0@if3610: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever root@818dcf340ce3:/# route # 查看容器内的路由表 Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface default 172.17.0.1 0.0.0.0 UG 0 0 0 eth0 172.17.0.0 * 255.255.0.0 U 0 0 0 eth0
可见该容器有一个环回网口 lo 和虚拟网口 eth0 ,虚拟 IP 为 172.17.0.2 。
查看宿主机的路由表:
[root@CentOS ~]# route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface default gateway 0.0.0.0 UG 0 0 0 eth0 # 缺省路由 10.1.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 # 宿主机的以太网接口 link-local 0.0.0.0 255.255.0.0 U 1002 0 0 eth0 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 # 将指向子网 172.17.0.0/16 的数据包发送到 docker0 虚拟网口
查看宿主机的部分 iptables 规则:
[root@CentOS ~]# iptables -t nat -nvL Chain POSTROUTING (policy ACCEPT 52308 packets, 3308K bytes) pkts bytes target prot opt in out source destination 79542 4774K MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0 # 转发来自 bridge 子网的数据包时,保留源 IP 0 0 MASQUERADE tcp -- * * 172.17.0.2 172.17.0.2 tcp dpt:80 # 允许容器访问自己的 80 端口 Chain DOCKER (2 references) pkts bytes target prot opt in out source destination 0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0 18666 1120K DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80 # 将发向宿主机 80 端口的数据包,转发到容器的 80 端口
例:使用自建的 bridge 网络
- 保留上例的 test1 容器,再创建第二个容器:
[root@CentOS ~]# docker run -it --rm --name test2 nginx bash root@818dcf340ce3:/# ping 172.17.0.2 # 能访问到容器 test1 的虚拟 IP PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data. 64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.088 ms 64 bytes from 172.17.0.2: icmp_seq=2 ttl=64 time=0.067 ms root@818dcf340ce3:/# ping test1 # 容器只接入初始的 bridge 网络时,不支持 DNS 解析容器名 ping: test1: Name or service not known
- 创建一个网络,连接两个容器:此时可见宿主机增加了一条路由规则:
[root@CentOS ~]# docker network create bridge1 950323e01c9f2c862a712c4fda12e55dd5a9b4afd8d59993fe1adaf581e008b0 [root@CentOS ~]# docker network connect bridge1 test1 [root@CentOS ~]# docker network connect bridge1 test2
[root@CentOS ~]# route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-950323e01c9f ...
- 在第二个容器中测试:
root@818dcf340ce3:/# ip addr # 此时容器增加了一个虚拟网口 eth1 ,用于在自建的 bridge 网络中通信 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 3611: eth0@if3612: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever 3616: eth1@if3617: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 172.18.0.3/16 brd 172.17.255.255 scope global eth1 valid_lft forever preferred_lft forever root@818dcf340ce3:/# ping test1 # 此时支持 DNS 解析容器名 PING test1 (192.168.112.2) 56(84) bytes of data. 64 bytes from test1.bridge1 (192.168.112.2): icmp_seq=1 ttl=64 time=0.054 ms 64 bytes from test1.bridge1 (192.168.112.2): icmp_seq=2 ttl=64 time=0.048 ms
例:映射端口
- 创建两个容器
[root@CentOS ~]# docker run -d --name test1 --network host nginx 9c1c537e8a304ad9e4244e3c7ae1743b88d45924b7b48cbb0a9f63606c82d76d [root@CentOS ~]# docker run -d --name test2 -p 2080:80 nginx 4601a81b438e31e5cb371291e1299e4c5333e853a956baeb629443774a066e9c
- 在宿主机上可以访问容器的端口:还可以通过环回地址访问容器的端口:
[root@CentOS ~]# curl -I 10.0.0.1:80 # test1 容器使用宿主机的网卡,因此能访问到 HTTP/1.1 200 OK ... [root@CentOS ~]# curl -I 10.0.0.1:2080 # test2 容器的端口已经映射到宿主机的网卡,因此能访问到 HTTP/1.1 200 OK ...
[root@CentOS ~]# curl -I 127.0.0.1:80 HTTP/1.1 200 OK ... [root@CentOS ~]# curl -I 127.0.0.1:2080 HTTP/1.1 200 OK ...
- 在容器内可以访问宿主机上的任意端口:
[root@CentOS ~]# docker exec -it test1 curl -I 10.0.0.1:80 HTTP/1.1 200 OK ... [root@CentOS ~]# docker exec -it test1 curl -I 10.0.0.1:2080 HTTP/1.1 200 OK ... [root@CentOS ~]# docker exec -it test2 curl -I 10.0.0.1:80 HTTP/1.1 200 OK ... [root@CentOS ~]# docker exec -it test2 curl -I 10.0.0.1:2080 HTTP/1.1 200 OK ...
- 在容器内访问环回地址的端口:
[root@CentOS ~]# docker exec -it test1 curl -I 127.0.0.1:80 HTTP/1.1 200 OK ... [root@CentOS ~]# docker exec -it test1 curl -I 127.0.0.1:2080 HTTP/1.1 200 OK ... [root@CentOS ~]# docker exec -it test2 curl -I 127.0.0.1:80 HTTP/1.1 200 OK ... [root@CentOS ~]# docker exec -it test2 curl -I 127.0.0.1:2080 # test2 容器的网卡上没有监听 2080 端口,因此不能访问 curl: (7) Failed to connect to 127.0.0.1 port 2080: Connection refused ...
# 存储
- 删除容器时,其 top layer 也会被删除,因此容器启动之后修改的文件都会丢失。
- 持久化存储容器内数据的几种方案:
- 让容器内进程,主动将数据发送到容器外,比如数据库中。
- bind mount :在创建容器时,将宿主机的文件、目录挂载到容器中某个路径。
- 挂载的文件、目录实际存储在宿主机上,而不是容器的 top layer 中,因此删除容器也不会影响。
- volume mount :挂载数据卷到容器中某个路径。
- 与 bind mounts 类似,但更容易迁移到其它主机。
- 同一个文件、目录或数据卷可以被多个容器同时挂载。
- tmpfs mount :将数据临时保存在内存中。
# 存储驱动器
- 存储驱动器(storage driver):用于控制容器对 layer 的读写。每个容器可以配置不同的 storage driver 。
- docker 支持多种存储驱动器:
- aufs
- fuse-overlayfs
- overlay2 :取代了旧版的 overlay 。
- zfs
- 容器的存储驱动器默认为 overlay2 ,而数据卷的驱动器默认为 local ,表示存储在本机。
# 挂载
docker run
-v <src_path>:<dst_path>[:mode] # --volume ,将宿主机的文件、目录或数据卷挂载到容器的 dst_path 路径(可重复使用该命令选项)
--mount type=bind,src=/tmp,dst=/tmp # --mount 的配置比 --volume 更详细,支持传入多个键值对形式的配置参数,用逗号分隔
--mount type=volume,src=volume_1,dst=/tmp,volume-driver=local,ro
挂载时,src_path 有多种形式:
- 如果为绝对路径,则视作一个宿主机路径。例如
/tmp:/tmp
。- 如果该绝对路径不存在,则会自动在宿主机上创建一个该路径的目录,所有权为 root 用户,然后挂载。
- 如果为相对路径,则报错不支持。例如
./tmp:/tmp
。 - 如果无路径,则视作一个数据卷的名称。 。
- 如果该数据卷不存在,则 dockerd 会自动在宿主机的
/var/lib/docker/volumes/<volumeID>/
目录下创建一个 _data 目录,作为数据卷,挂载到容器中。- 还会自动给 _data 目录分配合适的文件权限,供容器内进程访问。
- 如果该数据卷不存在,则 dockerd 会自动在宿主机的
- 如果为空,则会自动创建一个匿名的数据卷。例如
:/tmp
。- 用 docker inspect 命令可查看匿名卷的实际路径。
- 如果为绝对路径,则视作一个宿主机路径。例如
挂载宿主机的文件时,注意 docker 基于 inode 来挂载文件。在宿主机上用 vi/vim 修改被挂载文件时,会生成一个新 inode 的文件,而容器内依然挂载原 inode 的文件。
- 可通过以下方式更新挂载文件:
- 通过
cat f1.tmp > f1
的方式修改文件。 - 重启容器,使其自动重新挂载文件。
- 改为挂载目录,在目录中修改文件。
- 通过
- 可通过以下方式更新挂载文件:
挂载的文件、目录的所有权依然采用宿主机上的 uid、gid ,容器内使用非 root 用户时,可能对挂载路径没有访问权限。
- 此时需要先在宿主机上调整挂载路径的权限,比如
chown -R <UID> <PATH>
。 - 可以在挂载时限制访问权限 mode :
-v /etc/localtime:/etc/localtime:rw # 允许读写(默认采用) :ro # 挂载为 Read-only file system ,只允许读取,不能修改 :z # 添加 selinux 标签,将数据卷标记为会被多个容器共享 :Z # 添加 selinux 标签,将数据卷标记为不会被其它容器共享
- 此时需要先在宿主机上调整挂载路径的权限,比如
一些经常挂载的宿主机文件:
/etc/hosts /etc/passwd # 让容器采用宿主机的用户名、uid /etc/localtime # 让容器内采用与宿主机相同的时区,不过有的容器不会读取该文件 /var/run/docker.sock # 允许在容器内与 dockerd 通信,可以执行 docker ps 等命令
# 数据卷
docker volume
ls # 显示所有的数据卷
inspect <volume> # 查看数据卷的详细信息
create <volume> # 创建一个数据卷
-d local # --dirver ,选择驱动器,默认为 local
rm <volume> # 删除一个数据卷
prune # 删除所有未使用的数据卷