Docker知识点整理
阅读原文时间:2022年03月30日阅读:3

目录

1. Docker简介

  Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到其他机器上。容器可以简单地理解为是一个修改过的运行时环境,可以隔离所有的资源(除非明确允许)。UNIX中使用容器隔离资源已经有很长一段时间了,但是直接使用容器难度大,配置复杂,容易出错。Docker使用现有的容器技术,根据最佳实践提供一致的Docker构建方案,让大家可以很轻松的使用容器隔离资源,提供更强的安全性。目前,Docker可原生运行在Linux上,在OS X和Windows环境中通过单独的虚拟机也可以运行。

  Docker在linux上运行容器的结构如下图所示:

        

  命令行工具或CLI在被称为用户空间的内存中运行,就像是在操作系统上运行的其他程序。运行Docker可以认为是在用户空间运行着两个程序:一个是Docker守护进程,另一个是DockerCLI,DockerCLI是与用户交互的Docker程序。上图也显示了三个运行着的容器,每个都是以Docker守护程序的子进程运行,封装在容器中。在容器中运行的程序只能访问该容器内部的内存空间和资源(除非特别规定可以访问容器外部资源)。

  Docker可以执行、复制和轻松的分发容器,Docker通过一种打包和分发的软件完成传统容器的封装,这个用来充当容器(Container)分发角色的组件被称为镜像(Image)。镜像与容器的关系,个人认为类似类(Class)和实例的关系。基于一个类可以创建多个实例,每个实例都有独立的资源。而基于一个镜像可以创建多个容器,每个容器也互不干扰(如果不考虑容器连接等特殊情况)

 

2. Docker镜像

  Docker镜像是一个存在于系统上的实体,镜像内部是一个精简的操作系统和应用运行所需要的文件以及所有依赖(镜像不包含内核,容器都是共享所在Docker主机的内核)。镜像仓库(Image Repository)是一个有名字的镜像桶,用于存放镜像,将镜像集中存放就是为了方便大家获取所需的镜像。镜像仓库又有一个镜像仓库服务(Image Registry)来管理,Docker客户端的镜像仓库服务是可配置的,默认是Docker Hub。在每个镜像仓库中,标签是唯一指定镜像的重要途径,也是一种创建有用别名的遍历方法。一个标签在一个仓库中只能被应用到单个镜像,但一个镜像可以有多个标签。下图展示了镜像仓库服务、镜像仓库、镜像之间的关系:镜像仓库服务管理多个镜像仓库,每个镜像仓库又可以包含多个镜像。

                  

2.2.1 搜索拉取镜像

  Docker镜像存储在镜像仓库中,可以从中搜索和拉去镜像。

  如下为搜索镜像示例,其参数说明:

  • NAME: 镜像仓库源的名称

  • DESCRIPTION: 镜像的描述

  • OFFICIAL: 是否 docker 官方发布

  • STARS: 类似 Github 里面的 star,表示点赞、喜欢的意思。

  • AUTOMATED: 自动构建。

    $ docker search ubuntu
    NAME DESCRIPTION STARS OFFICIAL AUTOMATED
    ubuntu Ubuntu is a Debian-based Linux operating sys… 10873 [OK]
    dorowu/ubuntu-desktop-lxde-vnc Docker image to provide HTML5 VNC interface … 422 [OK]
    rastasheep/ubuntu-sshd Dockerized SSH service, built on top of offi… 244 [OK]
    consol/ubuntu-xfce-vnc Ubuntu container with "headless" VNC session… 217 [OK]
    ubuntu-upstart Upstart is an event-based replacement for th… 108 [OK]

      拉取镜像,可以指定标签拉取,不指定镜像标签默认拉取标签为 lastest 的镜像

    $ docker pull ubuntu
    Using default tag: latest
    latest: Pulling from library/ubuntu
    d51af753c3d3: Downloading [========================================> ] 23.13MB/28.56MB
    fc878cd0a91c: Download complete
    6154df8ff988: Download complete
    fee5db0ff82f: Waiting
    latest: Pulling from library/ubuntu
    d51af753c3d3: Pull complete
    fc878cd0a91c: Pull complete
    6154df8ff988: Pull complete
    fee5db0ff82f: Pull complete
    Digest: sha256:747d2dbbaaee995098c9792d99bd333c6783ce56150d1b11e333bbceed5c54d7
    Status: Downloaded newer image for ubuntu:latest
    docker.io/library/ubuntu:latest

      指定标签拉取镜像:

    $ docker pull ubuntu:18.04
    18.04: Pulling from library/ubuntu
    23884877105a: Pull complete
    bc38caa0f5b9: Pull complete
    2910811b6c42: Pull complete
    36505266dcc6: Pull complete
    Digest: sha256:3235326357dfb65f1781dbc4df3b834546d8bf914e82cce58e6e6b676e23ce8f
    Status: Downloaded newer image for ubuntu:18.04
    docker.io/library/ubuntu:18.04

2.2.2 查看镜像

  查看有哪些镜像:

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              1d622ef86b13        2 weeks ago         73.9MB
ubuntu              18.04               c3c304cb4f22        2 weeks ago         64.2MB

  查看镜像详细信息:

$ docker inspect ubuntu:18.04
[
    {
        "Id": "sha256:c3c304cb4f22ceb8a6fcc29a0cd6d3e4383ba9eb9b5fb552f87de7c0ba99edac",
        "RepoTags": [
            "ubuntu:18.04"
        ],
        "RepoDigests": [
            "ubuntu@sha256:3235326357dfb65f1781dbc4df3b834546d8bf914e82cce58e6e6b676e23ce8f"
        ],
        "Parent": "",
        "Comment": "",
        "Created": "2020-04-24T01:07:05.743682549Z",
        "Container": "f607979929fd999f71996754275dc5058e7345748f52d58ba72b6baf449c1fb2",
        ... # 内容较长,这里省略了
    }
]

2.2.3 制作镜像

  镜像的制作有两种方法:从本地容器生成镜像和用Dockerfile生成镜像,涉及内容较多,详见第6节。

2.2.4 镜像标签

# 给ubuntu:18.04添加一个新标签ubuntu:v18
$ docker tag ubuntu:18.04 ubuntu:v18

# 查看镜像,发现新标签ubuntu:v18和ubuntu:18.04的镜像ID相同,实际上docker tag 只是创建一个标签,和原始标签指向同一个镜像。
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              1d622ef86b13        2 weeks ago         73.9MB
ubuntu              18.04               c3c304cb4f22        2 weeks ago         64.2MB
ubuntu              v18                 c3c304cb4f22        2 weeks ago         64.2MB

2.2.5 删除镜像

  删除镜像要注意在基于镜像启动的所有容器全部停止之前,镜像是无法被删除的。

# 删除镜像要指定标签,不然默认删除标签为latest的镜像。可以指指定镜像ID进行删除。
$ docker image rm ubuntu:18.04
Untagged: ubuntu:18.04

# 查看镜像确实删除了
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              1d622ef86b13        2 weeks ago         73.9MB
ubuntu              v18                 c3c304cb4f22        2 weeks ago         64.2MB

2.3.1 通过镜像仓库分发

  把镜像上传到镜像仓库,使用者从镜像仓库拉取镜像。

Usage:    docker push [OPTIONS] NAME[:TAG]

Push an image or a repository to a registry

Options:
      --disable-content-trust   Skip image signing (default true)

2.3.2 手动分发

  镜像是个实体,可以存储在磁盘上,可以通过U盘传递。

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              1d622ef86b13        2 weeks ago         73.9MB

# 将镜像ubuntu:latest保存为ubuntu.tar。这样就可以通过传递ubuntu.tar来分发镜像了。
$ docker save -o ubuntu.tar ubuntu:latest

# 删除镜像ubuntu:lastest
$ docker image rm ubuntu:latest
Untagged: ubuntu:latest
Untagged: ubuntu@sha256:747d2dbbaaee995098c9792d99bd333c6783ce56150d1b11e333bbceed5c54d7
Deleted: sha256:1d622ef86b138c7e96d4f797bf5e4baca3249f030c575b9337638594f2b63f01
Deleted: sha256:279e836b58d9996b5715e82a97b024563f2b175e86a53176846684f0717661c3
Deleted: sha256:39865913f677c50ea236b68d81560d8fefe491661ce6e668fd331b4b680b1d47
Deleted: sha256:cac81188485e011e56459f1d9fc9936625a1b62cacdb4fcd3526e5f32e280387
Deleted: sha256:7789f1a3d4e9258fbe5469a8d657deb6aba168d86967063e9b80ac3e1154333f
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

# 从ubuntu.tar加载镜像
$ docker load -i ubuntu.tar
7789f1a3d4e9: Loading layer [==================================================>]  75.22MB/75.22MB
9e53fd489559: Loading layer [==================================================>]  1.011MB/1.011MB
2a19bd70fcd4: Loading layer [==================================================>]  15.36kB/15.36kB
8891751e0a17: Loading layer [==================================================>]  3.072kB/3.072kB
Loaded image: ubuntu:latest
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              1d622ef86b13        2 weeks ago         73.9MB

  Docker镜像由一些松耦合的只读镜像层组成,Docker负责堆叠这些镜像层,并且将它们表示为单个统一的对象。所有的Docker镜像都起始于一个基础镜像层,当进行修改或增加新的内容时,就会在当前镜像层之上创建新的镜像层。在添加额外的镜像层的同时,镜像始终保持是当前所有镜像层的组合。举个例子,假如一个镜像由两层,第一层包含文件1、2、3,第二层包含文件4、5、6,那么从系统角度看,这个镜像就包含1、2、3、4、5、6这六个文件。下图展示一个稍微复杂的三层镜像,其中文件7是文件5的一个更新版本。这种情况下,上层镜像层中的文件覆盖了底层镜像层中的文件。这样就使得文件的更新版本作为一个新镜像层添加到镜像当中(个人觉得最底层镜像层是一个基础层,然后上面的每层镜像层就像是一个步骤,从下往上一次执行每个步骤,最终的结果就是整个镜像对外显示的信息)。

                

                

  时刻记着:镜像层是只读的。这样的话多个镜像之间就可以共享镜像层了,这样可以节省空间并提升性能。最直观的就是拉取镜像的时候,如果该镜像包含的某些镜像层已经存在了,就不会再拉取这些镜像层了。

  镜像本身就是一个配置对象,其中包含了镜像层的列表以及一些元数据信息。镜像层才是实际数据存储的地方(比如文件等,镜像层之间是完全独立的,并没有从属于某个镜像集合的概念)。镜像的唯一标识是一个加密ID,即配置对象本身的散列值。每个镜像层也有一个加密ID区分,其值为镜像层本身内容的散列值。这意味着修改镜像的内容或其中任意的镜像层都会导致加密散列值的变化。所以,镜像和其镜像层都是不可变的,任何改动都能很轻松的被辨别(这就是所谓的内容散列)。

  多架构镜像(Multi-architecture Image)是为了解决镜像支持不同架构(Linux、Windows、ARM等)的问题。为了实现这个特性,镜像仓库服务API支持两种重要的结构:Manifest列表和Manifest。Manifest列表是指某个镜像标签支持的架构列表,其支持的每种架构都有自己的Manifest定义,其中列举了镜像的构成。如下图所示,图中左侧是Manifest列表,其中包含了该镜像支持的每种架构。Manifest列表的每一项都有一个箭头,指向具体的Manifest,其中包含了镜像配置和镜像层数据。

                

  多架构镜像原理:在拉取镜像的时候,Docker客户端会调用Docker镜像仓库服务的API完成拉取。如果该镜像有Manifest列表,则Docker客户端会找到当前主机架构对应的Manifest并解析出组成该镜像的镜像层加密ID,然后从镜像仓库中拉取每个镜像层。有些软件是不能跨平台的(可能别人不需要),所以Manifest列表是可选的,在没有Manifest列表的情况下,镜像仓库服务会返回普通的Manifest。

3. Docker容器

  容器是镜像的运行时实例,可以从单个镜像启动一个或多个容器。相对于虚拟机,容器较轻量级,而且容器启动非常快————与虚拟机运行在完整的操作系统之上相比,容器会共享其所在主机的操作系统/内核(一个很简单是证明方法就是在容器内部查看下进程,在容器所在主机的操作系统上也能找到容器内部运行的进程(由于容器拥有隔离的PID,所以进程号会不同,但是进程确是相同的))。Docker在创建容器的时候会为每个容器分配一个唯一的标识符,也会分配一个人性化的名称(如果用户没有指定名称的话)。Docker容器有四种状态:运行中(Up)、暂停中(Paused)、已退出(Exited)、重新启动中(一种临时状态),状态转移图如下:

                

3.2.1 启动停止Docker容器

  启动容器时的常用选项介绍:-i: 保持标准输入打开;-t:分配一个tty终端;-d:后台运行容器;-e MYENV=123:注入环境变量;-p 9000:80:发布到主机9000端口的请求都会映射到容器的80端口;-v /opt/soft:/soft:将主机的/opt/soft目录挂载到容器文件系统的/soft目录(第5节有详细介绍)。这里有一个小技巧:如果是前台启动的容器,按下Ctrl-PQ组合键则会退出容器但不会终止容器运行。

# docker容器运行、停止/启动、暂停/重启
docker container run
docker container stop
docker container start
docker container pause
docker container restart

3.2.2 查看Docker容器

# 列出运行中的容器
docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
b601d970cd1d        web:v1              "node app.js"       4 seconds ago       Up 2 seconds                            zealous_leakey

# 列出所有容器
$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                        PORTS               NAMES
b601d970cd1d        web:v1              "node app.js"            27 seconds ago      Up 26 seconds                                     zealous_leakey
9e3328fa1308        ubuntu:latest       "/bin/bash"              16 minutes ago      Exited (0) 16 minutes ago                         silly_nash
3e45ce78a31d        ubuntu:latest       "/bin/bash"              17 minutes ago      Exited (130) 16 minutes ago                       beautiful_easle

# 查看容器配置细节和运行时信息
docker inspect zealous_leakey
[
    {
        "Id": "b601d970cd1d051df92f8dcb2f8b9acd39d0a1e9a0138db6e597b690f134b57b",
        "Created": "2020-05-11T03:01:21.064780355Z",
        "Path": "node",
        "Args": [
            "app.js"
        ],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 23692,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2020-05-11T03:01:22.4655386Z",
            "FinishedAt": "0001-01-01T00:00:00Z"
        },
        "Image": "sha256:84f04d8b5d32a6d5b6dee7a67d2b25dcf9e12a5c6e36039353baf75d551c4dd1",
        ...
    }
]

3.2.3 连接到容器的Shell

  docker container exec 允许用户在运行状态的容器中启动一个新进程。该命令在将Docker主机Shell连接到一个运行中容器终端时非常有用。docker container exec -it <container-name or container-id> bash 命令会在容器内部启动一个Bash Shell进程,并连接到该Shell。为了是该命令生效,用于创建容器的镜像必须包含Bash shell。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
b601d970cd1d        web:v1              "node app.js"       2 minutes ago       Up 2 minutes                            zealous_leakey
$ docker container exec -it zealous_leakey bash
root@b601d970cd1d:/# 

3.2.4 删除容器

# 需要先停止容器才能删除
docker rm
# 如果加-f表示强杀容器然后删除
docker rm -f

3.3.1 Docker容器的自动重启策略

  通常建议在运行容器时配置好重启策略。这是容器的一种自我修复能力,可以在指定事件或者错误后重启来完成自我修复。重启策略应用于每个容器,在启动容器的时候传入--restart <重启策略>就可以了。常见的容器重启策略有三种:alwaysunless-stoppedon-failure。always策略是一种简单的方式,除非容器被明确停止(比如通过docker container stop命令),否则该策略会一直尝试重启处于停止状态的容器。使用always策略要注意,当Docker daemon重启的时候,停止的容器也会被重启。always和unless-stopped的最大区别,就是那些使用unless-stopped策略并处于已退出状态的容器,不会在Docker daemon重启的时候被重启。on-failure策略会在退出容器并且返回值不是0的时候重启容器,就算容器处于已退出状态,在Docker daemon重启的时候,容器也会重启。

3.3.2 使用init/systemctl和supervisor监控容器内的进程

  利用操作系统自带的守护进程(service、systemctl等)或者第三方进程监控软件(supervisor)来监控容器内的进程。创建镜像的时候配置好这些服务,启动容器的时候只需要启动这些监控程序就好,由监控程序去负责启动应用程序。

4. Docker网络

  Docker有四种网络容器模型:Closed容器、Bridged容器(默认)、Joined容器、Open容器,所有的Docker容器都要符合这四种模型中的一种。这些模型定义了一个容器如何与其他的本地容器、主机网络进行通信。下图形象地描绘了每一个模型,最强大(隔离程度最高)的在最左边,最脆弱的在最右边:

          

  启动容器的时候添加--network none就会创建一个Closed容器。Closed容器不允许任何的网络流量,运行在这种容器中的进程只能访问本地回环接口。

# 可以看到,Closed容器只有一个回环接口
$ docker run --name closed-container --network none node:7 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

  Bridged容器是Docker运行容器时的默认网络容器模型,也可以在启动容器的时候添加--network bridge明确创建一个Closed容器。Bridged容器有两个接口,一个是本地回环接口,另一个接口通过网桥连接到主机网络。Bridged容器可以通过主机网络访问任何主机网络能访问的外部网络。

# 一个本地回环接口,一个桥接到主机网络的接口
$ docker run --network bridge node:7 ip addr; ping -c 2 www.baidu.com
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
255: eth0@if256: <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
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
PING www.a.shifen.com (180.101.49.12) 56(84) bytes of data.
64 bytes from 180.101.49.12 (180.101.49.12): icmp_seq=1 ttl=50 time=29.0 ms
64 bytes from 180.101.49.12 (180.101.49.12): icmp_seq=2 ttl=50 time=28.4 ms

--- www.a.shifen.com ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 28.441/28.761/29.082/0.362 ms

  启动容器的时候添加--network container:<需要共享网络的容器> 就会创建一个Joined容器。注意这里在--network选项后面需要添加一个container:表示从现有的容器创建一个Joined容器,如果不加container:则Docker守护进程会认为是要从一个network创建容器,关于network目前使用不多,这里暂不介绍。使用Joined容器模型的所有容器共享一个网络栈,在这种情况下,这些容器之间没有任何网络隔离。Joined容器通过将某一个容器接口的访问权限提供给另外一个新的容器来构建。

# 创建一个closed容器,看下其内部网络状态,可以看到容器内部监听的是39439端口
$ docker run --name join-base-container --network none -d alpine:latest nc -l 8000
e2907c7a889d209734f63309a5351687ac2761489e129cd6a7d6a392234a3cde
$ docker exec join-base-container netstat -al
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:39439           0.0.0.0:*               LISTEN
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node Path

# 创建一个基于上面容器的joined容器,发现其共享了上面的close容器中的网络
$ docker run --network container:join-base-container alpine:latest netstat -al
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:39439           0.0.0.0:*               LISTEN
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node Path

  启动容器的时候添加--network host就会创建一个Open容器。Open容器没有网络容器,共享主机网络,并且对主机网络有完全的访问权。如下,启动一个Open容器,在容器内部可以看到主机所有的网络接口。

# 先看下主机有哪些网络接口
$ 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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 8c:ec:4b:ab:6d:38 brd ff:ff:ff:ff:ff:ff
    inet 10.55.2.40/24 brd 10.55.2.255 scope global dynamic noprefixroute eno1
       valid_lft 522699sec preferred_lft 522699sec
    inet6 fe80::7b57:66c8:6d06:833d/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:cc:a9:3a:68 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:ccff:fea9:3a68/64 scope link
       valid_lft forever preferred_lft forever
4: vboxnet0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 0a:00:27:00:00:00 brd ff:ff:ff:ff:ff:ff
    inet 192.168.99.1/24 brd 192.168.99.255 scope global vboxnet0
       valid_lft forever preferred_lft forever
    inet6 fe80::800:27ff:fe00:0/64 scope link
       valid_lft forever preferred_lft forever

# 发现open容器里能访问主机所有的网络接口
$ docker run --network host node:7 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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 8c:ec:4b:ab:6d:38 brd ff:ff:ff:ff:ff:ff
    inet 10.55.2.40/24 brd 10.55.2.255 scope global noprefixroute dynamic eno1
       valid_lft 522693sec preferred_lft 522693sec
    inet6 fe80::7b57:66c8:6d06:833d/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:cc:a9:3a:68 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:ccff:fea9:3a68/64 scope link
       valid_lft forever preferred_lft forever
4: vboxnet0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 0a:00:27:00:00:00 brd ff:ff:ff:ff:ff:ff
    inet 192.168.99.1/24 brd 192.168.99.255 scope global vboxnet0
       valid_lft forever preferred_lft forever
    inet6 fe80::800:27ff:fe00:0/64 scope link
       valid_lft forever preferred_lft forever

5. 卷和持久化数据

  一个主机或容器的目录树是由一组挂载点创建而成,这些挂载点描述了如何能构建出一个或多个文件系统。存储卷是容器目录树上的挂载点,存储卷提供了容器无关的数据管理方式。书上术语解释的比较模糊,个人理解如下:将主机磁盘的某个目录挂载到容器的文件系统(主机上的这个目录就称为存储卷),那么在容器内对这个挂载点的操作实际上都是对主机上那个目录的操作。比如将主机目录/opt/soft挂载到容器的/soft目录,那么容器中对/soft的所有操作实际上都是对主机目录/opt/soft的操作。

  存储卷有两种类型:绑定挂载存储卷和管理存储卷。绑定挂载存储卷使用用户提供的主机目录或文件,在启动容器时需要加入选项-v <主机位置>:<容器挂载点>。管理存储卷使用有Docker守护进程控制的位置,被称为Docker管理空间,在启动容器时需要加入命令-v <容器挂载点>,Docker守护进程会自动在主机文件系统中创建一个目录来挂载到容器中指定的挂载掉点。存储卷类型如下图:

          

  多个容器共享同一个存储卷有两种方法,第一种方法是在启动容器时挂载到同一个主机目录。第二种方法启动新容器是加入选项--volume-from <其他容器名称或ID>,下面演示下这种方法:

$ docker run --name fowler -v ~/example-books:/library/PoEAA -v /library/DSL alpine:latest echo "OK"
OK
$ docker run --name knuth -v /library/test1 -v /library/test2 -v /library/test3 alpine:latest echo "OK"
OK

# 下面使用 --volumes-from基于容器fowler和容器knuth创建新容器,可以在容器内部访问容器fowler和容器knuth内部的存储卷
$ docker run --volumes-from fowler --volumes-from knuth alpine:latest  ls -l /library
total 20
drwxr-xr-x    2 root     root          4096 May 11 03:41 DSL
drwxr-xr-x    2 root     root          4096 May  7 02:18 PoEAA
drwxr-xr-x    2 root     root          4096 May 11 03:43 test1
drwxr-xr-x    2 root     root          4096 May 11 03:43 test2
drwxr-xr-x    2 root     root          4096 May 11 03:43 test3

  删除容器时加入-v选项即可删除管理卷。使用Docker命令是不能删除绑定存储卷的,因为绑定存储卷不在Docker守护进程管理范围内。

6. 应用的容器化

  从本地容器创建镜像比较简单,直接采用命令docker commit <容器名称或ID> <生成的镜像的名称>。但是要注意,启动原始容器时附带的命令会被提交到新镜像中,下面将解释这句话。首先,我们拉取ubuntu镜像,然后执行docker run -it --name git-container ubuntu:latest /bin/bash运行容器,在容器里面安装git然后退出容器,接着执行生成镜像。

$ docker commit git-container ubuntu-git:v1
sha256:c1f13209eb865c7b726a80cc570e13cf1ad2e37b6d614b54b21017ecb3881920
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu-git          v1                  c1f13209eb86        14 seconds ago      197MB

然后我们用新镜像ubuntu-git运行容器,发现好像什么都没发生。

$ docker run --rm ubuntu-git:v1
$ 

出现上面的现象是因为启动原始容器时附带的命令会被提交到新镜像中,而之前启动创建新镜像的容器时附带的命令是/bin/bash。因此,当使用这个默认命令从新镜像启动一个容器时,他会启动一个shell并且立马停止它。

  我们在创建新镜像时可以设置入口点(entrypoint),为了设置入口点,需要使用--entrypoint <入口点命令>选项重新运行一个容器,并从这个容器创建新的镜像。

# 基于ubuntu-git:v1镜像启动一个带入口点的容器
$ docker run --name base-entrypoint --entrypoint git ubuntu-git:v1
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           <command> [<args>]
           ...

# 基于带入库点的容器创建镜像,保持镜像名称不变
$ docker commit base-entrypoint ubuntu-git:v1
sha256:7ab43fc4a1c911f4f2eeeb62f5a946f2b687303a423ae64451a7a846db7cf036

# 清除容器
$ docker rm base-entrypoint
base-entrypoint

# 从新镜像启动容器,可以看到已经自带了入口点
$ docker run --rm ubuntu-git:v1 version
git version 2.25.1

6.2.1 Dockerfile使用

  先介绍下如何用Dockerfile来创建上一小节中从本地容器创建的git镜像。创建一个名为Dockerfile的文件,将下面的代码复制到文件中保存:

FROM ubuntu:latest
RUN apt-get update \
    && apt-get install -y git
ENTRYPOINT ["git"]

执行命令docker build -t ubuntu-git:auto .创建新镜像,这时候docker image ls可以看到刚刚创建的镜像。启动容器来验证新镜像:

$ docker build  -t ubuntu-git:auto .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM ubuntu:latest
 ---> 1d622ef86b13
Step 2/3 : RUN apt-get update     && apt-get install -y git
 ---> Running in 24ca27336db7
Removing intermediate container 24ca27336db7
 ---> 6e0f2d7b38e1
Step 3/3 : ENTRYPOINT ["git"]
 ---> Running in bd1435f0e46d
Removing intermediate container bd1435f0e46d
 ---> 88475690ecbc
Successfully built 88475690ecbc
Successfully tagged ubuntu-git:auto

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu-git          auto                88475690ecbc        3 minutes ago       197MB
ubuntu              latest              1d622ef86b13        2 weeks ago         73.9MB

$ docker run --rm ubuntu-git:auto version
git version 2.25.1

6.2.2 常用Dockerfile指令

  使用Dockerfile应注意:每个Dockerfile指令都会导致一个新镜像层被创建,所以指令应该尽可能合并。镜像的创建是由Docker守护进程完成的,而不是Docker客户端。Docker客户端会将上下文发送到Docker守护进程,并且由Docker守护进程负责创建镜像,所以在写Dockerfile的时候不要把无关的数据添加到镜像。

  RUN、ENTRYPOINT、CMD等部分指令有两种格式:shell格式和exec格式。shell格式类似于一个shell命令,例如入口点设置为ENTRYPOINT pyhon /app/run.py,其中的参数以空格为界限分隔开来。exec格式是一个字符串数组,其中第一个值是要执行的命令,剩下的是参数。shell格式指定的命令将会作为默认shell的一个参数来执行。具体点说,指定的命令在运行时会以/bin/sh -c "python /app/run.py"的形式执行。最重要的是,如果ENTRYPOINT使用的shell格式,那么CMD指令提供的所有参数和运行容器时指定的额外参数都会被忽略。尽可能使用exec格式是个最佳实践。

  • FROM

      FROM指令用于指定基础镜像层。

      格式:FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]

      示例:FROM ubuntu:latest

  • RUN

      在当前镜像层之上创建一个新的镜像层,在新层中执行命令。

      格式:RUN <command>RUN ["executable", "param1", "param2"]

      示例:RUN apt-get update && install -y git

  • CMD

      在运行容器时提供默认参数。

      格式:CMD ["param1","param2"]CMD command param1 param2

      示例:CMD ["/usr/bin/wc","--help"]

  • LABEL

      用来定义键值对,这些键值对被记录为镜像或者容器的元数据。这和启动容器时的--label选项在功能上一致。

      格式LABEL <key>=<value> <key>=<value> <key>=<value> ...

      示例:LABEL multi.label1="value1" multi.label2="value2" other="value3"

  • MAINTAINER(不推荐使用)

      维护者信息。

      格式:MAINTAINER <name>

      示例:LABEL maintainer="SvenDowideit@home.org.au"

  • EXPOSE

      通知Docker容器启动时在指定的网络端口上进行监听。启动容器时可以使用-p选项来覆盖该设置。

      格式:EXPOSE <port> [<port>/<protocol>...]

      示例:EXPOSE 80/udp

  • ENV

      设置镜像的环境变量,类似启动容器时的-e(--env)选项。

      格式:ENV <key> <value>ENV <key>=<value> ...

      示例:ENV myName="John Doe" myDog=Rex\ The\ Dog myCat=fluffy

  • ADD

      复制文件到镜像中。

      格式:ADD [--chown=<user>:<group>] <src>... <dest>ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]

      示例:ADD test.txt relativeDir/

  • COPY

      复制文件到镜像中。

      格式:COPY [--chown=<user>:<group>] <src>... <dest>COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]

      示例:COPY test.txt relativeDir/

  • ENTRYPOINT

      设置入口点,指定容器启动时需要被运行的可执行程序。

      格式:ENTRYPOINT ["executable", "param1", "param2"]

      示例:ENTRYPOINT ["top", "-b"]

  • VOLUME

      创建Docker管理卷。

      格式:VOLUME ["/data"]

      示例:VOLUME ["/data"]

  • USER

      启动容器时指定用户。

      格式:USER <user>[:<group>]

      示例:USER patrick

  • WORKDIR

      指定默认的工作目录,若指定的目录不存在则创建。

      格式:WORKDIR /app

      示例:WORKDIR /app

6.2.3 .dockerignore介绍

  在Dockerk客户端将上下文发送到Docker守护进程之前,它将在上下文的根目录中查找名为.dockerignore的文件。 如果此文件存在,则客户端会修改上下文以排除与其中的模式匹配的文件和目录。 这有助于避免将不必要的大型文件或敏感文件和目录发送到守护进程,并避免使用ADD或COPY将它们添加到映像中。.dockerignore的模式匹配行为如下:

规则

行为

#comment

注释,被忽略

*

匹配任意数量的非分隔符,如/temp表示排除一级子目录中以temp开头的目录或文件,//temp*表示排除二级子目录中以temp开头的目录或文件

?

匹配任意单个非分隔符

!

以感叹号!开头的行表示该行不受.dockerignore模式匹配的控制,可以被ADD .加入到镜像

例如我们只把必要的文件加入镜像,下面的示例表示只会把app(不管是文件还是目录)、app.py、requirements.txt加入到镜像

# 先排除所有
*

# 再加入需要的文件或目录
!app
!app.py
!requirements.txt

7. Docker其他

  Docker运行容器时可以限制容器所使用的CPU、内存、设备的访问权,还可以运行特权容器(docker container run --privileged)。使用Docker过程中,可以通过docker help <命令>或者docker <命令> --help来查看帮助

docker run --help

Usage:    docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

Run a command in a new container

Options:
      --add-host list                  Add a custom host-to-IP mapping
                                       (host:ip)
  -a, --attach list                    Attach to STDIN, STDOUT or STDERR
  ...

8. 参考文献

  《深入浅出Docker》

  《Docker实战》

  https://docs.docker.com/engine/reference/builder/