yokon's blog

Docker入门:核心组件

2019.01.23

什么是Docker

Docker是一个能够把开发的应用程序自动部署到容器的开源引擎,他设计的目的就是要加强程序员写代码的开发环境和应用部署的生产环境的一致性,降低开发环境的代码在生产环境无法正常执行的风险。推荐阅读HERE

安装Docker

Docker 官方为了简化安装流程,提供了一套便捷的安装脚本,Ubuntu 系统可以使用这套脚本安装:

$ curl -fsSL get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh --mirror Aliyun

脚本就会自动的将一切准备工作做好,并且把 Docker CE 的稳定(stable)版本安装在系统中。

启动 Docker CE

$ sudo systemctl enable docker
$ sudo systemctl start docker

安装完毕,可以执行 docker info来查看是否安装成功。

操作镜像(Image)

Docker 镜像是由文件系统叠加而成。可以将镜像理解为一个可执行的文件包,其中包含了运行容器所需的程序、资源、环境变量以及配置文件。

查找镜像

Docker 公司运营着一个公共的镜像仓库(Registry) Docker Hub,可以让我们很方便的保存并分享自己制作的镜像。开发过程中,我们使用到的镜像大部分都是可以直接使用 Docker Hub 中已经存在的镜像,即使自己有新的需求,也只需要对已有的镜像进行相应的改动。

我们可以使用 docker search 命令来查找所有 Docker Hub上的公共可用镜像:

$ docker search ubuntu

post40_1.jpg

上图为命令返回结果截图,其中返回信息为:

  • NAME:仓库名;
  • DESCRIPTION:该镜像描述;
  • STARS:用户评价,star 数;
  • OFFICIAL:是否官方开发者管理的镜像,OK 即为是;
  • AUTOMATED:自动构建,表示该镜像是由 Docker Hub 的自动构建流程创建的。

拉取镜像

查找到镜像后,使用 docker pull 命令来拉取该仓库的镜像,以 ubuntu 镜像为例:

$ docker pull ubuntu:16.04
16.04: Pulling from library/ubuntu
7b8b6451c85f: Pull complete 
ab4d1096d9ba: Pull complete 
e6797d1788ac: Pull complete 
e25c5c290bde: Pull complete 
Digest: sha256:e547ecaba7d078800c358082088e6cc710c3affd1b975601792ec701c80cdd39
Status: Downloaded newer image for ubuntu:16.04

该命令指定了 ubuntu 镜像且标签为 16.04 ,如果不指定镜像标签,则默认为 latest 。实际上每个镜像都带有一个标签,所以一个仓库可以存储多个镜像。

注意: 如果没有梯子,国内拉取 Docker Hub 的镜像可能比较慢,建议配置 Docker 官方提供的中国加速地址:https://registry.docker-cn.com

$ vim /etc/docker/daemon.json
{
  "registry-mirrors": [
    "https://registry.docker-cn.com"
  ]
}

将上面的 json 写入文件后,重启服务:

$ systemctl daemon-reload
$ systemctl restart docker

查看配置是否生效:

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

列出镜像

本地镜像都保存在 Docker 宿主机的 /var/lib/docker 目录下,由 Docker 管理并且给我们提供必需的控制命令接口。如果想要查看当前守护进程(docker daemon)中存放和管理的镜像,可以使用 docker images 命令:

$ docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
yublog_db               latest              90ffef425f8a        2 weeks ago         373MB
yublog_web              latest              5e102ae5a68d        2 weeks ago         978MB
ubuntu                  16.04               a51debf7e1eb        6 weeks ago         116MB
jupyter/base-notebook   latest              65eb0b6c51aa        2 months ago        814MB
redis                   latest              0a153379a539        3 months ago        83.4MB
alpine                  3.4                 174b26fe09c7        3 months ago        4.82MB
python                  3                   a9d071760c82        3 months ago        923MB
nginx                   1.13.12             ae513a47849c        8 months ago        109MB

其中,我们可以得到一个镜像列表,其中包含了镜像仓库名,镜像标签,镜像ID,创建时间以及占用空间。

删除镜像

对于不再需要的镜像,可以使用 docker rmi 命令来删除指定镜像:

$ docker rmi a51deb
Untagged: ubuntu:16.04
Untagged: ubuntu@sha256:e547ecaba7d078800c358082088e6cc710c3affd1b975601792ec701c80cdd39
Deleted: sha256:a51debf7e1eb2018400cef1e4b01f2e9f591f6c739de7b5d6c142f954f3715a7
Deleted: sha256:ad38d0b8ff5e07d6875cb39931e2c79fb90cf142584ea813437b013d3639678f
Deleted: sha256:8ae3e0d35735ff77e9ef2a15816747b01316225829ece78dbc41bc50eddb7dfe
Deleted: sha256:a6a57518ff0cc0e30c0e5c964abc052038413f57cd570bd89ab4e4493741a5b3
Deleted: sha256:41c002c8a6fd36397892dc6dc36813aaa1be3298be4de93e4fe1f40b9c358d99

该命令后接镜像名或者镜像ID,需要注意的是如果该镜像创建了容器,则镜像不会被删除。 删除多个镜像:

$ docker rmi 90ffef425f8a 5e102ae5a68d

删除所有镜像:

$ docker rmi `docker images -a -q`

其中,-a 指令会列出一些仓库名和标签为 <none> 的镜像,这类镜像是虚悬镜像(dangling image),这类镜像已经没有任何价值,可以随意删除。-q 指令表示只列出镜像的 ID。

构建镜像

运行一个容器的时候,我们需要使用 docker run 命令,然后指定使用哪个镜像作为容器运行的基础。虽然大部分镜像都可以在 Docker Hub 上找到,但是当没有找到满足我们需求的镜像时,我们就需要自己构建。Docker 可以让我们在一个启动的容器中(如 ubuntu)进行更改,然后使用 docker commit 命令提交更新。但是基本上在所有的情况下,不建议大家这么做,定制镜像应该使用 Dockerfile 来完成,然后使用 dcoker build 命令来构建。

Dockerfile 是 Docker 的镜像构建定义文件,其中包含构建镜像过程中需要执行的命令和其他操作。Dockerfile 相对于 docker commit 来说更具备复用性,构建过程也更加透明。

手动构建镜像

$ docker run -it --name myweb ubuntu:16.04
root@4378d6da340c:/# apt-get -y update && apt-get -y install nginx
root@4378d6da340c:/# echo 'daemon off;' >>/etc/nginx/nginx.conf
root@4378d6da340c:/# exit

我们使用 docker run 命令启动一个名为 myweb 的容器,基于名为 ubuntu:16.04 的镜像,如果 docker 没有在本地发现该镜像,则会从 Docker Hub 上拉取。-i 参数是保证容器开启了 STDIN-t 参数使容器分配一个伪tty终端。进入容器后,我们安装 nginx ,并且在 nginx 的配置文件中添加一行 daemon off; 保持 nginx 在前台一直运行,不会在退出容器后被杀死。接着使用 docker commit 名令提交对容器的修改,指定容器名称或ID,且指定一个目标镜像仓库和镜像名:

$ docker commit -m "first attempt" myweb kyu/myweb:1.0
$ docker run -d -p 80:80 --name webcontainer kyu/myweb:1.0 /bin/bash -c "service nginx start"
$ curl -I http://localhost:80
HTTP/1.1 200 OK

提交镜像后,可以使用 docker images 命令查看到构建的镜像,docker inspect 命令接镜像名称或ID可以查看镜像的详细信息。docker run 以该镜像为基础启动了一个名为 webcontainer 的容器,并且在容器启动时启动 nginx 服务器。由于Docker 容器默认是在前台执行,所有需要加 -d 参数让容器在后台执行,-p 参数控制 Docker 运行时,将容器的80端口映射到宿主机80端口。

Dockerfile定制镜像

我们需要创建一个目录,并且在该目录创建 Dockerfile 文件。该目录即为我们的镜像构建上下文环境。Dokcer 会在构建镜像时将上下文和该上下文中的文件和目录上传到 Docker 守护进程。这样守护进程就可以直接访问到我们想在镜像中放置的任何数据。

# Dockerfile
FROM ubuntu:16.04
MAINTAINER kyu yukun@eager.live

 apt-get -y update \
    && apt-get -y install nginx \
    && echo 'daemon off;' >>/etc/nginx/nginx.conf
EXPOSE 80
CMD ["service", "nginx", "start"]

上面的 Dockerfile 中其实只是写入一条条指令,这些指令会被顺序执行,并且一条指令就做了一次 commit 提交,每一次 commit 实际上就是在上一层镜像上再提交一层,构建一个新的镜像。所以合理安排其中的指令,尽量不要让镜像显得很多层很臃肿。

$ docker build -t kyu/myweb:1.1 .
$ docker run -d -p 80:80 --name webcontainer2 kyu/myweb:1.1
$ curl -I http://localhost:80
HTTP/1.1 200 OK

docker build 命令用来构建镜像,Dockerfile 中的指令会被顺序执行,-t 参数用来指定镜像仓库和名称,建议大家为构建的镜像设置优雅的名称,以方便我们后续的管理。后面的 . 指定了当前目录的 Dockerfile。

Dockerfile指令
  • FROM:Dockerfile 文件第一条指令必须是 FROM ,该指令指定一个基础镜像,后面的指令都是基于该镜像进行;
  • MAINTAINER:该指令主要是保存镜像构建者的信息;
  • RUN:该指令用于在构建镜像中执行指定的命令,多条 RUN 指令,建议串联做一条指令;
  • EXPOSE:该指令表示容器内的应用使用了容器的那个端口;
  • CMD:该指令指定容器__启动时__,需要执行的命令,在 dokcer run 启动容器时可以被覆盖;
  • ENTRYPOINT:该指令和 CMD 指令类似,不过该指令在容器启动时,不会被 docker run 命令覆盖且该命令的参数会作为 ENTRYPOINT 指令的参数,如果同时也定义了 CMD 指令,则 CMD 也会被当中参数传给 ENTRYPOINT 的参数;
  • ADD:该指令将上下文环境下的文件或目录复制到镜像内,不能对构建目录外的文件进行 ADD 操作,要赋值的源文件可以也是一个url地址。如果复制的是一个压缩文件,该指令为自动解压缩到镜像路径。示例:ADD index.html /usr/share/nginx/html/index.html
  • COPY:该指令和 ADD 类似,但是它不会做提取和解压的工作;
  • VOLUME:该指令会在基于该镜像创建的容器中建立挂载点,用于保存一些必要的数据,这些数据不会被提交到镜像中,且可以在多个容器间共享。该指令只能指定容器中的挂载点(可以指定多个),无法指定宿主机上对应的目录。示例:VOLUME /data
  • WORKDIR:该指令指定工作目录,后面构建的每一层镜像也就是每一条指令,都是在该指令指定的目录下工作,如果目录不存在,该指令会自动建立。示例:WORKDIR /work
  • ENV:该指令用于设置环境变量,后面顺序执行的指令可以直接使用该指令定义的变量,运行的容器中的应用也可以直接使用。示例:ENV CONFIG=test

使用容器(Container)

容器是基于一个 Docker 镜像创建的,它还包含一个程序执行环境和一系列指令集合。镜像是 Docker 生命周期中构建打包阶段,而容器则是启动执行阶段。

启动容器

使用 docker run 命令可以启动一个容器,在上面构建镜像的时候已经提到了,不再举例。

列出容器

docker ps 可以列出运行中的容器,增加 -a 参数可以列出所有容器:

$ docker ps -a
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS                      PORTS                    NAMES
b9e4ce3b55ee        kyu/myweb:1.1           "service nginx start"    2 hours ago         Up 2 hours                  0.0.0.0:80->80/tcp       webcontainer2
ad101983fa61        ubuntu:16.04            "/bin/bash"              25 hours ago        Exited (130) 25 hours ago                            myweb

我们可以看到容器的ID,基于的镜像名称,启动时容器最后执行的命令,创建时间,运行状态,端口映射情况,以及容器名称。

停止容器

$ docker stop b9e4ce3b55ee

启动已停止的容器

$ docker start b9e4ce3b55ee

进入运行中的容器

大部分情况,我们的容器时在后台运行的,而如果我们想再次进入容器,可以使用 docker attachdocker exec 命令。以上面运行的 webcontainer2 容器为例:

docker attach

$ docker attach webcontainer2

因为,该容器内部在前台运行着 Nginx ,可以看到,我们虽然进入了容器内,却没有进入交互式会话的 shell 。此外,使用 docker attach 进入容器内部,如果使用 exit 退出容器的话,该容器也会跟着停止。

docker exec

$ docker exec -it webcontainer2 /bin/bash
root@4378d6da340c:/# exit

该命令后面可以接 -i -t 选项,让我们可以进入交互式会话,而该命令进入容器,并使用 exit 退出,并不会导致容器停止。由于该命令后面可以跟指定的选项,我们可以利用它来在运行中的容器内,执行后台任务( $ docker exec -d webcontainer2 touch /etc/new_file )。

查看容器日志

$ docker logs webcontainer2
* Starting nginx nginx

查看最后 10 行日志输出:

$ docker logs --tail 10 webcontainer2

查看容器内进程

$ docker top webcontainer2

查看容器详细信息

$ docker inspect webcontainer2

删除容器

$ docker rm webcontainer2

删除所有容器:

$ docker rm `docker ps -a -q`

网络(Network)

Docker 推崇一个容器运行一个应用进程,而大部分情况下,一个服务不可能只包含一个应用进程。在 Docker 中,容器之间的连接且进行数据交换,是通过网络进行的,即 Docker Networking 。我们可以通过 Docker 封装的命令,很方便的为容器制定相应的通信方案。

创建网络

$ docker network create app
e662833b17caa8701cc0e3f4a37a91cc14fd609712a52a1576f24993f9fe8288

docker network 命令创建一个桥接网络,并且命名为 app,该命令给我们返回了新建网络的ID。

列出网络

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
e662833b17ca        app                 bridge              local
ca3663583680        bridge              bridge              local
22d6c1b30299        host                host                local
c81ee8057a47        none                null                local

Docker 默认创建的网络类型是 bridge ,在 docker network create 命令后添加 -d 选项可以自己选择网络类型为 overlayoverlay 网络允许跨多台宿主机进行容器间通信。

连接容器

$ docker run -d --net app --name cache redis:latest

连接网络只要在 --net 选项后面添加需要连接的网络名称。

查看网络详情

$ docker network inspect app
[
    ...
    "Containers": {
            "cdda3301a31f57604ceac29bf4cc0d8cf02e21e888ac9a28d6aa181482b262a6": {
                "Name": "cache",
                "EndpointID": "10920e91a17f85fce62b510a63f7cbd4d7a7bfa66017d5dc93f18a9eaffd5d89",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            }
        },
    ...
]

可以看到该网络中多了一个连接的容器,以及他的 MAC 和 IP 地址。

容器间通信

$ docker run --net app --name test -it ubuntu:16.04 /bin/bash
root@4378d6da340c:/# apt-get -y update && apt-get -y install redis-tools

要想连接到上面创建的 redis 容器,甚至不需要指定 redis 容器的 IP 地址,只要指定需要连接 redis 容器的容器名称。

root@4378d6da340c:/# redis-cli -h cache
cache:6379>

已有容器连接网络

docker network connect 命令可以使正在运行的容器连接到已有的网络中。

$ docker network connect app webcontainer2
$ docker network inspect app

删除网络

$ docker network rm app

数据交互

在 Docker 中,实现容器内外或者容器之间的数据交互,需要使用到卷。卷是一个被选定的目录,可以绕过 Docker 的分层的联合文件系统(Union File System),提供数据共享的功能。

Bind Mount

挂载宿主机的目录和文件到容器内,只需指定宿主机的路径和容器内的路径,就可以形成挂载映射关系,对目录或文件的修改会直接生效,并且互相可见。

$ mkdir mydocker && cd mydocker
$ touch hello.txt
$ docker run --name test2 -it -v $PWD/hello.txt:/home/hello.txt ubuntu:16.04 /bin/bash
root@4378d6da340c:/# ls /home/
hello.txt
root@4378d6da340c:/# echo 'hello world' > /home/hello.txt
root@4378d6da340c:/# cat /home/hello.txt
hello world!!!
root@4378d6da340c:/# exit
$ cat hello.txt
hello world!!!

使用 -v--volume 选项,并且指定宿主机路径和容器内路径,格式:-v <host-path>:<container-path> ,如果容器内没有该路径,Docker 会自己创建。需要注意的是,这里指定的路径必须是绝对路径,这是为了避免定义数据卷名称的时候,出现混淆。在容器路径文件或目录后接 :ro 或者 :rw 可以指定容器内目录的读写状态:

$ mkdir hello
$ docker run --name test3 -it -v $PWD/hello:/home/hello:ro ubuntu:16.04 /bin/bash

很多时候 Bind Mount 挂载方式,是非常方便的。比如在构建镜像时,不想把应用或者代码构建到镜像中,把宿主机的程序挂载到容器内是很方便的方式。对于程序代码改动频繁,又不想在开发中频繁重构镜像,很适合使用这种挂载方式。在指定一些如 nginx、tomcat 软件的配置文件时,把文件挂载到容器内,也是很好的选择。

Volume

数据卷也是从宿主机中挂载目录到 Docker 容器内,不过挂载的目录有 Docker 进行管理,定义数据卷时只需指定容器内的目录,并且数据卷默认会一直存在,即使引用的容器被删除。这种挂载方式适用于 Docker 写入本地的场景,比如我们想把数据库容器的数据写入到宿主机管理,并且可以让多个容器共享。

$ docker run -d --name db -it -v /var/lib/mysql -e MYSQL_ROOT_PASSWORD=password mysql:5.7

定义数据卷,仍然可以使用 -v 选项,不过我们只需要指定容器内的路径。为了方便我们记住数据卷,还可以指定数据卷的名称,格式:-v <name>:<container-path>

$ docker run -d --name db -it -v dbdata:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=password mysql:5.7
$ docker inspect db
...
    "Mounts": [
            {
                "Type": "volume",
                "Name": "dbdata",
                "Source": "/var/lib/docker/volumes/7c2f70b0ed00045af78106f5e4d91f32d2064442b91e435b9e03ffe9c0f4df22/_data",
                "Destination": "/var/lib/mysql",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],
...

查看容器的详情,可以看到容器中挂载的数据卷信息。其中 Name 是数据卷的名称,如果不指定,默认是数据卷的ID。Source 信息是 Docker 为我们分配用于挂载的宿主机目录。

创建数据卷

除了在启动容器时指定数据卷外,还可以使用 docker volume 命令来创建和管理数据卷。

$ docker volume create mysqldb
$ docker volume ls
DRIVER              VOLUME NAME
local                mysqldb
...

这样,在启动容器时,直接指定该数据卷即可,多个容器共享数据卷,只要指定数据卷名称或ID。

删除数据卷

$ docker volume rm mysqldb

如果有容器引用该数据卷,并且容器未被删除,则无法删除数据卷。数据卷在关联的容器被删除时不会一起被清除,所以在删除容器时加上 -v 选项可以一起删除关联的数据卷,当然需要保证该数据卷没有被其他容器所关联。

删除所以未被容器引用的数据卷:

$ docker volume prune -f

备份和迁移数据卷

如果想要备份或者迁移数据卷,可以使用 docker inspect 命令找到数据卷在宿主机上的位置,然后很轻松的实现备份或者迁移。

但是 Docker 为我们提供了更优雅的方法,我们可以利用数据卷容器共享的优势,指定一个 ubuntu 基础镜像,利用 --volumes-from 选项,挂载目标容器所有的卷,作为一个新的容器,然后执行 tar cvf 命令打包需要备份的目录。

$ mkdir backup
$ docker --rm --volumes-from db -v $PWD/backup:/backup ubuntu tar cvf /backup/backup.tar /var/lib/mysql 

命令解释:

  1. 启动以一个 ubuntu 作基础镜像的容器;
  2. --rm 选项指定该容器进程运行结束后,自动删除容器;
  3. --volumes-from 选项指定了该容器挂载了目标容器 db 的所有卷;
  4. -v 选项将宿主机的 $PWD/backup 目录挂载为 /backup
  5. 将数据卷 /var/lib/mysql 的内容打包为 backup.tar

参考

Docker官方文档

可能是把Docker的概念讲的最清楚的一篇文章