摘自 开发者必备的 Docker 实践指南 | 掘金小册

目录

1. Docker 的技术实现

Docker 的实现,主要归结于三大技术命名空间 (Namespaces)、控制组 (Control Groups) 和联合文件系统 (Union File System)。

实现 Docker 的三大技术
实现 Docker 的三大技术

Namespace

命名空间Linux 内核2.4版本之后逐渐引入的一项用于进程运行隔离的模块

和很多编程语言中命名空间的概念类似,Linux Kernel 中的 Namespace 能够将计算机资源进行切割划分,形成各自独立的空间

实现而言,命名空间可以分为很多具体的子系统,如User NamespaceNet NamespacePID NamespaceMount Namespace等等。

利用PID Namespace,Docker 就实现了容器中运行进程相互隔离这一目标。

CGroups

资源控制组(常缩写为CGroups)是 Linux 内核在2.6版本后逐渐引入的一项对计算机资源进行控制的模块

顾名思义,CGroups 的作用就是控制计算机资源。它与 Namespace 的对比如下:

  • Namespace:以隔离进程、网络、文件系统虚拟资源为目的
  • CGroups:主要做的是硬件资源的隔离

虚拟化除了制造出虚拟的环境以隔离统一物理平台运行的不同程序之外,另一大作用就是控制硬件资源的分配CGroups的使用正是为了这样的目的。

CGroups 除了隔离硬件资源,还有控制资源分配这个关键性作用。通过 CGroups,我们可以指定任意一个隔离环境对任意资源的占用值或占用率,在很多分布式场景下会很有帮助

Union File System

联合文件系统(Union File System)是一种能够同时挂载不同实际文件或文件夹到同一目录,形成一种联合文件结构的文件系统。Docker 创新性的将其引入到容器实现中,用它解决虚拟环境对文件系统占用过量、实现虚拟环境快速启停等问题。

在 Docker 中,提供了一种对 UnionFS 的改进实现,也就是 AUFS(Advanced Union File System)

AUFS 将文件的更新挂载到旧的文件上,而不去修改那些不更新的内容(类似差量更新的概念)。这样一来,Docker 就大幅减少了虚拟文件系统对物理存储空间的占用

2. Docker 的理念

先来看一张 Docker 官方提供的容器结构设计架构图

Docker 容器结构设计架构图
Docker 容器结构设计架构图

与其他虚拟化实现甚至其他容器引擎不同的是,Docker 推崇一种轻量级容器结构,即一个应用一个容器

Docker 的轻量级容器实现虚拟机相关参数对比如下:

属性 Docker 虚拟机
启动速度 秒级 分钟级
硬盘使用 MB 级 GB 级
性能 接近原生 较低
普通机器支撑量 数百个 几个

3. Docker 的核心组成

之前提到了 Docker 实现容器引擎的一些技术,但都是相对底层的原理实现。在 Docker 将它们进行封装后,我们并不会直接去操作它们。在 Docker 中,还另外提供了一些软件层面的概念,这才是我们操作 Docker 所针对的对象

在 Docker 的体系中,有四大基本组件(Object):

  • 镜像(Image)
  • 容器(Container)
  • 网络(Network)
  • 数据卷(Volume)

镜像

镜像(Image)也是其他虚拟化技术中常常使用的一个概念。所谓镜像,可以理解为一个只读的文件包,其中包含了虚拟环境运行最原始文件系统的内容

Docker 的镜像与虚拟机中的镜像还是存在一定区别的。首先,Docker 利用 AUFS 作为底层文件系统实现。通过这种方式,Docker 实现了一种增量式的镜像结构

Docker 镜像的增量式分层结构
Docker 镜像的增量式分层结构

每次对镜像内容的修改,Docker 都会将这些修改写入一个新镜像层。因此,Docker 镜像实质上是无法修改的,因为所有对镜像的修改只会产生新的镜像,而不是更新原有的镜像。

容器

在容器技术中,容器(Container)就是用来隔离虚拟环境的基础设施。而在 Docker 里,它也被引申为隔离出来的虚拟环境

可以将镜像理解为编程中的类,那么容器就是类的一个实例。镜像内存放的是不可变化的东西,而当以它们为基础的容器启动后,容器内也就成为了一个“活”的空间

网络

Docker 实现了强大的网络功能,我们不但能够轻松的对每个容器的网络进行配置,还可以在容器间建立虚拟网络,将多个容器包裹其中,同时与其他网络环境隔离

另外,Docker 还可以在容器中构建独立的域名解析环境,这使得我们可以在不修改代码和配置的前提下直接迁移容器,而 Docker 会为我们完成新环境的网络适配。

对于这个功能,甚至可以在不同的物理服务器之间实现,让处在两台物理机上的两个 Docker 容器,加入到同一个虚拟网络中,形成完全屏蔽硬件的效果

数据卷

得益于 Docker 底层 UnionFS 技术的支持,我们除了能够从宿主机操作系统中挂载目录之外,还可以建立独立的目录以持久化存放数据,或者在容器之间共享数据

在 Docker 中,通过这几种方式进行数据共享持久化文件或目录,我们都称之为数据卷(Volume)

Docker Engine

目前这款实现容器化的工具是由 Docker 官方进行维护的,Docker 官方将其命名为 Docker Engine,同时定义其为工业级的容器引擎(Industry-standard Container Engine)。在 Docker Engine 中,实现了 Docker 技术最核心的部分——容器引擎

docker daemon

深究 Docker Engine,会发现它其实是由多个独立软件所组成的软件包。在这些程序中,最核心的就是 docker daemondocker CLI

Docker 所提供的容器管理、应用编排、镜像分发等功能,都集中在了 docker daemon 中。而我们之前所提到的镜像模块、容器模块、数据卷模块和网络模块也都实现在其中。

在操作系统中,docker daemon 通常以服务的形式运行以便静默的提供这些功能,所以我们通常称之为 Docker 服务

docker CLI

在 docker daemon 管理容器等相关资源的同时,它也向外暴露了一套 RESTful API,我们能够通过这套接口对 docker daemon 中运行的容器和其他资源进行管理。

为了方便我们通过控制台对 docker daemon 进行管理,Docker Engine 直接附带了 docker CLI 这个控制台程序。

容易看出,docker daemondocker CLI 组成了一个标准的 C/S 结构应用程序。而衔接这两者的,正是 docker daemon 所提供的 RESTful API

4. 安装 Docker

略。参见 CentOS 7 安装 Docker CE | 苏易北

docker version

> docker version

Client:
 Version:           18.06.1-ce
 API version:       1.38
 Go version:        go1.10.3
 Git commit:        e68fc7a
 Built:             Tue Aug 21 17:24:56 2018
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.06.1-ce
  API version:      1.38 (minimum version 1.12)
  Go version:       go1.10.3
  Git commit:       e68fc7a
  Built:            Tue Aug 21 17:23:21 2018
  OS/Arch:          linux/amd64
  Experimental:     false

docker info

> docker info

Containers: 32
 Running: 16
 Paused: 0
 Stopped: 16
Images: 33
Server Version: 18.06.1-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 468a545b9edcd5932818eb9de8e72413e616e86e
runc version: 69663f0bd4b60df09991c08812a60108003fa340
init version: fec3683
Security Options:
 apparmor
 seccomp
  Profile: default
Kernel Version: 4.15.0-38-generic
Operating System: Ubuntu 18.04.1 LTS
OSType: linux
Architecture: x86_64
CPUs: 8
Total Memory: 31.21GiB
Name: abelsu7-ubuntu
ID: RT3B:UYYD:MO4K:IMYS:3TG6:ZKGT:PUUK:DZBO:4FF5:KUA5:2OH7:YTDL
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

配置国内镜像源

修改/etc/docker/daemon.json(若文件不存在则直接新建)这个 Docker 服务的配置文件

{
    "registry-mirrors": [
        "https://registry.docker-cn.com"
    ]
}

之后重启 Docker 使配置生效:

> sudo systemctl restart docker

可通过docker info查阅当前注册的镜像源列表

> docker info
## ......
Registry Mirrors:
 https://registry.docker-cn.com/
## ......

5. 镜像与容器

Docker 镜像

可以将 Docker 镜像理解为包含应用程序及其相关依赖的一个基础文件系统,在 Docker 容器启动的过程中,它会以只读的方式被用于创建容器的运行环境

深入镜像实现

与其他虚拟机的镜像管理不同,Docker 将镜像管理纳入到了自身设计中,也就是说,所有的 Docker 镜像都是按照 Docker 所设定的逻辑打包的,也是受到 Docker Engine 所控制的

对于每一个记录文件系统修改镜像层来说,Docker 都会根据它们的信息生成一个 Hash 码,这是一个长度为 64 位的字符串,足以保证全球唯一性

由于镜像层都拥有唯一的编码,我们就能够区分不同的镜像层并保证它们的内容与编码是一致的,从而允许我们在镜像之间共享镜像层

查看镜像

使用docker images命令查看当前连接的 docker daemon 中存放和管理了哪些镜像

> docker images

REPOSITORY                           TAG                 IMAGE ID            CREATED             SIZE
redis                                alpine              a5cff96d7b8f        5 weeks ago         50.8MB
k8s.gcr.io/kube-controller-manager   v1.13.3             0482f6400933        5 weeks ago         146MB
k8s.gcr.io/kube-proxy                v1.13.3             98db19758ad4        5 weeks ago         80.3MB
k8s.gcr.io/kube-apiserver            v1.13.3             fe242e556a99        5 weeks ago         181MB
k8s.gcr.io/kube-scheduler            v1.13.3             3a6f709e97a0        5 weeks ago         79.6MB
quay.io/coreos/flannel               v0.11.0-amd64       ff281650a721        6 weeks ago         52.6MB
ubuntu                               16.04               b0ef3016420a        2 months ago        117MB
influxdb                             latest              623f651910b3        3 months ago        238MB
memcached                            latest              8230c836a4b3        3 months ago        62.2MB
mongo                                3.2                 fb885d89ea5c        3 months ago        300MB
mist/mailmock                        latest              95c29bda552f        3 months ago        299MB
mist/docker-socat                    latest              f00ed0eed13f        3 months ago        7.8MB
mistce/logstash                      v3-3-1              0f90a36d12c8        4 months ago        730MB
mistce/api                           v3-3-1              4a21b676352f        4 months ago        705MB
mistce/nginx                         v3-3-1              4f55dd9b39e0        4 months ago        109MB
mistce/gocky                         v3-3-1              ee93caf66f70        4 months ago        440MB
mistce/elasticsearch-manage          v3-3-1              10a48b9ea0e1        4 months ago        65.8MB
mistce/ui                            v3-3-1              b8fdbe0ccb23        4 months ago        626MB
ubuntu-with-vi-dockerfile            latest              74ba87f80b96        4 months ago        169MB
ubuntu-with-vi                       latest              9d2fac08719d        4 months ago        169MB
k8s.gcr.io/coredns                   1.2.6               f59dcacceff4        4 months ago        40MB
ubuntu                               latest              ea4c82dcd15a        4 months ago        85.8MB
centos                               latest              75835a67d134        5 months ago        200MB
k8s.gcr.io/etcd                      3.2.24              3cab8e1b9802        5 months ago        220MB
hello-world                          latest              4ab4c602aa5e        6 months ago        1.84kB
elasticsearch                        5.6.10              73e6fdf8bd4f        7 months ago        486MB
mistce/landing                       v3-3-1              b0e433749aa9        7 months ago        532MB
kibana                               5.6.10              bc661616b61c        8 months ago        389MB
hello-world                          <none>              2cb0d9787c4d        8 months ago        1.85kB
traefik                              v1.5                fde722950ccf        12 months ago       49.7MB
mist/swagger-ui                      latest              0b5230f1b6c4        12 months ago       24.8MB
k8s.gcr.io/pause                     3.1                 da86e6ba6ca1        14 months ago       742kB
rabbitmq                             3.6.6-management    c74093aa9895        2 years ago         179MB

镜像命名

docker images命令打印出来的内容中,我们还能看到两个与镜像命名有关的数据:REPOSITORYTAG,这两者共同组成了 Docker 镜像的命名规则

准确来说,Docker 镜像的命名可以分成三个部分usernamerepositorytag

  • username:主要用于识别上传镜像的不同用户,与 Github 中的用户空间类似
  • repository:主要用于识别镜像的内容,形成对镜像的表意描述
  • tag:主要用于标记镜像的版本,方便区分镜像内容的不同细节

有的镜像没有username这个部分,表示这个镜像是由 Docker 官方所维护和提供的,就不再单独标记用户了

另外,Docker 中还有一个约定,当我们在操作中没有具体给出镜像的tag时,Docker 会采用latest作为缺省tag

容器的生命周期

下面是一张容器运行状态流转图

上图展示了几种常见的对 Docker 容器的操作命令,以及执行它们之后容器运行状态的变化。重点关注容器以下几个核心状态

  1. Created:容器已创建,但尚未运行
  2. Running:容器运行中
  3. Paused:容器暂停运行
  4. Stopped:容器停止运行(注意与Create的区别)
  5. Deleted:容器被删除

主进程

在 Docker 的设计中,容器的生命周期其实与容器中PID1的进程有着密切的关系容器的启动,本质上可以理解为这个进程的启动,而容器的停止也意味着这个进程的停止,反之亦然。

当我们启动容器时,Docker 会按照镜像中的定义,启动对应的程序,并将这个程序的主进程作为容器的主进程(也就是PID1的进程)。而当我们控制容器停止时,Docker 会向主进程发送结束信号,通知程序退出

写时复制机制

Docker 的写时复制(Copy on Write)与编程中的相类似,也就是在通过镜像运行容器时,并不是马上就把镜像里的所有内容拷贝到容器所运行的沙盒文件系统中,而是利用 UnionFS 将镜像以只读的方式挂载到沙盒文件系统中。只有在容器中发生对文件的修改时,修改才会体现到沙盒环境上。

换言之,容器在创建和启动的过程中,不需要进行任何的文件系统复制操作,也不需要为容器单独开辟大量的硬盘空间,Docker 容器的启动速度也由此得到了保障

Docker 官网关于容器与镜像关系的说明

A container is launched by running an image. An image is an executable package that includes everything needed to run an application—the code, a runtime, libraries, environment variables, and configuration files.


A container is a runtime instance of an image—what the image becomes in memory when executed (that is, an image with state, or a user process). You can see a list of your running containers with the command, docker ps, just as you would in Linux.

6. 镜像仓库

如果说我们把镜像的结构用 Git 项目的结构做类比,那么镜像仓库就可以看作是 Gitlab、Github 等代码托管平台,只不过 Docker 的镜像仓库托管的不是代码项目,而是镜像

借助镜像仓库这个中转站,Docker 实现了镜像的分发功能。我们可以将开发环境上所使用的镜像推送至镜像仓库,并在测试或生产环境上拉取它们,而这个过程仅需要几个命令,甚至可以自动化的完成。

拉取镜像

可以使用docker pull命令拉取镜像

> docker pull ubuntu

Using default tag: latest
latest: Pulling from library/ubuntu
124c757242f8: Downloading [===============================================>   ]  30.19MB/31.76MB
9d866f8bde2a: Download complete 
fa3f2f277e67: Download complete 
398d32b153e8: Download complete 
afde35469481: Download complete

没有显式指定镜像的标签时,Docker 将默认使用latest。当然,也可以使用完整的镜像名来拉取镜像

> docker pull openresty/openresty:1.13.6.2-alpine

1.13.6.2-alpine: Pulling from openresty/openresty
ff3a5c916c92: Pull complete 
ede0a2a1012b: Pull complete 
0e0a11843023: Pull complete 
246b2c6f4992: Pull complete 
Digest: sha256:23ff32a1e7d5a10824ab44b24a0daf86c2df1426defe8b162d8376079a548bf2
Status: Downloaded newer image for openresty/openresty:1.13.6.2-alpine

Docker Hub

Docker HubDocker 官方建立的中央镜像仓库,同时也是 Docker Engine 的默认镜像仓库

搜索镜像

使用docker search命令搜索 Docker Hub 中的镜像

> docker search ubuntu

NAME                                                   DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
ubuntu                                                 Ubuntu is a Debian-based Linux operating sys…   9312                [OK]                
dorowu/ubuntu-desktop-lxde-vnc                         Docker image to provide HTML5 VNC interface …   281                                     [OK]
rastasheep/ubuntu-sshd                                 Dockerized SSH service, built on top of offi…   208                                     [OK]
consol/ubuntu-xfce-vnc                                 Ubuntu container with "headless" VNC session…   161                                     [OK]
ubuntu-upstart                                         Upstart is an event-based replacement for th…   96                  [OK]                
ansible/ubuntu14.04-ansible                            Ubuntu 14.04 LTS with ansible                   96                                      [OK]
neurodebian                                            NeuroDebian provides neuroscience research s…   56                  [OK]                
1and1internet/ubuntu-16-nginx-php-phpmyadmin-mysql-5   ubuntu-16-nginx-php-phpmyadmin-mysql-5          49                                      [OK]
ubuntu-debootstrap                                     debootstrap --variant=minbase --components=m…   40                  [OK]                
nuagebec/ubuntu                                        Simple always updated Ubuntu docker images w…   23                                      [OK]
tutum/ubuntu                                           Simple Ubuntu docker images with SSH access     19                                      
i386/ubuntu                                            Ubuntu is a Debian-based Linux operating sys…   17                                      
1and1internet/ubuntu-16-apache-php-7.0                 ubuntu-16-apache-php-7.0                        13                                      [OK]
ppc64le/ubuntu                                         Ubuntu is a Debian-based Linux operating sys…   12                                      
eclipse/ubuntu_jdk8                                    Ubuntu, JDK8, Maven 3, git, curl, nmap, mc, …   8                                       [OK]
codenvy/ubuntu_jdk8                                    Ubuntu, JDK8, Maven 3, git, curl, nmap, mc, …   5                                       [OK]
darksheer/ubuntu                                       Base Ubuntu Image -- Updated hourly             5                                       [OK]
pivotaldata/ubuntu                                     A quick freshening-up of the base Ubuntu doc…   2                                       
smartentry/ubuntu                                      ubuntu with smartentry                          1                                       [OK]
1and1internet/ubuntu-16-sshd                           ubuntu-16-sshd                                  1                                       [OK]
paasmule/bosh-tools-ubuntu                             Ubuntu based bosh-cli                           1                                       [OK]
pivotaldata/ubuntu-gpdb-dev                            Ubuntu images for GPDB development              0                                       
1and1internet/ubuntu-16-healthcheck                    ubuntu-16-healthcheck                           0                                       [OK]
ossobv/ubuntu                                          Custom ubuntu image from scratch (based on o…   0                                       
1and1internet/ubuntu-16-rspec                          ubuntu-16-rspec                                 0                                       [OK]

管理镜像

要想获得镜像更详细的信息,可以使用docker inspect命令:

> docker inspect mongo:3.2

[
    {
        "Id": "sha256:fb885d89ea5c35ac02acf79a398b793555cbb3216900f03f4b5f7dc31e595e31",
        "RepoTags": [
            "mongo:3.2"
        ],
        "RepoDigests": [
            "mongo@sha256:9e09fe9e747fb0ee1e64b572818e7397eb9a73e36a2b08bcc7846e9acf0a587f"
        ],
        "Parent": "",
        "Comment": "",
        "Created": "2018-11-16T00:55:06.547559408Z",
        "Container": "16a23b0d45ef66220aec0a2e542ff527da9da07889d4d862087630912d9ad86f",
        "ContainerConfig": {
            "Hostname": "16a23b0d45ef",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "27017/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "GOSU_VERSION=1.10",
                "JSYAML_VERSION=3.10.0",
                "GPG_KEYS=DFFA3DCF326E302C4787673A01C4E7FAAAB2461C \t42F3E95A2C4F08279C4960ADD68FA50FEA312927",
                "MONGO_PACKAGE=mongodb-org",
                "MONGO_REPO=repo.mongodb.org",
                "MONGO_MAJOR=3.2",
                "MONGO_VERSION=3.2.21"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"mongod\"]"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:d7430950b72ba7ecb5986396f9a3404b5b0d88c2ba39eb7f2d4b51b002db00ea",
            "Volumes": {
                "/data/configdb": {},
                "/data/db": {}
            },
            "WorkingDir": "",
            "Entrypoint": [
                "docker-entrypoint.sh"
            ],
            "OnBuild": [],
            "Labels": {}
        },
        "DockerVersion": "17.06.2-ce",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "27017/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "GOSU_VERSION=1.10",
                "JSYAML_VERSION=3.10.0",
                "GPG_KEYS=DFFA3DCF326E302C4787673A01C4E7FAAAB2461C \t42F3E95A2C4F08279C4960ADD68FA50FEA312927",
                "MONGO_PACKAGE=mongodb-org",
                "MONGO_REPO=repo.mongodb.org",
                "MONGO_MAJOR=3.2",
                "MONGO_VERSION=3.2.21"
            ],
            "Cmd": [
                "mongod"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:d7430950b72ba7ecb5986396f9a3404b5b0d88c2ba39eb7f2d4b51b002db00ea",
            "Volumes": {
                "/data/configdb": {},
                "/data/db": {}
            },
            "WorkingDir": "",
            "Entrypoint": [
                "docker-entrypoint.sh"
            ],
            "OnBuild": [],
            "Labels": null
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 300019217,
        "VirtualSize": 300019217,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/de4bf7c9580fda62420fd6a4e529783aeb161b44457ec6636bfaa97e94084ab0/diff:/var/lib/docker/overlay2/31a2f54b5cf142ae50d5ff530fd9159cd61129a47ab76b6b32656b6db42b765b/diff:/var/lib/docker/overlay2/f495f57ba2b9e665444151dc913bd1b8952a2e3d416d546b6722e44a038900c0/diff:/var/lib/docker/overlay2/51696b913195f45c1ce36c76240a0cf9836b593a16b0853238a5515bd9178322/diff:/var/lib/docker/overlay2/bcb73a5809c820e1eeb3c7cf4acc04c89b9e4d17be7c5ce9e3962580d14f2446/diff:/var/lib/docker/overlay2/d84695101463e67d0a4c901285a557cb0f4fc84a56840ce6433a225b799e2fc4/diff:/var/lib/docker/overlay2/d86783053f0a3e71f89c7b05328b2021a75bcf833911f7dc5fdad50e166a3d39/diff:/var/lib/docker/overlay2/a7a9a982dc727d527a3af4d04a19e359062c2d74bdd8fb497a057ca09ffcf290/diff:/var/lib/docker/overlay2/cbd9ce2cce4a6e2ec032e6cf25281016715a57f11ede109097c796383d13aac2/diff:/var/lib/docker/overlay2/339f33b0ff9703a7e50cce8459b89f5fb932ccc9460c8489a0f5d2cf65114033/diff",
                "MergedDir": "/var/lib/docker/overlay2/6ba7ad9bf4a5e344d1edd12b87fe42d9abd828d480385a2637a47947c9a4af7f/merged",
                "UpperDir": "/var/lib/docker/overlay2/6ba7ad9bf4a5e344d1edd12b87fe42d9abd828d480385a2637a47947c9a4af7f/diff",
                "WorkDir": "/var/lib/docker/overlay2/6ba7ad9bf4a5e344d1edd12b87fe42d9abd828d480385a2637a47947c9a4af7f/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:337a2e6463ae008c12681f29c50edde52ea5be2cc2f46d09b8254fd835b1f5a9",
                "sha256:9d3049f87bb2ba7ac0469cad7ee11f871ff4fc735cc4dfdc1be6a1fe877861a5",
                "sha256:75c2031620755be658ab335a6abb72376804e533e91fecb52315682b21aeeeca",
                "sha256:ed81bb40beffca626f698965d5ec236b394ad8db229bb6dbfeb7be7a61b32768",
                "sha256:38ccb1166c8a15aedf5a9d7f12b81436b9812175c3ce6c50fac39246a3ffc935",
                "sha256:1f5a9fb2648f17bd23ab13f9e70f8631d233f33f73329302144da1aa2e4a5b0f",
                "sha256:fcd5eec06559827da59d45500626b2dbf5673d03bba7aea9c9b9b786e8a10b54",
                "sha256:2bcf250f248858339faf2dc746c44197c9eecc34999d485c60d636c7fcbc4d20",
                "sha256:f6a5611931ed6ed6db65ad6a87abd7774f267c68c6f6d84cae65e0760c8a47b0",
                "sha256:b436f480c034edfc426e1fcadbebaf50c72c0ce92c66924b6cf6ba344e455560",
                "sha256:7eaf69109a2207f735d6423fe61a05200e3431ba9cdeafd6a27fa3c067c9f0ae"
            ]
        },
        "Metadata": {
            "LastTagTime": "0001-01-01T00:00:00Z"
        }
    }
]

删除镜像

可以使用docker rmi命令删除镜像,参数是镜像名或 ID,可以同时删除多个镜像。需要注意的是,需要先通过docker rm删除依赖该镜像的容器之后,该镜像才可以被删除:

> docker rmi redis:3.2 redis:4.0

Untagged: redis:3.2
Untagged: redis@sha256:745bdd82bad441a666ee4c23adb7a4c8fac4b564a1c7ac4454aa81e91057d977
Deleted: sha256:2fef532eadb328740479f93b4a1b7595d412b9105ca8face42d3245485c39ddc
## ......
Untagged: redis:4.0
Untagged: redis@sha256:b77926b30ca2f126431e4c2055efcf2891ebd4b4c4a86a53cf85ec3d4c98a4c9
Deleted: sha256:e1a73233e3beffea70442fc2cfae2c2bab0f657c3eebb3bdec1e84b6cc778b75
## ......

7. 运行和管理容器

容器的创建和启动

先来回顾一下容器状态转换图

Docker 容器的状态转换图
Docker 容器的状态转换图

可以看到,Docker 容器的生命周期共分为以下五种状态

  • Created:容器已经被创建,容器所需的相关资源已经准备就绪,但容器中的程序还未处于运行状态
  • Running:容器正在运行,其中的应用程序也正在运行
  • Paused:容器已经暂停,其中的所有程序都处于暂停状态
  • Stopped:容器处于停止状态,占用的资源和沙盒环境依然存在,只是容器中的应用程序均已停止运行
  • Deleted:容器已删除相关占用的资源及存储在 Docker 中的管理信息也都被释放和移除

创建容器

> docker create --name=nginx nginx:1.12
34f277e22be252b51d204acbb32ce21181df86520de0c337a835de6932ca06c3

启动容器

> docker start nginx

Docker 还允许我们通过docker run这个命令docker createdocker start这两步操作合成为一步

> docker run --name nginx -d nginx:1.12

通过-d--detach选项告诉 Docker 在启动后将程序与控制台分离,使其在后台运行

管理容器

使用docker ps查看正在运行中的 Docker 容器

> docker ps

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
89f2b769498a        nginx:1.12          "nginx -g 'daemon of…"   About an hour ago   Up About an hour    80/tcp              nginx

添加-a--al选项查看所有状态下的容器

> docker ps -a

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
425a0d3cd18b        redis:3.2           "docker-entrypoint.s…"   2 minutes ago       Created                                 redis
89f2b769498a        nginx:1.12          "nginx -g 'daemon of…"   About an hour ago   Up About an hour    80/tcp              nginx

停止和删除容器

使用docker stop命令停止正在运行中的容器

> docker stop nginx

容器停止后,其维持的文件系统沙盒环境还是存在的,内部被修改的内容也都会保留,我们可以通过docker start命令再次启动这个容器

当需要完全删除容器时,可以使用docker rm命令:

> docker rm nginx

正在运行中的容器默认情况下是不能被删除的,可以增加-f--force选项来强制停止并删除容器,不过这样做并不妥当

进入容器

我们知道,容器是一个隔离运行环境的东西,它里面除了镜像所规定的主进程之外,其他进程也是能够运行的。Docker 为我们提供了docker exec命令来让容器运行我们所给出的命令

> docker exec nginx more /etc/hostname
::::::::::::::
/etc/hostname
::::::::::::::
83821ea220ed

通过下列命令可以在容器中另外启动一个bash终端,并利用-it参数启用一个伪终端,方便我们与容器中的bash进行交互

> docker exec -it nginx bash
root@83821ea220ed:/>
  • -i--interactive)表示保持我们的输入流,只有使用它才能保证控制台程序能够正确识别我们的命令
  • -t--tty)表示启用一个伪终端,形成我们与bash的交互。如果没有它,我们就无法看到bash内部的执行结果

连接到容器主程序

Docker 为我们提供了一个docker attach命令,用于将当前的输入输出流连接到指定的容器上

> docker attach nginx

可以理解为:将容器中的主程序转为了前台运行

由于我们的输入输出流连接到了容器的主程序上,我们的输入输出操作也就直接针对了这个程序,而我们发送的 Linux 信号也会转移到这个程序上。例如我们可以通过Ctrl^C来向程序发送停止信号,这样一来容器也会停止运行

8. 为容器配置网络

容器网络

容器网络实质上也是由 Docker 为应用程序所创造的虚拟环境的一部分,它能让应用从宿主机操作系统的网络环境中独立出来,形成容器自有的网络设备、IP 协议栈、端口套接字、IP 路由表、防火墙等等与网络相关的模块。

Docker 网络中,有三个比较核心的概念,沙盒(Sandbox)、网络(Network)、端点(Endpoint)

  • 沙盒:提供了容器的虚拟网络栈,隔离了容器网络与宿主机网络,形成了完全独立的容器网络环境
  • 网络:可以理解为 Docker 内部的虚拟子网,网络内的参与者相互可见并能够进行通讯。Docker 的这种虚拟网络也是与宿主机网络存在隔离关系的,主要是为了形成容器间的安全通讯环境
  • 端点:是位于容器或网络隔离墙之上的洞,其主要目的是形成一个可以控制的、突破封闭网络环境的出入口。当容器的端点与网络的端点形成配对后,就如同在这两者之间架起了桥梁,便能够进行数据传输了

这三者一起构成了 Docker 网络的核心模型,即容器网络模型(Container Network Model)

浅析 Docker 的网络实现

容器网络模型容器引擎提供了一套标准的网络对接范式,而在 Docker 中,实现这套范式的是 Docker 所封装的libnetwork模块。

目前 Docker 官方提供了五种网络驱动BridgeHostOverlayMacLanNone

其中,Bridge网络是 Docker 容器的默认网络驱动,而Overlay网络则是借助 Docker Swarm 来搭建的跨 Docker Daemon 网络,我们可以通过它搭建跨物理主机的虚拟网络,进而让不同物理机中运行的容器感知不到多个物理机的存在

容器互联

让一个容器连接到另外一个容器,我们可以在容器通过docker createdocker run创建时通过--link选项进行配置

> docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes mysql
> docker run -d --name webapp --link mysql webapp:latest

要想在 Web 应用中连接到 MySQL 数据库,只需要将容器的网络命名填入到连接地址中。例如下面的代码,连接地址中的mysql就类似我们常见的域名解析,Docker 会将其指向 MySQL 容器的 IP 地址

String url = "jdbc:mysql://mysql:3306/webapp"

暴露端口

虽然容器间的网络打通了,但并不意味着我们可以任意访问被连接容器中的任何服务。Docker 为容器网络增加了一套安全机制,只有容器自身允许的端口,才能被其他容器所访问

docker ps的结果中可以看到容器暴露给其他容器访问的端口

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                 NAMES
95507bc88082        mysql:5.7           "docker-entrypoint.s…"   17 seconds ago      Up 16 seconds       3306/tcp, 33060/tcp   mysql

暴露端口可以通过 Docker 镜像定义,也可以在容器创建时通过--expose选项进行定义

> docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --expose 13306 --expose 23306 mysql:5.7

可以看到1330623306这两个端口已经成功的打开:

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                       NAMES
3c4e645f21d7        mysql:5.7           "docker-entrypoint.s…"   4 seconds ago       Up 3 seconds        3306/tcp, 13306/tcp, 23306/tcp, 33060/tcp   mysql

通过别名连接

Docker 还支持连接时使用别名来摆脱容器名的限制:

> docker run -d -name webapp --link mysql:database webapp:latest

这里使用了--link <name>:<alias>的形式连接到 MySQL 容器,并设置它的别名为database。这样当我们要在 Web 应用中使用 MySQL 连接时,就可以用database替代连接地址:

String url = "jdbc:mysql://database:3306/webapp";

管理网络

容器能够互相连接的前提是两者同处于一个网络中,这里的网络可以理解为 Docker 所虚拟的子网,而容器网络沙盒可以看作是虚拟的主机。只有当多个主机在同一个子网时,才能互相看到并进行网络数据交换。

当我们启动 Docker 服务时,他会为我们创建一个默认的bridge网络。而我们创建的容器在不专门指定网络的情况下,都会连接到这个网络上。

通过docker inspect命令查看容器,可在Network部分看到容器网络的相关信息

> docker inspect mysql
[
    {
## ......
        "NetworkSettings": {
## ......
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "NetworkID": "bc14eb1da66b67c7d155d6c78cb5389d4ffa6c719c8be3280628b7b54617441b",
                    "EndpointID": "1e201db6858341d326be4510971b2f81f0f85ebd09b9b168e1df61bab18a6f22",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:11:00:02",
                    "DriverOpts": null
                }
            }
## ......
        }
## ......
    }
]

创建网络

使用docker network create命令创建网络,通过-d选项指定驱动类型,默认为bridge

> docker network create -d bridge individual

通过docker network ls或者docker network list查看 Docker 中已经存在的网络:

> docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
bc14eb1da66b        bridge              bridge              local
35c3ef1cc27d        individual          bridge              local

在之后创建容器时,可以通过--network指定容器所要加入的网络

> docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --network individual mysql:5.7

通过docker inspect mysql观察一下此时的容器网络:

> docker inspect mysql
[
    {
## ......
        "NetworkSettings": {
## ......
            "Networks": {
                "individual": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": [
                        "2ad678e6d110"
                    ],
                    "NetworkID": "35c3ef1cc27d24e15a2b22bdd606dc28e58f0593ead6a57da34a8ed989b1b15d",
                    "EndpointID": "41a2345b913a45c3c5aae258776fcd1be03b812403e249f96b161e50d66595ab",
                    "Gateway": "172.18.0.1",
                    "IPAddress": "172.18.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:12:00:02",
                    "DriverOpts": null
                }
            }
## ......
        }
## ......
    }
]

可以看到容器所加入的网络已经变成为individual

当两个容器处于不同的网络时,之间是不能互相连接引用

端口映射

Docker 提供了端口映射的功能来允许我们从容器外部通过网络访问容器中的应用

Docker 中的端口映射
Docker 中的端口映射

要映射端口,我们可以在创建容器时使用-p--publish选项**,格式为-p <ip>:<host-port>:<container-port>

> docker run -d --name nginx -p 80:80 -p 443:443 nginx:1.12

之后就可以在容器列表里看到端口映射的配置

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                      NAMES
bc79fc5d42a6        nginx:1.12          "nginx -g 'daemon of…"   4 seconds ago       Up 2 seconds        0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   nginx

9. 管理和存储数据

Docker 中的沙盒文件系统虽然说有很多优势,但也存在弊端:

  1. 沙盒文件系统是随容器生命周期所创建和移除的,数据无法直接被持久化存储
  2. 由于容器隔离,我们很难从容器外部直接获得或操作容器内部文件中的数据

为了解决这些问题,UnionFS 支持挂载不同类型的文件系统到统一的目录结构中

挂载方式

基于底层存储实现,Docker 提供了三种适用于不同场景的文件系统挂载方式Bind MountVolumeTmpfs Mount

Docker 中三种不同的文件系统挂载方式
Docker 中三种不同的文件系统挂载方式
  • Bind Mount:能够直接将宿主机操作系统中的目录和文件挂载到容器内的文件系统中,需要同时指定容器内、外的路径。在容器内外对文件读写,都是相互可见
  • Volume:也是从宿主机操作系统中挂载目录到容器,只不过这个挂载的目录由 Docker 进行管理,我们只需要指定容器内的目录即可
  • Tmpfs Mount:支持挂载系统内存的中的一部分到容器的文件系统里,不过存储并不是持久的,其中的内容会随着容器的停止而消失

挂载文件到容器

创建容器时通过传递-v--volume选项来指定挂载对应的目录或文件,格式为-v <host-path>:<container-path>

> docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html nginx:1.12

容器启动后,就可以看到挂载的目录或文件已经出现在容器中

> docker exec nginx ls /usr/share/nginx/html
index.html

可以通过docker inspect查看容器数据挂载的相关信息

> docker inspect nginx
[
    {
## ......
        "Mounts": [
            {
                "Type": "bind",
                "Source": "/webapp/html",
                "Destination": "/usr/share/nginx/html",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }
        ],
## ......
    }
]

可以看到有一个RW字段,表示挂载目录或文件具有读写性(Read and Write)。

Docker 还支持以只读的方式挂载,这样目录或文件只能被容器中的程序读取,而无法修改。只需要在挂载选项后添加:ro

> docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html:ro nginx:1.12

挂载临时文件目录

Tmpfs Mount是一种特殊的挂载方式,它主要利用内存来存储数据,因此其特征就是高读写速度、临时性挂载

在创建容器时,通过--tmpfs传递挂载到容器内的目录即可,不需要指定内存的具体位置:

> docker run -d --name webapp --tmpfs /webapp/cache webapp:latest

也可以通过docker inspect命令进行查看:

> docker inspect webapp
[
    {
## ......
         "Tmpfs": {
            "/webapp/cache": ""
        },
## ......
    }
]

Tmpfs Mount有以下几种常见的使用场景

  • 应用不需要进行持久化保存敏感数据,可以借助内存的非持久性和程序隔离性保障安全
  • 读写速度要求较高、数据变化量大,但不需要持久化保存的数据,可以借助内存的高读写速度减少操作的时间

使用数据卷

数据卷(Volume)本质上仍然是宿主机操作系统上的一个目录,只不过它存放在 Docker 内部,接受 Docker 的管理

在使用Volume进行挂载时,我们不需要知道数据具体存储在了宿主机操作系统的何处,只需要给定容器中的哪个目录会被挂载即可

> docker run -d --name webapp -v /webapp/storage webapp:latest

数据卷挂载到容器后,可以通过docker inspect命令查看容器中数据卷的挂载信息

> docker inspect webapp
[
    {
## ......
        "Mounts": [
            {
                "Type": "volume",
                "Name": "2bbd2719b81fbe030e6f446243386d763ef25879ec82bb60c9be7ef7f3a25336",
                "Source": "/var/lib/docker/volumes/2bbd2719b81fbe030e6f446243386d763ef25879ec82bb60c9be7ef7f3a25336/_data",
                "Destination": "/webapp/storage",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],
## ......
    }
]

为了方便识别数据卷,可以通过-v <name>:<container-path>的形式来命名数据卷

> docker run -d --name webapp -v appdata:/webapp/storage webapp:latest

共用数据卷

由于数据卷的命名在 Docker 中是唯一的,因此可以很方便的让多个容器挂载同一个数据卷

> docker run -d --name webapp -v html:/webapp/html webapp:latest
> docker run -d --name nginx -v html:/usr/share/nginx/html:ro nginx:1.12

使用-v选项来挂载数据卷时,如果数据卷不存在,Docker 就会自动创建和分配宿主机操作系统的目录。如果同名数据卷已经存在,则会直接引用

删除数据卷

可以直接通过docker volume rm命令来删除指定的数据卷

> docker volume rm appdata

在删除数据卷之前,我们必须保证数据卷没有被任何容器所使用,否则 Docker 会报错

docker rm删除容器的命令中,还可以添加-v选项删除容器关联的数据卷

docker rm -v webapp

如果没有随容器删除这些数据卷,Docker 在创建新的容器时也不会启用它们。这时可以通过docker volume prune命令删除那些没有被容器引用的数据卷

> docker volume prune
Deleted Volumes:
af6459286b5ce42bb5f205d0d323ac11ce8b8d9df4c65909ddc2feea7c3d1d53
0783665df434533f6b53afe3d9decfa791929570913c7aff10f302c17ed1a389
65b822e27d0be93d149304afb1515f8111344da9ea18adc3b3a34bddd2b243c7
## ......

数据卷容器

所谓数据卷容器(Volume Container),就是一个没有具体指定应用,甚至不需要运行的容器。我们使用它的目的,是为了定义一个或多个数据卷持有它们的引用

数据卷容器
数据卷容器

可通过以下命令创建一个数据卷容器

> docker create --name appdata -v /webapp/storage ubuntu

数据卷容器可以看作是容器间的文件系统桥梁,可以像加入网络一样引用数据卷容器,添加--volumes-from参数即可:

> docker run -d -name webapp --volumes-from appdata webapp:latest

备份和迁移数据卷

利用数据卷容器,可以很方便的对数据卷中的数据进行迁移

数据备份、迁移、恢复的过程可以理解为对数据进行打包,移动到其他位置,在需要的地方解压的过程

首先建立一个用来存放打包文件的目录/backup。要备份数据,我们还需要建立一个临时容器,将用于备份的目录要备份的数据卷挂载上去

> docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar cvf /backup/backup.tar /webapp/storage

--rm选项用来让容器在停止后自动删除

备份后,就可以在/backup目录下找到数据卷的备份文件backup.tar了。

恢复数据卷中的数据,也可以借助临时容器来完成:

> docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar xvf /backup/backup.tar -C /webapp/storage --strip

通过 mount 选项挂载

Docker 还为我们提供了一个支持相对丰富的挂载方式,也就是通过--mount选项来配置挂载

> docker run -d --name webapp webapp:latest --mount 'type=volume,src=appdata,dst=/webapp/storage,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>' webapp:latest

10. 保存和共享镜像

提交容器更改

Docker 将容器内沙盒文件系统记录成镜像层的时候,会先暂停容器的运行,保证容器内的文件系统处于一个相对稳定的状态,以确保数据的一致性。

> docker commit -m "Configured" webapp
sha256:0bc42f7ff218029c6c4199ab5c75ab83aeaaed3b5c731f715a3e807dda61d19e

> docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
<none>                <none>              0bc42f7ff218        3 seconds ago       372MB
## ......

为镜像命名

使用docker tag能够为未命名的镜像指定镜像名

> docker tag 0bc42f7ff218 webapp:1.0

也可以为已有的镜像创建一个新的命名

> docker tag webapp:1.0 webapp:2.0

当我们对未命名的镜像进行命名后,Docker 就不会在镜像列表里继续显示这个镜像,取而代之的是我们新的命名。而如果我们对已有镜像使用docker tag时,旧的镜像依然会存在于镜像列表中

> docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
webapp                1.0                 0bc42f7ff218        29 minutes ago      372MB
webapp                latest              0bc42f7ff218        29 minutes ago      372MB
## ......

还可以直接在提交镜像更改时指定新的镜像名

> docker commit -m "Upgrade" webapp webapp:2.0

镜像的迁移

可以使用管道

> docker save webapp:1.0 > webapp-1.0.tar

或者可以使用docker save命令,并添加-o选项,用来指定输出文件

> docker save -o ./webapp-1.0.tar webapp:1.0

导入镜像

可以使用管道

> docker load < webapp-1.0.tar

或者添加-i选项指定输入文件

> docker load -i webapp-1.0.tar

批量迁移

通过docker savedocker load命令还可以批量迁移镜像,只要在docker save传入多个镜像名作为参数,就可以将这些镜像都打成一个包,方便我们一次性迁移多个镜像

> docker save -o ./images.tar webapp:1.0 nginx:1.12 mysql:5.7

导入和导出容器

使用docker export命令可以直接导出容器,可以简单的将其理解为docker commitdocker save命令的结合体:

> docker export -o ./webapp.tar webapp

相对的,使用docker export导出的容器包,我们可以使用docker import导入。使用docker import并非直接将容器导入,而是将容器运行时的内容以镜像的形式导入,所以导入的结果还是一个镜像,而不是容器

> docker import ./webapp.tar webapp:1.0

11. 通过 Dockerfile 创建镜像

关于 Dockerfile

Dockerfile 是 Docker 中用于定义镜像自动化构建流程配置文件,在 Dockerfile 中,包含了构建镜像过程中需要执行的命令和其他操作

Dockerfile 的内容很简单,主要以两种形式呈现:一种是注释行,另一种是指令行

编写 Dockerfile

首先来看一个完整的 Dockerfile 例子,这是用于构建 Docker 官方所提供的Redis镜像Dockerfile 文件

FROM debian:stretch-slim

# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added
RUN groupadd -r redis && useradd -r -g redis redis

# grab gosu for easy step-down from root
# https://github.com/tianon/gosu/releases
ENV GOSU_VERSION 1.10
RUN set -ex; \
    \
    fetchDeps=" \
        ca-certificates \
        dirmngr \
        gnupg \
        wget \
    "; \
    apt-get update; \
    apt-get install -y --no-install-recommends $fetchDeps; \
    rm -rf /var/lib/apt/lists/*; \
    \
    dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
    wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
    wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
    export GNUPGHOME="$(mktemp -d)"; \
    gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
    gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
    gpgconf --kill all; \
    rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
    chmod +x /usr/local/bin/gosu; \
    gosu nobody true; \
    \
    apt-get purge -y --auto-remove $fetchDeps

ENV REDIS_VERSION 3.2.12
ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-3.2.12.tar.gz
ENV REDIS_DOWNLOAD_SHA 98c4254ae1be4e452aa7884245471501c9aa657993e0318d88f048093e7f88fd

# for redis-sentinel see: http://redis.io/topics/sentinel
RUN set -ex; \
    \
    buildDeps=' \
        wget \
        \
        gcc \
        libc6-dev \
        make \
    '; \
    apt-get update; \
    apt-get install -y $buildDeps --no-install-recommends; \
    rm -rf /var/lib/apt/lists/*; \
    \
    wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
    echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
    mkdir -p /usr/src/redis; \
    tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
    rm redis.tar.gz; \
    \
# disable Redis protected mode [1] as it is unnecessary in context of Docker
# (ports are not automatically exposed when running inside Docker, but rather explicitly by specifying -p / -P)
# [1]: https://github.com/antirez/redis/commit/edd4d555df57dc84265fdfb4ef59a4678832f6da
    grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \
    sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \
    grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \
# for future reference, we modify this directly in the source instead of just supplying a default configuration flag because apparently "if you specify any argument to redis-server, [it assumes] you are going to specify everything"
# see also https://github.com/docker-library/redis/issues/4#issuecomment-50780840
# (more exactly, this makes sure the default behavior of "save on SIGTERM" stays functional by default)
    \
    make -C /usr/src/redis -j "$(nproc)"; \
    make -C /usr/src/redis install; \
    \
    rm -r /usr/src/redis; \
    \
    apt-get purge -y --auto-remove $buildDeps

RUN mkdir /data && chown redis:redis /data
VOLUME /data
WORKDIR /data

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD ["redis-server"]

Dockerfile 的结构

总体上来看,可以将 Dockerfile 理解为一个由上往下执行指令的脚本文件。可以将 Dockerfile 的指令简单的分为五大类

  • 基础指令:用于定义新镜像的基础和性质
  • 控制指令:是指导镜像构建的核心部分
  • 引入指令:用于将外部文件直接引入到构建镜像内部
  • 执行指令:能够为基于镜像所创建的容器,指定在启动时需要执行的脚本或命令
  • 配置指令:对镜像以及基于镜像所创建的容器,可以通过配置指令对其网络、用户等内容进行配置

Dockerfile 常见指令

1. FROM

镜像构建的过程中,可以通过FROM指令指定一个基础镜像。Docker 会先获取到这个基础镜像,再在这个镜像的基础上进行构建操作

FROM <image> [AS <name>]
FROM <image>[:<tag>] [AS <name>]
FROM <image>[@<digest>] [AS <name>]
2. RUN

RUN指令之后,我们直接拼接上需要执行的命令。在构建时,Docker 就会执行这些命令,并将它们对文件系统的修改记录下来,形成镜像的变化

RUN <command>
RUN ["executable", "param1", "param2"]

RUN指令支持以\换行,如果单行的长度过大,建议对内容进行切割,方便阅读

3. ENTRYPOINT 和 CMD

基于镜像启动的容器,在容器启动时会根据镜像所定义的一条命令来启动容器中进程号为1的进程。而这个命令的定义,就是通过 Dockerfile 中的ENTRYPOINTCMD实现的

ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2

CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
  • ENTRYPOINTCMD指令用法近似,都是给出需要执行的指令,并且它们都可以为空
  • ENTRYPOINTCMD同时给出时CMD中的内容会作为ENTRYPOINT定义命令的参数,最终执行容器启动的还是ENTRYPOINT所给出的命令
4. EXPOSE

通过EXPOSE指令可以为镜像指定要暴露的端口

EXPOSE <port> [<port>/<protocol>...]

当我们通过EXPOSE指令配置了镜像的端口暴露定义,那么基于这个镜像所创建的容器,在被其他容器通过--link选项连接时,就能够直接允许来自其他容器对这些端口的访问

5. VOLUME

在一些程序里,我们需要持久化一些数据。可以通过VOLUME指令来定义基于此镜像的容器所自动建立的数据卷,这样就无需单独使用-v选项进行配置

VOLUME ["/data"]
6. COPY 和 ADD

制作新镜像时,我们可能需要将一些软件配置、程序代码、执行脚本等直接导入到镜像内的文件系统里。使用COPYADD指令能够帮助我们直接从宿主机的文件系统里拷贝内容到镜像里的文件系统中

COPY [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] <src>... <dest>

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

COPYADD指令的定义完全一样主要区别在于ADD能够支持使用网络端的URL地址作为src,并且在源文件被识别为压缩包时,自动进行解压,而COPY则没有这两个能力。

构建镜像

在编写好Dockerfile之后,我们就可以使用docker build命令构建我们所定义的镜像

> docker build ./webapp

docker build可以接收一个参数,这个参数为一个目录路径(本地路径或 URL 路径)。Docker 会将这个目录作为构建的环境目录,默认情况下,也会从这个目录下寻找名为Dockerfile的文件。

如果我们的Dockerfile文件路径不在这个目录下,则可以通过-f选项单独给出Dockerfile文件的路径

> docker build -t webapp:latest -f ./webapp/a.Dockerfile ./webapp

最好在构建镜像时添加-t选项,用来指定新生成的镜像的名称

> docker build -t webapp:latest ./webapp

12. Dockerfile 使用技巧

构建中使用变量

在 Dockerfile 里,可以使用ARG指令建立一个参数变量。我们可以在构建时通过构建指令传入这个参数变量,并且在 Dockerfile 里使用它

FROM debian:stretch-slim

## ......

ARG TOMCAT_MAJOR
ARG TOMCAT_VERSION

## ......

RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"

## ......

如果我们需要通过这个 Dockerfile 文件构建 Tomcat 镜像,可以在构建时通过docker build--build-arg选项来设置参数变量

> docker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat

环境变量

环境变量通过ENV指令定义:

FROM debian:stretch-slim

## ......

ENV TOMCAT_MAJOR 8
ENV TOMCAT_VERSION 8.0.53

## ......

RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"

参数变量只能影响构建过程不同,环境变量不仅能够影响构建,还能够影响基于此镜像创建的容器

环境变量设置的实质,其实就是定义操作系统环境变量,所以在运行的容器里,一样拥有这些变量,而容器中运行的程序也能够得到这些变量的值

由于环境变量在容器运行时依然有效,所以运行容器时我们还可以对其进行覆盖

创建容器时使用-e--env选项,可以对环境变量的值进行修改定义新的环境变量

> docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7

ENV指令所定义的变量,永远会覆盖ARG所定义的变量,即使它们定义时的顺序是相反的

合并命令

构建镜像时,RUN指令两种写法

RUN apt-get update; \
    apt-get install -y --no-install-recommends $fetchDeps; \
    rm -rf /var/lib/apt/lists/*;
# 或
RUN apt-get update
RUN apt-get install -y --no-install-recommends $fetchDeps
RUN rm -rf /var/lib/apt/lists/*

而我们更常见的是第一种形式,这就要从镜像构建的过程说起了。

看似连续的镜像构建过程,其实是由多个小段组成的。每当一条能够形成对文件系统改动的指令在被执行前,Docker 先会基于上条命令的结果启动一个容器,在容器中运行这条指令的内容,之后将结果打包成一个镜像层,如此反复,最终形成镜像

所以,构建而来的镜像是由多个镜像层叠加而得的,而这些镜像层其实就是在我们 Dockerfile 中每条指令所生成的

因此,绝大多数镜像会将命令合并到一条指令中,因为这样不但减少了镜像层的数量,也减少了镜像构建过程中反复创建容器的次数提高了镜像构建的速度

构建缓存

Docker 在镜像构建的过程中,还支持一种缓存策略提高镜像的构建速度

由于镜像是多个指令所创建的镜像层组合而得,那么如果我们判断新编译的镜像层与已经存在的镜像层未发生变化,那么我们完全可以直接利用之前构建的结果,而不需要再执行这条构建指令,这就是镜像构建缓存的原理

基于这个原则,我们在条件允许的前提下,更建议将不容易发生变化的搭建过程放到 Dockerfile 的前部,充分利用构建缓存提高镜像构建的速度。

另外,指令的合并也不宜过度,而是将易变和不易变的过程拆分,分别放到不同的指令里。

不希望 Docker 在构建镜像中使用构建缓存时,可以通过--no-cache选项禁用

> docker build --no-cache ./webapp

搭配 ENTRYPOINT 和 CMD

ENTRYPOINTCMD两个命令都是用来指定基于此镜像所创建容器里主进程的启动命令,而它们的区别在于,ENTRYPOINT指令的优先级高于CMD指令。当ENTRYPOINTCMD同时在镜像中被指定时,CMD里的内容会作为ENTRYPOINT的参数,两者拼接之后,才是最终执行的命令。

ENTRYPOINT 和 CMD 的组合
ENTRYPOINT 和 CMD 的组合

之所以ENTRYPOINTCMD要分成两个不同的命令,是因为它们的设计目的是不同的

  • ENTRYPOINT:主要用于对容器进行一些初始化
  • CMD:用于真正定义容器中主程序的启动命令

Redis镜像为例:

## ......

COPY docker-entrypoint.sh /usr/local/bin/

ENTRYPOINT ["docker-entrypoint.sh"]

## ......

CMD ["redis-server"]

可以看到,CMD指令定义的正是启动Redis的服务程序,而ENTRYPOINT使用的则是外部引入的脚本文件docker-entrypoint.sh,内容如下:

#!/bin/sh
set -e

# first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
    set -- redis-server "$@"
fi

# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
    find . \! -user redis -exec chown redis '{}' +
    exec gosu redis "$0" "$@"
fi

exec "$@"

脚本的最后一条命令exec "$@"其作用是运行一个程序,而运行命令就是ENTRYPOINT脚本的参数,所以实际执行的就是CMD里的命令

13. 使用 Docker Compose 管理容器

Docker Compose

在 Docker 开发中最常使用的多容器定义和运行软件就是 Docker Compose

如果说 Dockerfile将容器内运行环境的搭建固化下来,那么 Docker Compose 就可以理解为将多个容器运行的方式和配置固化下来

Docker Compose 里,我们通过一个docker-compose.yml配置文件将所有与应用系统相关的软件及它们对应的容器进行配置,之后使用 Docker Compose 提供的命令进行启动,就能让 Docker Compose 将刚才我们所提到的那些复杂问题解决掉。

安装 Docker Compose

Docker Compose 是一个由 Python 编写的软件。通过下面的命令下载 Docker Compose 到应用执行目录,并附上运行权限,这样 Docker Compose 就可以在机器中使用了:

> sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
> sudo chmod +x /usr/local/bin/docker-compose
> sudo docker-compose version

docker-compose version 1.21.2, build a133471
docker-py version: 3.3.0
CPython version: 3.6.5
OpenSSL version: OpenSSL 1.0.1t  3 May 2016

也可以通过pip安装

> sudo pip install docker-compose

Docker Compose 的基本使用逻辑

简单来说,使用 Docker Compose 的步骤共分为三步

  1. 如果需要,编写容器所需镜像的Dockerfile(也可以使用现有镜像)
  2. 编写用于配置容器docker-compose.yml
  3. 使用docker-compose命令启动应用栈
1. 编写 docker-compose.yml

一个简单的例子:

version: '3'

services:

  webapp:
    build: ./image/webapp
    ports:
      - "5000:5000"
    volumes:
      - ./code:/code
      - logvolume:/var/log
    links:
      - mysql
      - redis

  redis:
    image: redis:3.2

  mysql:
    image: mysql:5.7
    environment:
      - MYSQL_ROOT_PASSWORD=my-secret-pw

volumes:
  logvolume: {}
2. 启动和停止

对与开发而言,最常使用的就是docker-compose updocker-compose down命令:

docker-compose up

docker-compose up命令类似于 Docker Engine 中的docker run。它会根据docker-compose.yml中配置的内容创建所有的容器、网络、数据卷等内容,并将它们启动

默认情况下docker-compose up会在前台运行,可以使用-d选项使其在后台运行

> sudo docker-compose up -d

需要注意的是,docker-compose命令默认会识别当前控制台所在目录内的docker-compose.yml文件,而且会以当前目录的名字作为组装的应用项目的名称。可以通过-f选项指定配置文件名通过-p选项定义项目名

> sudo docker-compose -f ./compose/docker-compose.yml -p myapp up -d

docker-compose down

docker-compose down命令用于停止所有的容器,并将它们删除,同时清除网络等配置内容

> sudo docker-compose down
3. 容器命令

除了启动和停止命令之外,Docker Compose 还为我们提供了很多直接操作服务的命令服务可以看成是一组相同容器的集合

可以使用docker-compose logs命令查看容器中主进程的输出内容

> sudo docker-compose logs nginx

通过docker-compose create/start/stop可以实现与docker create/start/stop相似的效果,只不过操作的对象由 Docker Engine 中的容器变为了 Docker Compose 中的服务

> sudo docker-compose create webapp
> sudo docker-compose start webapp
> sudo docker-compose stop webapp

更新中…