# 容器

# 启动

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 选项则终止子进程组。
        • 如果直接子进程是僵尸进程,则自动清理。
  • 无状态容器(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
      

# 网络

# 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 特有的功能。
  • 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 发送数据包,流程如下:
        1. 容器 A 从自己的 eth0 网口发出数据包,被传输到宿主机的 veth 网口。
        2. 宿主机收到数据包,根据 ARP 协议寻址,将数据包转发到容器 B 的 veth 网口,并通过 iptables 规则进行 NAT 转换。
        3. 容器 B 从自己的 eth0 网口收到数据包。
    • 如果一个容器没有接入某个 bridge 网络,则不能访问该虚拟子网的任何 IP ,会报错:No route to host
    • 如果一个容器接入多个 bridge 网络,则拥有多个虚拟网口、多个虚拟子网 IP ,可以与各个子网的容器相互通信。
  • bridge 网络的连通性:

    • 从容器内可以访问到宿主机、其它主机,比如 ping 其它主机的 IP 。
    • 从宿主机不能访问到容器内,比如 ping 容器的虚拟 IP 。
      • 容器内进程监听端口时,是监听其虚拟网口上的 Socket ,因此默认不能从容器外访问到该端口。
      • 访问容器内端口的几种方案:
        • 创建容器时,映射端口到宿主机。
        • 让容器采用 host 网络。
  • 关于 DNS 。

    • 创建容器时,容器内的 /etc/hosts 文件由 Docker 镜像决定,与宿主机的 /etc/hosts 文件无关。
      • Docker 会在容器内的 /etc/hosts 文件中,自动添加一行规则 <当前容器IP> <当前容器名>
    • 创建容器时,容器内的 /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 解析容器名。

# 网络实例

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 网络

  1. 创建一个容器,映射 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 。

  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 虚拟网口
    
  3. 查看宿主机的部分 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 网络

  1. 保留上例的 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
    
  2. 创建一个网络,连接两个容器:
    [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
    ...
    
  3. 在第二个容器中测试:
    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
    

例:映射端口

  1. 创建两个容器
    [root@CentOS ~]# docker run -d --name test1 --network host nginx
    9c1c537e8a304ad9e4244e3c7ae1743b88d45924b7b48cbb0a9f63606c82d76d
    [root@CentOS ~]# docker run -d --name test2 -p 2080:80 nginx
    4601a81b438e31e5cb371291e1299e4c5333e853a956baeb629443774a066e9c
    
  2. 在宿主机上可以访问容器的端口:
    [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
    ...
    
  3. 在容器内可以访问宿主机上的任意端口:
    [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
    ...
    
  4. 在容器内访问环回地址的端口:
    [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 目录分配合适的文件权限,供容器内进程访问。
    • 如果为空,则会自动创建一个匿名的数据卷。例如 :/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             # 删除所有未使用的数据卷