前言 #

这是我在QQ群 325486037 里,碰到的一些问题及其解答,大多是初学 Docker 时常见的问题。其中的回答,是基于我学习和使用Docker过程中的一些认知,多数是遵循 Docker 官方的最佳实践的原则而进行的解答。由于个人能力所限,回答可能会片面或错误。如发现错误后,欢迎告诉我,以方便修正该文档避免误导他人,文档会不定期进行更新。

Docker 配置问题 #

怎么修改了 /etc/default/docker 后不起作用? #

改动真的生效了么?在宿主上运行一下 ps -ef | grep docker 看看,自己做的那些配置有么?没有的话就说明没有生效。那么就要检查原因了,除了简单的忘记了重启 Docker 服务外,还有可能修改错了配置文件。

最近两年处于 Upstart/SysinitV 到 systemd 的过渡期,所以配置服务的方式对于不同的系统是不一样的,要看自己使用的是什么操作系统,以及什么版本。

对于 Upstart 的系统(Ubuntu 14.10或以前的版本,Debian 7或以前的版本),配置文件可能在

  • Ubuntu/Debian: /etc/default/docker

而对于 systemd 的系统(Ubuntu 15.04及以后的版本,Debian 8及以后的版本,CentOS/RHEL 7),配置文件则一般在 /etc/systemd/system/ 下的 docker.service 中。如果已经用命令 systemctl enable docker 启用了 Docker 服务,那么配置文件应该在:

  • /etc/systemd/system/multi-user.target.wants/docker.service

具体位置不同系统不同,而且要注意 Upstart 的服务配置文件和 systemd 的配置文件的格式也不同,不要混淆乱配:

参考官网文档:
https://docs.docker.com/engine/admin/configuring/#ubuntu
https://docs.docker.com/engine/admin/systemd/

Docker 使用问题 #

宿主如果和容器系统不同的话,那不是和虚拟机一样,一层层的调用,那么Docker和虚拟机还有什么差别? #

要把 Windows 和 Linux 分清楚,更要把内核(kernel)和用户空间(userland)分清楚。

容器内的进程是直接运行于宿主内核的,这点和宿主进程一致,只是容器的userland不同,容器的userland由容器镜像提供,也就是说镜像提供了 rootfs

假设宿主是 Ubuntu,容器是 CentOSCentOS 容器中的进程会直接向 Ubuntu 宿主内核发送 syscall,而不会直接或间接的使用任何 Ubuntuuserland的库。

这点和虚拟机有本质的不同,虚拟机是虚拟环境,在现有系统上虚拟一套物理设备,然后在虚拟环境内运行一个虚拟环境的操作系统内核,在内核之上再跑完整系统,并在里面调用进程。

还以上面的例子去考虑,虚拟机中,CentOS 的进程发送 syscall 内核调用,该请求会被虚拟机内的 CentOS 的内核接到,然后 CentOS 内核访问虚拟硬件时,由虚拟机的服务软件截获,并使用宿主系统,也就是 Ubuntu 的内核及userland的库去执行。

而且,Linux 和 Windows 在这点上非常不同。Linux 的进程是直接发 syscall 的,而 Windows 则把 syscall 隐藏于一层层的 DLL 服务之后,因此 Windows 的任何一个进程如果要执行,不仅仅需要 Windows 内核,还需要一群服务来支撑,所以如果 Windows 要实现类似的机制,容器内将不会像 Linux 这样轻量级,而是非常臃肿。看一下微软移植的 Docker 就非常清楚了。

所以不要把 Docker 和虚拟机弄混,Docker容器只是一个进程而已,只不过利用镜像提供的rootfs提供了调用所需的userland库支持,使得进程可以在受控环境下运行而已,它并没有虚拟出一个机器出来。

参考:

https://www.docker.com/what-docker

视频笔记: Windows Server 和 Docker - John Starks

如何在 Docker 容器内使用 docker 命令(比如在 Jenkins 容器中)? #

首先,不要在 Docker 容器中安装、运行 Docker 引擎,也就是所谓的 Docker In Docker (DIND),参考文章:

https://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/

为了让容器内可以构建镜像,应该使用 Docker Remote API 的客户端来直接调用宿主的 Docker Engine。可以是原生的 Docker CLI (docker 命令),也可以是其它语言的库

添加 Docker 命令行 #

下面以定制 jenkins 镜像为例,使用 Dockerfile 添加 docker 命令行可执行文件,并调整权限。

1
2
3
4
5
6
7
8
9
10
11
12
FROM jenkins:alpine
# 下载安装Docker CLI
USER root
RUN curl -O https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz \
&& tar zxvf docker-latest.tgz \
&& cp docker/docker /usr/local/bin/ \
&& rm -rf docker docker-latest.tgz
# 将 `jenkins` 用户的组 ID 改为宿主 `docker` 组的组ID,从而具有执行 `docker` 命令的权限。
ARG DOCKER_GID=999
USER jenkins:${DOCKER_GID}

在这个例子里,我们下载了静态编译的 docker 可执行文件,并提取命令行安装到系统目录下。然后调整了 jenkins 用户的组 ID,调整为宿主 docker 组ID,从而使其具有执行 docker 命令的权限。

组 ID 使用了 DOCKER_GID 参数来定义,以方便进一步定制。构建时可以通过 --build-arg 来改变 DOCKER_GID 的默认值,运行时也可以通过 --user jenkins:1234 来改变运行用户的身份。

这里的基础镜像使用的是 jenkins:alpine,换为非 alpine 的镜像 jenkins:latest 也是一样的。

用下面的命令来构建镜像(假设镜像名为 jenkins-docker):

1
$ docker build -t jenkins-docker .

如果需要构建时调整 docker 组 ID,可以使用 --build-arg 来覆盖参数默认值:

1
$ docker build -t jenkins-docker --build-arg DOCKER_GID=1234 .

在启动容器的时候,将宿主的 /var/run/docker.sock 文件挂载到容器内的同样位置,从而让容器内可以通过 unix socket 调用宿主的 Docker 引擎。

比如,可以用下面的命令启动 jenkins

1
2
3
4
5
$ docker run --name jenkins \
-d \
-p 8080:8080 \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkins-docker

jenkins 容器中,就已经可以执行 docker 命令了,可以通过 docker exec 来验证这个结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ docker exec -it jenkins sh
/ $ id
uid=1000(jenkins) gid=999(ping) groups=999(ping)
/ $ docker version
Client:
Version: 1.12.3
API version: 1.24
Go version: go1.6.3
Git commit: 6b644ec
Built: Wed Oct 26 23:26:11 2016
OS/Arch: linux/amd64
Server:
Version: 1.13.0-rc2
API version: 1.25
Go version: go1.7.3
Git commit: 1f9b3ef
Built: Wed Nov 23 06:32:39 2016
OS/Arch: linux/amd64
/ $

怎么固定容器 IP 地址?每次重启容器都要变化IP地址怎么办? #

一般情况是不需要指定容器IP地址的。这不是虚拟主机,而是容器。其地址是供容器间通讯的,容器间则不用ip直接通讯,而使用主机名、服务名、网络别名。

为了保持向后兼容,docker run 在不指定--net时所在的网络是default bridge,在这个网络下,需要使用 --link 参数才可以让两个容器找到对方。

这是有局限性的,因为这个时候使用的是 /etc/hosts 静态文件来进行的解析,比如一个主机挂了后,重新启动IP可能会改变。虽然说这种改变Docker是可能更新/etc/hosts文件,但是这有诸多问题,可能会因为竞争冒险导致 /etc/hosts 文件损毁,也可能还在运行的容器在取得 /etc/hosts 的解析结果后,不再去监视该文件是否变动。种种原因都可能会导致旧的主机无法通过容器名访问到新的主机。

参考官网文档:https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/

如果可能不要使用这种过时的方式,而是用下面说的自定义网络的方式。

而对于新的环境(Docker 1.10以上),应该给容器建立自定义网络,同一个自定义网络中,可以使用对方容器的容器名、服务名、网络别名来找到对方。这个时候帮助进行服务发现的是Docker 内置的DNS。所以,无论容器是否重启、更换IP,内置的DNS都能正确指定到对方的位置。

参考官网文档:https://docs.docker.com/engine/userguide/networking/work-with-networks/#linking-containers-in-user-defined-networks

建议参考一下我写的 LNMP 的例子:
https://coding.net/u/twang2218/p/docker-lnmp/git

如何修改容器的 /etc/hosts 文件? #

容器内的 /etc/hosts 文件不应该被随意修改,如果必须指定 host,应该在 docker run 时使用 --add-host 参数,或者在 docker-compose.yml 中添加 extra_hosts 项。

不过在用之前,应该再考虑一下真的需要修改 /etc/hosts 么?如果只是为了容器间互相访问,应该建立自定义网络,并使用 Docker 内置的 DNS 服务。

可以参考一下我写的这个 LNMP 多容器互连的例子:https://coding.net/u/twang2218/p/docker-lnmp/git

Docker 容器如何随系统一同启动? #

1
--restart=always

参考官网文档:https://docs.docker.com/engine/reference/commandline/run/#restart-policies-restart

怎么映射宿主端口?Dockerfile 中的EXPOSEdocker run -p 有啥区别? #

Docker中有两个概念,一个叫做 EXPOSE ,一个叫做 PUBLISH

  • EXPOSE 是镜像/容器声明要暴露该端口,可以供其他容器使用。这种声明,在没有设定 --icc=false的时候,实际上只是一种标注,并不强制。也就是说,没有声明 EXPOSE 的端口,其它容器也可以访问。但是当强制 --icc=false 的时候,那么只有 EXPOSE 的端口,其它容器才可以访问。
  • PUBLISH 则是通过映射宿主端口,将容器的端口公开于外界,也就是说宿主之外的机器,可以通过访问宿主IP及对应的该映射端口,访问到容器对应端口,从而使用容器服务。

EXPOSE 的端口可以不 PUBLISH,这样只有容器间可以访问,宿主之外无法访问。而 PUBLISH 的端口,可以不事先 EXPOSE,换句话说 PUBLISH 等于同时隐式定义了该端口要 EXPOSE

docker run 命令中的 -p, -P 参数,以及 docker-compose.yml 中的 ports 部分,实际上均是指 PUBLISH

小写 -p 是端口映射,格式为 [宿主IP:]<宿主端口>:<容器端口>,其中宿主端口和容器端口,既可以是一个数字,也可以是一个范围,比如:1000-2000:1000-2000。对于多宿主的机器,可以指定宿主IP,不指定宿主IP时,守护所有接口。

大写 -P 则是自动映射,将所有定义 EXPOSE 的端口,随机映射到宿主的某个端口。

我要映射好几百个端口,难道要一个个-p么? #

-p 是可以用范围的:

1
-p 8001-8010:8001-8010

vethxxxx 这种虚拟网卡和容器的对应关系从哪里看? #

北京-ZZ-虾米提供了一个好办法。

1
2
3
4
$ docker network ls
NETWORK ID NAME DRIVER
56f04389b8f0 dockerlnmp_backend bridge
094fcb269385 dockerlnmp_frontend bridge

注意这里的 NETWORK ID,然后运行 ip a | grep veth

1
2
3
4
5
6
$ ip a | grep veth
12: veth22996d2@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-56f04389b8f0 state UP group default
14: veth34ace9a@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-56f04389b8f0 state UP group default
16: veth0bb3771@if15: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-56f04389b8f0 state UP group default
22: veth399b874@if21: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-094fcb269385 state UP group default
24: vethf24a0a9@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-094fcb269385 state UP group default

注意这里的 br-56f04389b8f0 以及 br-094fcb269385br-后面的是上面的网络id,由此可以看出 veth 和 Docker 网络的对应关系,而容器都是连接到了某个Docker网络上的,从而就有了容器和veth的对应关系。

对于某个网络出现了多个 veth 的情况,可以观察 veth22996d2@if11 后面的 if11 这部分,和容器内的 ip addr 的结果,一般 奇-偶是一对。

容器磁盘可以限制配额么? #

对于 devicemapper, btrfs, zfs 来说,可以通过 --storage-opt size=100G 这种形式限制 rootfs 的大小。

1
docker create -it --storage-opt size=120G fedora /bin/bash

参考官网文档:https://docs.docker.com/engine/reference/commandline/run/#/set-storage-driver-options-per-container

docker stats 显示的只有容器ID,怎么才能显示容器名字? #

1
docker stats $(docker ps --format='{{.Names}}')

容器内的数据该保存在镜像里还是物理机里? #

如果所谓数据是指运行时动态的数据,那么这部分数据文件不应该保存于镜像内。在运行时要保持容器基础文件不可变的特性,而变化部分使用挂载宿主目录,或者数据卷来解决。

建议看一下官网 docker volume 的文档:https://docs.docker.com/engine/tutorials/dockervolumes/

看到总说要保持容器无状态,那什么是无状态? #

这里说到的有两个层面的无状态:

容器存储层的无状态 #

这里提到的存储层是指用于存储镜像、容器各个层的存储,一般是Union FS,如 AUFS,或者是使用块设备的一些机制(如snapshot)进行模拟,如 devicemapper

存储层不应该有任何文件变化,所有变化部分用进行持久化。由于卷的生存周期和容器不同,容器消亡重建,卷不会跟随消亡。所以容器可以随便删了重新run,而其挂载的则会保持之前的数据。

服务层面的无状态 #

使用卷持久化容器状态,虽然从存储层的角度看,是无状态的,但是从服务层面看,这个服务是有状态的。

从服务层面上说,也存在无状态服务。就是说服务本身不需要写入任何文件。比如前端 nginx,它不需要写入任何文件(日志走Docker日志驱动),中间的 php, node.js 等服务,可能也不需要本地存储,它们所需的数据都在 redis, mysql, mongodb 中了。这类服务,由于不需要卷,也不发生本地写操作,删除、重启、不保存自身状态,并不影响服务运行,它们都是无状态服务。这类服务由于不需要状态迁移,不需要分布式存储,因此它们的集群调度更方便。

之前没有 docker volume 的时候,有些人说 Docker 只可以支持无状态服务,原因就是只看到了存储层需求无状态,而没有 docker volume 的持久化解决方案。

现在这个说法已经不成立,服务可以有状态,状态持久化用 docker volume

当服务可以有状态后,如果使用默认的local卷驱动,并且使用本地存储进行状态持久化的情况,单机服务、容器的再调度运行没有问题。但是顾名思义,使用本地存储的卷,只可以为当前主机提供持久化的存储,而无法跨主机。

但这只是使用默认的 local 驱动,并且使用 本地存储 而已。使用分布式/共享存储就可以解决跨主机的问题。docker volume 自然支持很多分布式存储的驱动,比如 flockerglusterfscephipfs 等等。常用的插件列表可以参考官方文档:https://docs.docker.com/engine/extend/legacy_plugins/#/volume-plugins

Dockerfile 中的VOLUMEdocker run -v,以及Compose中的volumes都有什么区别? #

先明白几个概念,挂载分为挂载本地宿主目录,或者挂载数据卷,而数据卷又分为匿名数据卷命名数据卷

那么,在Dockerfile中定义的是挂载是指匿名数据卷

这个设置可以在运行时覆盖。通过 docker run-v 参数或者 docker-compose.ymlvolumes 指定。使用命名卷的好处是可以复用,其它容器可以通过这个命名数据卷的名字来指定挂载,共享其内容(不过要注意并发访问的竞争问题)。

数据卷默认可能会保存于 /var/lib/docker/volumes,不过一般不需要、也不应该访问这个位置。

多个 Docker 容器之间共享数据怎么办?NFS? #

如果是同一个宿主,那么可以绑定同一个数据卷,当然,程序上要处理好并发问题。

如果是不同宿主,则可以使用分布式数据卷驱动,让分布在不同宿主的容器都可以访问到的分布式存储的位置。如S3之类:

https://docs.docker.com/engine/extend/plugins/#volume-plugins

既然一个容器一个应用,那么我想在该容器中用计划任务 cron 怎么办? #

cron 其实是另一个服务了,所以应该另起一个容器来进行,如需访问该应用的数据文件,那么可以共享该应用的数据卷即可。而 cron 的容器中,cron 以前台运行即可。

比如,我们希望有个 python 脚本可以定时执行。那么可以这样构建这个容器。

首先基于 python 的镜像定制:

1
2
3
4
5
6
7
8
9
10
FROM python:3.5.2
ENV TZ=Asia/Shanghai
RUN apt-get update \
&& apt-get install -y cron \
&& apt-get autoremove -y
COPY ./cronpy /etc/cron.d/cronpy
CMD ["cron", "-f"]

其中所提及的 cronpy 就是我们需要计划执行的cron脚本。

1
* * * * * root /app/task.py >> /var/log/task.log 2>&1

在这个计划中,我们希望定时执行 /app/task.py 文件,日志记录在 /var/log/task.log 中。这个 task.py 是一个非常简单的文件,其内容只是输出个时间而已。

1
2
3
#!/usr/local/bin/python
from datetime import datetime
print("Cron job has run at {0} with environment variable ".format(str(datetime.now())))

task.py 可以在构建镜像时放进去,也可以挂载宿主目录。在这里,我以挂载宿主目录举例。

1
2
3
4
5
6
7
8
9
10
# 构建镜像
docker build -t cronjob:latest .
# 运行镜像
docker run \
--name cronjob \
-d \
-v $(pwd)/task.py:/app/task.py \
-v $(pwd)/log/:/var/log/ \
cronjob:latest

需要注意的是,应该在构建主机上赋予 task.py 文件可执行权限。

docker pull 下来的镜像文件都在哪? #

初学 Docker 要反复告诫自己,Docker 不是虚拟机。

Docker不是虚拟机,Docker 镜像也不是虚拟机的 ISO 文件。Docker 的镜像是分层存储,每一个镜像都是由很多层,很多个文件组成。而不同的镜像是共享相同的层的,所以这是一个树形结构,不存在具体哪个文件是 pull 下来的镜像的问题。

具体镜像保存位置取决于系统,一般Linux系统下,在 /var/lib/docker 里。对于使用 Union FS 的系统(Ubuntu),如 aufs, overlay2 等,可以直接在 /var/lib/docker/{aufs,overlay2} 下看到找到各个镜像的层、容器的层,以及其中的内容。

但是,对于CentOS这类没有Union FS的系统,会使用如devicemapper这类东西的一些特殊功能(如snapshot)模拟,镜像会存储于块设备里,因此无法看到具体每层信息以及每层里面的内容。

需要注意的是,默认情况下,CentOS/RHEL 使用 lvm-loop,也就是本地稀疏文件模拟块设备,这个文件会位于 /var/lib/docker/devicemapper/devicemapper/data 的位置。这是非常不推荐的,如果发现这个文件很大,那就说明你在用 devicemapper + loop 的方式,不要这么做,去参照官方文档,换 direct-lvm,也就是分配真正的块设备给 devicemapper 去用。

docker images 命令显示的镜像占了好大的空间,怎么办?每次都是下载这么大的镜像? #

这个显示的大小是计算后的大小,要知道 docker image 是分层存储的,在1.10之前,不同镜像无法共享同一层,所以基本上确实是下载大小。但是从1.10之后,已有的层(通过SHA256来判断),不需要再下载。只需要下载变化的层。所以实际下载大小比这个数值要小。而且本地硬盘空间占用,也比docker images列出来的东西加起来小很多,很多重复的部分共享了。

docker images -a 后显示了好多 <none> 的镜像?都是什么呀?能删么? #

简单来说,<none> 就是说该镜像没有打标签。而没有打标签镜像一般分为两类,一类是依赖镜像,一类是丢了标签的镜像

依赖镜像 #

Docker的镜像、容器的存储层是Union FS,分层存储结构。所以任何镜像除了最上面一层打上标签(tag)外,其它下面依赖的一层层存储也是存在的。这些镜像没有打上任何标签,所以在 docker images -a 的时候会以 <none> 的形式显示。注意观察一下 docker pull 的每一层的sha256的校验值,然后对比一下 <none> 中的相同校验值的镜像,它们就是依赖镜像。这些镜像不应当被删除,因为有标签镜像在依赖它们。

丢了标签的镜像 #

这类镜像可能本来有标签,后来丢了。原因可能很多,比如:

  • docker pull 了一个同样标签但是新版本的镜像,于是该标签从旧版本的镜像转移到了新版本镜像上,那么旧版本的镜像上的标签就丢了;
  • docker build 时指定的标签都是一样的,那么新构建的镜像拥有该标签,而之前构建的镜像就丢失了标签。

这类镜像被称为 dangling (晃荡着的)镜像,这些镜像可以删除,使用 dangling=true 过滤条件即可。

手动删除 dangling 镜像

1
docker rmi $(docker images -aq -f "dangling=true")

对于频繁构建的机器,比如 Jenkins 之类的环境。手动清理显然不是好的办法,应该定期执行固定脚本来清理这些无用的镜像。很幸运,Spotify 也面临了同样的问题,他们已经写了一个开源工具来做这件事情:https://github.com/spotify/docker-gc

为什么 Docker Hub 的镜像尺寸和 docker images 不一致? #

Docker Hub上显示的是经过 gzip 压缩后的镜像大小,这个大小也是你将下载的镜像大小,一般来说也是 Docker Hub 用户最关心的大小。

docker images 显示的是pull下来并解压缩后的大小,因为使用docker images的时候更关心的是本地磁盘空间占用的大小,所以这里显示的是未压缩镜像的大小。

Docker 日志都在哪里?怎么收集? #

日志分两类,一类是 docker daemon 日志,既 Docker 引擎服务日志;另一类是 容器日志

docker daemon 日志 一般是交给了 Upstart(Ubuntu 14.04) 或者 systemd (CentOS 7, Ubuntu 16.04)。前者一般位于 /var/log/upstart/docker.log 下,后者一般通过 jounarlctl -u docker 来读取。不同系统的位置都不一样,SO上有人总结了一份列表,我稍微修正了一下,可以参考:

系统 日志位置
Ubuntu(14.04) /var/log/upstart/docker.log
Ubuntu(16.04) journalctl -u docker.service
CentOS 7/RHEL 7/Fedora journalctl -u docker.service
CoreOS journalctl -u docker.service
OpenSuSE journalctl -u docker.service
OSX ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/log/d‌​ocker.log
Debian GNU/Linux 7 /var/log/daemon.log
Debian GNU/Linux 8 journalctl -u docker.service
Boot2Docker /var/log/docker.log

容器的日志,则可以通过 docker logs 命令来访问,而且可以像 tail -f 一样,使用 docker logs -f 来实时查看。如果使用 Docker Compose,则可以通过 docker-compose logs <服务名> 来查看。

如果深究其日志位置,每个容器的日志默认都会以 json-file 的格式存储于 /var/lib/docker/containers/<容器id>/<容器id>-json.log 下,不过并不建议去这里直接读取内容,因为Docker提供了更完善地日志收集方式 - Docker 日志收集驱动

关于日志收集,Docker 内置了很多日志驱动,可以通过类似于 fluentd, syslog 这类服务收集日志。无论是 docker daemon,还是容器,都可以使用日志驱动。比如,如果打算用 fluentd 收集某个容器日志,可以这样启动容器:

1
2
3
4
5
$ docker run -d \
--log-driver=fluentd \
--log-opt fluentd-address=10.2.3.4:24224 \
--log-opt tag="docker.{{.Name}}" \
nginx

其中 10.2.3.4:24224fluentd 服务地址,实际环境中应该换成真实的地址。

我用的是阿里云 Ubuntu 14.04 主机,内核还是3.13,怎么办? #

其实 Ubuntu 14.04 官方维护的内核已经到 4.4 了,可以通过下面的命令升级内核:

1
sudo apt-get install -y --install-recommends linux-generic-lts-xenial

如何动态修改内存限制? #

Docker 1.10 之后支持动态修改,使用 docker update 命令,如:

1
docker update -m 300m

经常在各种 Docker 命令里看到 --labellabel 是什么?干什么用的? #

Label键值对,是 metadata,是贯穿于 Docker 各个资源的,包括引擎、镜像、容器、卷、网络、Swarm 节点、服务等。

  • key:格式要求只可以包含字母和数字,以及.-。推荐使用类似于 Java 那种反向域名格式,如 com.example.mytag
  • value:格式必须是字符串,除了普通字符串外,还可以是 JSON, XML, CSV 或者 YAML,当然,需要先进行序列化。

当资源很少的时候,我们可以直接对一个个资源进行操作,但是,在管理很多资源的时候,这么做就变得不大现实。经常的需求是针对某一类的资源进行操作,而不是一个个的操作。这种情况,经常会使用 label 来帮助实现。

当创建一个资源的时候,可以指定这个资源的 label(一个资源可以有很多个 label),而当创建了很多个资源的时候,就可以通过过滤 label 的键、值来得到所需的资源列表。

比如,我们可以使用 docker run 运行一堆容器,在运行时,通过 label 指定容器是架构中的哪一部分。

  • 前端:--label type=frontend
  • 中间件:--label type=middleware
  • 存储:--label type=storage

在后期维护时,可以直接过滤显示想要的容器,比如我们只想看前端容器运行情况:

1
docker ps --filter label=type=frontend

而且,还可以进一步的和其它命令配合操作这组容器,比如我们需要停止所有前端容器:

1
docker stop $(docker ps -f label=type=frontend)

使用 label 在集群调度中也非常有用。

比如,我们可以在不同的 Docker 主机的引擎 dockerd 参数中,通过 label 来加入存储类型的信息,如:

  • 存储类型为 SSD--label storage=ssd
  • 存储类型为 HDD--label storage=hdd

对于数据库的服务,我们自然希望跑在 SSD 上以获得更大的性能,而日志、备份服务则希望跑在 HDD 上获得更高的容量。那么可以这么做:

1
2
3
4
docker service create \
--name mysql \
--constraint 'engine.labels.storage == ssd' \
mysql

添加label以及过滤 #

添加 label 大多格式都是在创建、修改资源时,使用 --label <key>=<value> 参数(部分命令提供了 -l 缩写形式)。value 可以省略,格式为 --label <key>。如果需要定义多组 label,只需多组 --label 即可。

过滤 label 则大多发生在列表命令中,使用 --filter label=<key>=<value>,或者对于不关心 value 的情况,--filter label=<key>(部分命令提供了 -f 的缩写形式)。

下面的列表,列出了支持 label 的命令(除非特殊声明,”添加”命令使用 --label 选项添加 label;”过滤”命令使用 --filter 过滤label):

除了上述资源外,docker events 也可以使用 label 过滤结果:https://docs.docker.com/engine/reference/commandline/events/

集群调度约束 #

如:

1
2
3
4
5
6
version: "2"
services:
redis:
image: redis
environment:
- "constraint:storage==ssd"

如下面的例子中,使用 Swarm 节点label 进行约束(注意,这次用的不是引擎label):

1
2
3
4
docker service create \
--name web \
--constraint 'node.labels.type == frontend' \
nginx

Dockerfile 相关问题 #

docker commit 怎么用啊? #

简单的回答就是,不要用 commit,去写 Dockerfile

Docker 不是虚拟机。这句话要在学习 Docker 的过程中反复提醒自己。所以不要把虚拟机中的一些概念带过来。

Docker 提供了很好的 Dockerfile 的机制来帮助定制镜像,可以直接使用 Shell 命令,非常方便。而且,这样制作的镜像更加透明,也容易维护,在基础镜像升级后,可以简单地重新构建一下,就可以继承基础镜像的安全维护操作。

使用 docker commit 制作的镜像被称为黑箱镜像,换句话说,就是里面进行的是黑箱操作,除本人外无人知晓。即使这个制作镜像的人,过一段时间后也不会完整的记起里面的操作。那么当有些东西需要改变时,或者因基础镜像更新而需要重新制作镜像时,会让一切变得异常困难,就如同重新安装调试配置服务器一样,失去了 Docker 的优势了。

另外,Docker 不是虚拟机,其文件系统是 Union FS,分层式存储,每一次 commit 都会建立一层,上一层的文件并不会因为 rm 而删除,只是在当前层标记为删除而看不到了而已,每次 docker pull 的时候,那些不必要的文件都会如影随形,所得到的镜像也必然臃肿不堪。而且,随着文件层数的增加,不仅仅镜像更臃肿,其运行时性能也必然会受到影响。这一切都违背了 Docker 的最佳实践。

使用 commit 的场合是一些特殊环境,比如入侵后保存现场等等,这个命令不应该成为定制镜像的标准做法。所以,请用 Dockerfile 定制镜像。

Dockerfile 怎么写? #

最直接也是最简单的办法是看官方文档。

这篇文章讲述具体Dockerfile的命令语法:https://docs.docker.com/engine/reference/builder/

然后,学习一下官方的Dockerfile最佳实践:https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/

最后,去 Docker Hub 学习那些Official的镜像Dockerfile咋写的。

Dockerfile 就是 shell 脚本吧?那我懂,一行行把需要装的东西都写进去不就行了。 #

不是这样的。Dockerfile 不等于 .sh 脚本

Dockerfile 确实是描述如何构建镜像的,其中也提供了 RUN 这样的命令,可以运行 shell 命令。但是和普通 shell 脚本还有很大的不同。

Dockerfile 描述的实际上是镜像的每一层要如何构建,所以每一个RUN是一个独立的一层。所以一定要理解“分层存储”的概念。上一层的东西不会被物理删除,而是会保留给下一层,下一层中可以指定删除这部分内容,但实际上只是这一层做的某个标记,说这个路径的东西删了。但实际上并不会去修改上一层的东西。每一层都是静态的,这也是容器本身的immutable特性,要保持自身的静态特性。

所以很多新手会常犯下面这样的错误,把 Dockerfile 当做 shell 脚本来写了:

1
2
3
4
5
6
RUN yum update
RUN yum -y install gcc
RUN yum -y install python
ADD jdk-xxxx.tar.gz /tmp
RUN cd xxxx && install
RUN xxx && configure && make && make install

这是相当错误的。除了无畏的增加了很多层,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。

正确的写法应该是把一个任务放到一个 RUN 下,多条命令应该用 && 连接,并且在最后要打扫干净所使用的环境。比如下面这段摘自官方 redis 镜像 Dockerfile 的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
RUN buildDeps='gcc libc6-dev make' \
&& set -x \
&& 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_SHA1 *redis.tar.gz" | sha1sum -c - \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& rm redis.tar.gz \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
&& rm -r /usr/src/redis \
&& apt-get purge -y --auto-remove $buildDeps

context 到底是一个什么概念? #

context,上下文,是 docker build 中很重要的一个概念。构建镜像必须指定 context

1
docker build -t xxx <context路径>

或者 docker-compose.yml 中的

1
2
3
4
app:
build:
context: <context路径>
dockerfile: dockerfile

这里都需要指定 context

context 是工作目录,但不要和构建镜像的Dockerfile 中的 WORKDIR 弄混,contextdocker build 命令的工作目录。

docker build 命令实际上是客户端,真正构建镜像并非由该命令直接完成。docker build 命令将 context 的目录上传给 docker daemon,由它负责制作镜像。

在 Dockerfile 中如果写 COPY ./package.json /app/ 这种命令,实际的意思并不是指执行 docker build 所在的目录下的 package.json,也不是指 Dockerfile 所在目录下的 package.json,而是指 context 目录下的 package.json

这就是为什么有人发现 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因,因为它们都在 context 之外,如果真正需要,应该将它们复制到 context 目录下再操作。

docker build -t xxx . 中的这个.,实际上就是在指定 Context 的目录,而并非是指定 Dockerfile 所在目录。

默认情况下,如果不额外指定 Dockerfile 的话,会将 Context 下的名为 Dockerfile 的文件作为 Dockerfile。所以很多人会混淆,认为这个 . 是在说 Dockerfile 的位置,其实不然。

ENTRYPOINTCMD 到底有什么不同? #

Dockerfile 的目的是制作镜像,换句话说,实际上是准备的是主进程运行环境。那么准备好后,需要执行一个程序才可以启动主进程,而启动的办法就是调用 ENTRYPOINT,并且把 CMD 作为参数传进去运行。也就是下面的概念:

1
ENTRYPOINT "CMD"

假设有个 myubuntu 镜像 ENTRYPOINTsh -c,而我们 docker run -it myubuntu uname -a。那么 uname -a 就是运行时指定的 CMD,那么 Docker 实际运行的就是结合起来的结果:

1
sh -c "uname -a"
  • 如果没有指定 ENTRYPOINT,那么就只执行 CMD
  • 如果指定了 ENTRYPOINT 而没有指定 CMD,自然执行 ENTRYPOINT;
  • 如果 ENTRYPOINTCMD 都指定了,那么就如同上面所述,执行 ENTRYPOINT "CMD"
  • 如果没有指定 ENTRYPOINT,而 CMD 用的是上述那种 shell 命令的形式,则自动使用 sh -c 作为 ENTRYPOINT

注意最后一点的区别,这个区别导致了同样的命令放到 CMDENTRYPOINT 下效果不同,因此有可能放在 ENTRYPOINT 下的同样的命令,由于需要 tty 而运行时忘记了给(比如忘记了docker-compose.ymltty:true)导致运行失败。

这种用法可以很灵活,比如我们做个 git 镜像,可以把 git 命令指定为 ENTRYPOINT,这样我们在 docker run 的时候,直接跟子命令即可。比如 docker run git log 就是显示日志。

拿到一个镜像,如何获得镜像的 Dockerfile#

  • 直接去 Docker Hub 上看:大多数 Docker Hub 上的镜像都会有 Dockerfile,直接在 Docker Hub 的镜像页面就可以看到 Dockerfile 的链接;
  • 如果是自己公司做的,最简单的办法就是打个电话、发个消息问一下。别看这个说法看起来很傻,不少人都宁可自己琢磨也不去问;
  • 如果没有 Dockerfile,一般这类镜像就不应该考虑使用了,这类黑箱似的镜像很容有有问题。如果是什么特殊原因,那继续往下看;
  • docker history 可以看到镜像每一层的信息,包括命令,当然黑箱镜像的 commit 看不见操作;
  • docker inspect 可以分析镜像很多细节。
  • 直接运行镜像,进入shell,然后根据上面的分析结果去进一步分析日志、文件内容及变化。
  • 经过分析后,自己写 Dockerfile 还原操作。

在你的 LNMP 的例子中,PHP 的 Dockerfile 里面的 “构建依赖” 和 “运行依赖” 都是什么意思? #

这里所提到的是我的那个 LNMP 例子的 php 服务的 Dockerfilehttps://coding.net/u/twang2218/p/docker-lnmp/git/blob/master/php/Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
FROM php:7-fpm
RUN set -xe \
# "构建依赖"
&& buildDeps=" \
build-essential \
php5-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libmcrypt-dev \
libpng12-dev \
" \
# "运行依赖"
&& runtimeDeps=" \
libfreetype6 \
libjpeg62-turbo \
libmcrypt4 \
libpng12-0 \
" \
# "安装 php 以及编译构建组件所需包"
&& apt-get update \
&& apt-get install -y ${runtimeDeps} ${buildDeps} --no-install-recommends \
# "编译安装 php 组件"
&& docker-php-ext-install iconv mcrypt mysqli pdo pdo_mysql zip \
&& docker-php-ext-configure gd \
--with-freetype-dir=/usr/include/ \
--with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install gd \
# "清理"
&& apt-get purge -y --auto-remove \
-o APT::AutoRemove::RecommendsImportant=false \
-o APT::AutoRemove::SuggestsImportant=false \
$buildDeps \
&& rm -rf /var/cache/apt/* \
&& rm -rf /var/lib/apt/lists/*

这里是针对 php 镜像进行定制,默认情况下 php:7-fpm 中没有安装所需的 mysqli, pdo_mysql, gd 等组件,所以这里需要安装,而且,部分组件还需要编译。

因此,这里涉及了两类依赖库/工具,一类是安装、编译阶段所需要的依赖;另一类是运行时所需的依赖。要记住 Dockerfile 的最佳实践中要求最终镜像只应该保留最小的所需依赖,因此安装构建的依赖应该在安装结束后清除,这一层只保留真正需要的运行时依赖。

因此,遵循最佳实践的要求,这里区分了 buildDepsruntimeDeps 后,可以在安装结束后,卸载、清理 buildDeps 的依赖。这样确保没有无关的东西还在该层中。

应用代码是应该挂载宿主目录还是放入镜像内? #

两种方法都可以。

如果代码变动非常频繁,比如开发阶段,代码几乎每几分钟就需要变动调试,这种情况可以使用 --volume 挂载宿主目录的办法。这样不用每次构建新镜像,直接再次运行就可以加载最新代码,甚至有些工具可以观察文件变化从而动态加载,这样可以提高开发效率。

如果代码没有那么频繁变动,比如发布阶段,这种情况,应该将构建好的应用放入镜像。一般来说是使用 CI/CD 工具,如 Jenkins, Drone.io, Gitlab CI 等,进行构建、测试、制作镜像、发布镜像、以及分步发布上线。

对于配置文件也是同样的道理,如果是频繁变更的配置,可以挂载宿主,或者动态配置文件可以使用卷。但是对于并非频繁变更的配置文件,应该将其纳入版本控制中,走 CI/CD 流程进行部署。

需要注意的一点是,绑定宿主目录虽然方便,但是不利于集群部署,因为集群部署前还需要确保集群各个节点同步存在所挂载的目录及其内容。因此集群部署更倾向于将应用打入镜像,方便部署。

Docker Compose 相关问题 #

你那个LNMP例子中的 docker-compose.yml 中有好多 networks,都是什么意思啊? #

我写的LNMP多容器互通的例子:https://coding.net/u/twang2218/p/docker-lnmp/git

前面services下的每个服务下面的networks,是说这个服务要接到哪个网络上。
而最后的那个总的networks下面的,是这几个网络的定义。

也就是说,nginx 接到了名为 frontend 的前端网络;mysql 接到了名为 backend 的后端网络;而作为中间的 php 既需要和 nginx 通讯,又需要和 mysql 通讯,所以同时连接了 frontendbackend 网络。由于 nginxmysql 不处于同一网络,所以二者无法通讯,起到了隔离的作用。

关于Docker自定义网络,你可以看一下官方文档的介绍:
https://docs.docker.com/engine/userguide/networking/dockernetworks/#/user-defined-networks

关于在Docker Compose中使用自定义网络的部分,可以看官方这部分文档:
https://docs.docker.com/compose/networking/

使用 Compose 的时候碰到 “An HTTP request took too long to complete….” 错误,怎么办? #

Compose 的请求超时时限是可以配置的:

1
export COMPOSE_HTTP_TIMEOUT=120

不过,这不是问题的解决办法,因为一般情况下不应该超时,超时的原因是因为所访问的 Docker Engine 过于繁忙,而无法响应 Compose 的请求。应该检查具体 Docker Engine 出了什么问题,是不是还在用着 CentOS 默认的 device mapper 的 loop 设备,等等。

Docker Swarm 相关问题 #

Docker 多宿主网络怎么配置? #

我写了一个配置的例子,可以在这里看。
https://gist.github.com/twang2218/def4097648deac398a949b58e2a31610

其中两个脚本:

  • 带swarm一起玩 overlay:build-overlay-with-swarm.sh
  • 不带swarm玩,直接构建overlay:build-overlay-without-swarm.sh

Swarm环境中怎么指定某个容器在指定的宿主上运行呢? #

每个 Docker Host 建立时都可以通过 --label 指定其 Docker Daemon 的标签,比如:

1
2
3
docker daemon \
--label com.example.environment="production" \
--label com.example.storage="ssd"

注意,上面的配置参数应该配置在 docker daemon 的配置文件里,如 docker.service,而不是简单的命令行执行……

然后运行容器时,使用环境变量约束调度即可。可以使用 Compose 文件的 environment 配置,也可以使用 docker run-e 环境变量参数。下面以 Compose 配置文件为例:

1
2
3
4
5
6
version: "2"
services:
mongodb:
image: mongo:latest
environment:
- "constraint:com.example.storage==ssd"

这样这个 mongodb 的服务就会运行在标记为 com.example.storage="ssd" 的宿主上运行。

为什么 Swarm 集群的 overlay network 跨宿主无法互访? #

首先,检查建立 Swarm 的时候,对其它节点所宣告的本节点的地址是否正确。

对于单网卡、单IP的宿主,Swarm 会自动选择网卡地址,但是多网卡、多IP的宿主,就必须手动宣告地址。

  • 对于一代 Swarm 而言,检查一下 docker daemon 的配置中,--cluster-advertise 地址是否配置正确。
  • 对于二代 Swarm,则检查一下创建、加入 Swarm 的时候,--advertise-addr 是否填写正确。

宣告地址必须是全集群可以互访的,由于该地址端口是 Docker Remote API 端口,所以可以用 curl 来连接其它节点,以判断互通性。

然后,检查宿主间的网络互通问题,特别是宿主的防火墙开启的情况下,检查下列服务端口有没有放开:

  • 7946/{udp,tcp}
  • 4789/{udp,tcp}
  • {2375,2376,2377,3375,3376}/tcp (具体端口取决于实际 SwarmEngine 守护端口)

可以通过 telnet, curl 之类的工具确保上述端口可以互访。

如果还是有问题,可以进一步启用各个 docker daemon 的调试模式。和配置 --insecure-registry 一样,编辑 Docker 配置文件,在 dockerd 后添加 -D 参数。然后重新启动,建立集群、网络、服务。如果问题重现,可以分析 Docker Daemon 的日志,具体查看日志的方法见前面的问答。

需要注意的是二代 Swarm 创建的service默认的 --endpoint-modevip,也就是虚拟IP,利用IPVS做负载均衡的那个。使用服务名解析的时候,会返回这个vip地址,该地址是无法跨宿主 ping 的。因此这种情况下要验证跨宿主服务互通性,应该直接确认TCPUDP服务是否可以访问。如果必须ping,那么不要去ping这个vip,使用服务容器真实地址tasks.<服务名>,比如服务名为web的情况下,使用命令ping tasks.web

参考:

https://docs.docker.com/swarm/plan-for-production/

https://docs.docker.com/engine/swarm/swarm-tutorial/#/open-ports-between-the-hosts

https://docs.docker.com/engine/swarm/networking/

Docker 二代Swarm (既 Swarm Mode),docker service create 不可以使用 -v 那怎么使用卷(Volume)? #

从二代 Swarm 开始,将使用 --mount 参数来进行卷挂载,并且对语义进行更明确的划分。

挂载分两种:

  • 绑定挂载 bind-mount:这类挂载将宿主目录/文件绑定到容器的某个位置。这类挂载需要注意宿主和容器的不同uid导致的权限、访问控制差异问题。
  • 数据卷 data volumes:这类挂载是之前推荐使用的卷。卷可以分为命名卷named volume以及匿名卷anonymous volume

挂载参数的格式基本上为 --mount <key1>=<value1>[,<key2>=<value2>,...]

主要参数有:

  • type: 如之前所说,两种类型: volumebind。如果不指定 type,默认为 volume
  • srcsource:源:
    • 如果 type=volumesrc 则是卷的名字,是可选项。如果存在就是命名卷,如果没指定 src 则是匿名卷;
    • 如果 type=bindsrc 是宿主本地的路径;
  • dstdestinationtarget:将在容器内挂载的路径。如果路径不存在,会在挂载前自动建立路径。
  • readonlyro:是否让该挂载为只读,默认是读写。

除此之外还有一些常见的参数可以设置:

  • volume-driver:指定卷驱动,默认是 local,可以通过这个参数指定其它(如 flocker, glusterfs, ceph)之类的驱动;
  • volume-label:指定卷的元数据(metadata),从而方便过滤操作;
  • volume-opt:不同的卷驱动可能需要额外的参数,这个选项可以指定这些参数。

--mount--volume 有一些差异需要注意:

  • --mount 可以直接使用卷,而无需事先使用 docker volume create 来创建卷,并且可以多组不同驱动的卷;
  • --mount 如果 type=bind 的话,宿主必须存在指定目录,否则报错。而 --volume 则在宿主不存在该路径时,在宿主创建一个空目录来进行绑定。

举几个例子:

挂载命名卷:

1
2
3
4
5
docker service create \
--name my-service \
--replicas 3 \
--mount type=volume,source=my-volume,destination=/path/in/container,volume-label="color=red",volume-label="shape=round" \
nginx:alpine

挂载匿名卷:

1
2
3
4
5
docker service create \
--name my-service \
--replicas 3 \
--mount type=volume,destination=/path/in/container \
nginx:alpine

绑定宿主目录

1
2
3
4
docker service create \
--name my-service \
--mount type=bind,source=/path/on/host,destination=/path/in/container \
nginx:alpine

参考官网文档:https://docs.docker.com/engine/reference/commandline/service_create/#/add-bind-mounts-or-volumes

对于两节点集群来说,--replicas=2--mode=global 是不是一个意思? #

首先,二者语义就不同。

  • --replicas=2,是要求该服务有2个副本,无论集群多少个节点,也不在乎这两个副本是不是都跑在一个宿主上,所以无法确保每个节点一个副本;
  • --mode=global,是要求服务在集群每一个节点上跑一个副本。

现象上也不一样,--replicas=2 是要确保副本为2个。那么如果一个节点挂了,会在另一个节点上在起一个副本,从而确保副本数为2。而对于 --mode=global 来说,如果一个节点挂了,不会再另一个节点上起一个副本。

Docker Machine 相关问题 #

如何在 Docker Toolbox 中创建的 default 虚拟机中添加DOCKER_OPTS之类的配置? #

其实在最初创建该docker host时,就可以利用 docker-machine 指定引擎配置参数,如果不要紧,可以直接rm掉这个虚拟机,重新建立。

如果不方便 rm 掉这个虚拟机,可以 docker-machine ssh 进入这个虚拟机,然后修改 /var/lib/boot2docker/profile 文件,修改里面的 EXTRA_ARGS 参数即可。

docker-machine 创建的主机怎么直接 ssh 进去?改了 root 密码好像也没用? #

docker-machine 创建的主机,会遵循安全最佳实践,因此一般不会允许 root 登录,而且一般不会允许密码登录,只允许密钥登录(也就是很多国内文章称为的免密登录,其实并非免密)。

因此,使用密钥 ~/.docker/machine/machines/<机器名>/id_rsa 登录即可。

1
2
3
4
ssh -i ~/.docker/machine/machines/default/id_rsa \
-o UserKnownHostsFile=/dev/null \
-o StrictHostKeyChecking=no \
docker@$(docker-machine ip default)

这个例子中连接的是 default 这个机器,需要连接其它的机器换成别的即可。另外的两个 -o 的参数是让其不要校验服务器密钥,这当然是不安全的,不过这里只是试验的虚拟机,所以没关系。

docker-machine 使用 -d generic 时,指定用户 --generic-ssh-user 后发现要 sudo 密码,结果报错退出,这是怎么回事? #

你应该再仔细看看 generic 的官方文档:https://docs.docker.com/machine/drivers/generic/#/sudo-privileges

里面说的很清楚,默认用户是 root,但如果通过 --generic-ssh-user 指定其它用户的话,该用户必须拥有无密码sudo的能力,换句话说,就是在 sudoers 文件中对该用户配置 NOPASSWD

Docker Registry 相关问题 #

docker push 的时候怎么报 authentication required 错误? #

因为你没有登录。如果是向 Docker Hub 推送镜像,需要在注册一个用户: https://hub.docker.com/

我注册用户 aaa 了,怎么还是无法 docker push bbb/xxx 啊? #

😒……因为你 push 到别人的 repo 了,你只能 pushaaa/xxx 下。

不管用啊,我这回 docker push aaa/xxx 了,怎么告诉我不存在啊? #

😓……因为你没有 tag 对应的镜像为 aaa/xxx

所有这些问题,都是由于你没有去看文档,建议不要这么一次次的瞎撞,去看官网文档:

https://docs.docker.com/engine/getstarted/step_six/
https://docs.docker.com/engine/tutorials/dockerrepos/

docker push 到私有 registry 总是不成功,怎么办? #

如果在报错中看到了 https,那很可能是因为 registry 没有配置证书。

很多人最开始配置 registry 的时候,为了简单而没有配置 TLS 证书。

这是不安全的做法,在 Docker 中不推荐使用。因此,刻意的增加了使用这种不安全 registry 的复杂度。使用者必须在 docker daemon 配置中,明确声明要使用这些不安全的 registry

比如,在 Ubuntu 16.04 中,编辑 /etc/systemd/system/multi-user.target.wants/docker.service 中的 ExecStart= 的结尾,加入 --insecure-registry=192.168.99.100:5000,将 192.168.99.100:5000 替换成你的 registry 地址。如果有很多 registry,可以设置多组。或者如果虚拟机的 IP 总是变化,也可以使用 CIDR 的形式,比如 --insecure-registry=192.168.99.0/24

不过测试过后,一定要配上 TLS 证书。现在 Let’s Encrpyt 已经支持 DNS 认证了,不需要暴露内部的机器于公网,用其脚本自动取得免费证书是很方便的。

docker push 了很多镜像到私有的 registry 上,怎么才能查看上面都有啥?或者搜索? #

两种办法,一种是使用 Registry V2 API。可以列出所有镜像:

1
curl http://<私有registry地址>/v2/_catalog

如果私有 Registry 尚支持 V1 API(已经废弃),可以使用 docker search

1
docker search <私有registry地址>/<关键字>

如何删除私有 registry 中的镜像? #

首先,在默认情况下,docker registry 是不允许删除镜像的,需要在配置config.yml中启用:

1
2
delete:
enabled: true

然后,使用 API GET /v2/<镜像名>/manifests/<tag> 来取得要删除的镜像:Tag所对应的 digest

Registry 2.3 以后,必须加入头 Accept: application/vnd.docker.distribution.manifest.v2+json,否则取到的 digest 是错误的,这是为了防止误删除。

比如,要删除 myimage:latest 镜像,那么取得 digest 的命令是:

1
2
3
4
$ curl --header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
-I -X HEAD http://192.168.99.100:5000/v2/myimage/manifests/latest \
| grep Digest
Docker-Content-Digest: sha256:3a07b4e06c73b2e3924008270c7f3c3c6e3f70d4dbb814ad8bff2697123ca33c

然后调用 API DELETE /v2/<镜像名>/manifests/<digest> 来删除镜像。比如:

1
curl -X DELETE http://192.168.99.100:5000/v2/myimage/manifests/sha256:3a07b4e06c73b2e3924008270c7f3c3c6e3f70d4dbb814ad8bff2697123ca33c

至此,镜像已从 registry 中标记删除,外界访问 pull 不到了。但是 registry 的本地空间并未释放,需要等待垃圾收集才会释放。而垃圾收集不可以在线进行,必须停止 registry,然后执行。比如,假设 registry 是用 Compose 运行的,那么下面命令用来垃圾收集:

1
2
3
docker-compose stop
docker run -it --name gc --rm --volumes-from registry_registry_1 registry:2 garbage-collect /etc/registry/config.yml
docker-compose start

其中 registry_registry_1 可以替换为实际的 registry 的容器名,而 /etc/registry/config.yml 则替换为实际的 registry 配置文件路径。

参考官网文档:

https://docs.docker.com/registry/configuration/#/delete

https://docs.docker.com/registry/spec/api/#/deleting-an-image

使用国内镜像还是慢,公司内好多 docker 主机,都需要去重复下载镜像,咋办? #

在局域网内,本地架设个 Docker Registry mirror,作为缓存即可。

建立一个空目录,并且添加 Registry 的配置文件 config.yml,其内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
version: 0.1
log:
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
proxy:
remoteurl: https://registry-1.docker.io

并且,建立个 docker-compose.yml 文件方便启动这个服务:

1
2
3
4
5
6
7
8
version: '2'
services:
mirror:
image: registry:2
ports:
- "5000:5000"
volumes:
- ./config.yml:/etc/docker/registry/

然后用 Docker Compose 启动这个镜像服务:docker-compose up -d

然后在局域网中的所有 Docker 主机中的 Docker Daemon 配置中,都添加一条 --registry-mirror=<这个镜像服务器的地址>

首先用docker pull下载一个本地不存在的镜像,看一下时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ time docker pull php:7-fpm-alpine
7-fpm-alpine: Pulling from library/php
e110a4a17941: Pull complete
d9f63633faf6: Pull complete
ac309a5bc5d5: Pull complete
4523ec888a62: Pull complete
6a77f79ab9b5: Pull complete
27243562b67c: Pull complete
33e1803456c2: Pull complete
a1219b0a1418: Pull complete
Digest: sha256:f7d6f6844df64f8f615fa50ca28b3f1ad82be0a2dcde0b55205d31c1bb9f4820
Status: Downloaded newer image for php:7-fpm-alpine
docker pull php:7-fpm-alpine 0.07s user 0.07s system 0% cpu 2:30.43 total

上面我们下载了 php:7-fpm-alpine,用时 2 分 30秒,然后我们删掉镜像:

1
2
3
4
5
$ docker rmi php:7-fpm-alpine
Untagged: php:7-fpm-alpine
Deleted: sha256:b80ca1f4f99d13e00ac6ef13aca7c1ef6e2fb83ec2fe6a035e8beeeb05afb4b6
Deleted: sha256:69ee0f31988504dc3e3b068476f11d06b43fc34465a1c58d351406b9d2368e7a
...

然后重新下载镜像,测试时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ time docker pull php:7-fpm-alpine
7-fpm-alpine: Pulling from library/php
e110a4a17941: Pull complete
d9f63633faf6: Pull complete
ac309a5bc5d5: Pull complete
4523ec888a62: Pull complete
6a77f79ab9b5: Pull complete
27243562b67c: Pull complete
33e1803456c2: Pull complete
a1219b0a1418: Pull complete
Digest: sha256:f7d6f6844df64f8f615fa50ca28b3f1ad82be0a2dcde0b55205d31c1bb9f4820
Status: Downloaded newer image for php:7-fpm-alpine
docker pull php:7-fpm-alpine 0.05s user 0.04s system 0% cpu 13.778 total

这次由于该 docker image 本地 mirror 缓存了,所以用时约14秒,速度大大提高了。

参考官网文档:

服务端:https://docs.docker.com/registry/configuration/#/proxy

客户端:https://docs.docker.com/engine/reference/commandline/dockerd/

自己架的 registry 怎么任何用户都可以取到镜像?这不安全啊? #

那是因为没有加认证,不加认证的意思就是允许任何人访问的。

添加认证有两种方式:

1
2
3
4
5
6
7
8
9
auth:
token:
realm: token-realm
service: token-service
issuer: registry-token-issuer
rootcertbundle: /root/certs/bundle
htpasswd:
realm: basic-realm
path: /path/to/htpasswd
1
2
3
4
5
6
location /v2/ {
...
auth_basic "Registry realm";
auth_basic_user_file /etc/nginx/conf.d/nginx.htpasswd;
...
}

CentOS/RHEL 红帽系统特有问题 #

在 CentOS 6 上安装后怎么最高只有 Docker 1.7 这个版本? #

Docker 已经不再支持 CentOS 6 了,现在看到的是很久以前的老版本,之后再也没有发布过 CentOS 6 的版本。

所以不要再在 CentOS 6上用 Docker 了。换 CentOS 7 或者 Ubuntu 吧。

挂载宿主目录,结果 Permission denied,没权限 #

原因是 CentOS/RHEL中的 SELinux 限制了目录权限。需要添加规则。

下面是 man docker-run 的解释:

When using SELinux, be aware that the host has no knowledge of container
SELinux policy. Therefore, in the above example, if SELinux policy is enforced,
the /var/db directory is not writable to the container. A “Permission Denied”
message will occur and an avc: message in the host’s syslog.

To work around this, at time of writing this man page, the following command
needs to be run in order for the proper SELinux policy type label to be
attached to the host directory:

1
chcon -Rt svirt_sandbox_file_t /var/db

参考:http://www.projectatomic.io/blog/2015/06/using-volumes-with-docker-can-cause-problems-with-selinux/

Docker的 /var/lib/docker/devicemapper 占用空间不断增长, 怎么破? #

这类问题一般是 CentOS/RHEL 红帽系的问题,CentOS 这类红帽系统中,由于不像 Ubuntu 那样有成熟的 Union FS实现(如aufs),所以只能使用 devicemapper,而默认使用的是lvm-loop,也就是用一个稀疏文件来当成一个块设备,给devicemapper用,作为Docker镜像容器文件系统。这是非常不推荐使用的,性能很差不说,不稳定,还有很多bug,如果没办法换Ubuntu/Debian系统,那么最起码应该建立块设备(分区、卷)给 devicemapper用。

参考官网文档:https://docs.docker.com/engine/userguide/storagedriver/device-mapper-driver/#configure-direct-lvm-mode-for-production

严格来说 CentOS/RHEL 7 中实际上有一个 Union FS 实现,虽然 CentOS/RHEL 7 的内核是3.10,不过红帽从 Linux 3.18 backport 回来了 overlay fs 的驱动。但是,红帽自己都在官方的发布声明中说能不要用就不用。

https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/7.2_Release_Notes/technology-preview-file_systems.html

CentOS 7 的内核太老了 3.10,是不是很多Docker功能不支持? #

之前我也是这样以为的,毕竟很多内核功能需要更高的版本。比如 Overlay FS 需要Linux 3.18,而Overlay network需要Linux 3.16。而CentOS 7内核为3.10,按理说不会支持这些高级特性。

但是,事实并非如此,红帽团队会把一些新内核的功能backport回老的内核。比如 overlay fs等。所以一些功能依旧会支持。因此 CentOS 7的Docker Engine同样可以支持 overlay network,以及 overlay fs。因此在新的 Docker 1.12 中,CentOS/RHEL 7 才有可能支持 Swarm Mode。

即使红帽会把一些高版本内核的功能backport回 3.10 内核中,这种修修补补出来的功能,并不一定很稳定,所以依旧建议使用其它维护内核版本升级的Linux发行版,如 Ubuntu。

CentOS 7/RHEL 7 升级 1.12 后,无法启动,报错 Unit docker.socket failed to load: No such file or directory. #

其原因是由于从 1.12 开始,不需要在 systemd 中写个 docker.socket 文件了,所以这个文件就随升级而删除了。而 docker.service 由于被修改过(或别的什么原因),导致 yum 升级的时候没有替换这个文件。于是出现了旧的 docker.service 中配置要求有 docker.socket 文件,而这个文件已经在新的版本中删除了,所以导致启动错误。

解决办法很简单,直接打开 docker.service,将其参照 1.12 的默认配置文件修改即可。寻找到 Required=docker.socket 那行,删掉。然后寻找到 docker daemon 那行,将其后的 -H fd:// 删掉。如果愿意,可以进一步将 docker daemon 改为 dockerd,因为从 1.12 开始改名叫这个了。保存退出重启服务即可。

Mac / Windows 相关问题 #

为什么在Mac下挂载宿主目录/usr/local/nginx不成功? #

虽然 Docker 团队尽量让使用 Docker Toolbox, Docker for Mac and Docker fo Windows 的用户感觉操作 Docker 就像在 Linux 下一样,但实际上在 Mac/Windows 上并非是直接运行 Docker 的。中间经过了一个 Linux 虚拟机,而 Docker 运行在那个虚拟机里。

因此 Mac 主机上的目录实际上并不是 Docker 眼中的宿主目录,为了让用户尽量感觉不到这个差异,Boot2Docker 或者 Docker for Mac / Windows 中,将一部分物理主机的目录映射到了 Linux 虚拟机中,这样其上 Docker 就可以访问到这些物理机的目录了。

出于安全考虑,并不会把物理机的所有目录都映射到 Linux 虚拟机内。一般来说只有当前用户目录在内的一些目录会被映射到 Linux 虚拟机内,比如 /Users, /Volumes 等。

对于 Docker for Mac 的用户,可以直接在配置界面 File Sharing 中添加额外的映射目录,但是,出于安全考虑,不添加额外映射,而使用当前用户目录下的目录,是更好地做法。

伟大的墙相关问题 #

Docker Toolbox/Compose/Machine 总是下载不来怎么办? #

首先感谢伟大的墙,然后翻墙下载。

对于部分常用的软件,我上传了一份到百度云上,鉴于百度云这山寨货文件完整性无法保证,我zip压缩了不同系统的工具。所以较大,慢慢下载。

链接:https://pan.baidu.com/s/1o7V5scM 密码:tzxc

  • Linux: linux-1.12.0.zip
  • Mac: mac-1.12.0.zip
  • Windows: windows-1.12.0.zip

ZIP 文件内包含了 md5sum.txt 以及 sha256sum.txt,下载后可以用其确认文件完整性。

是直接用 yum / apt-get 安装 Docker 吗? #

无论是CentOS还是Ubuntu,都不要使用系统源里面的Docker,版本太古老,没法用。

官方正式的安装Docker方法:

1
curl -fsSL https://get.docker.com/ | sh

如果访问官方源太慢,可以使用国内的源安装:

使用阿里云的安装脚本:

1
curl -sSL http://acs-public-mirror.oss-cn-hangzhou.aliyuncs.com/docker-engine/internet | sh -

使用DaoCloud的Docker安装脚本:

1
curl -sSL https://get.daocloud.io/docker | sh

docker pull 好慢啊怎么办? #

要感恩伟大的墙。使用阿里云加速器或者DaoCloud的加速器(也就是代理、镜像)吧:

要申请阿里云加速器地址,注册阿里云开发账户(免费的)后,访问这个链接就可以看到加速器地址: https://cr.console.aliyun.com/#/accelerator

要申请 DaoCloud加速器地址,注册 DaoCloud 账户(支持微信登录),然后访问: https://www.daocloud.io/mirror#accelerator-doc
DaoCloud 还提供了一个脚本安装加速器:比如:

1
curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://ef2bef07.m.daocloud.io

参考博文:

http://www.imike.me/2016/04/20/Docker%E4%B8%8B%E4%BD%BF%E7%94%A8%E9%95%9C%E5%83%8F%E5%8A%A0%E9%80%9F/

Ubuntu 14.04 配置 #

对于使用 Upstart 的系统(如 Ubuntu 14.04),可以用下类方法配置加速器。

1
echo "DOCKER_OPTS=\"\$DOCKER_OPTS –registry-mirror=http://your-id.m.daocloud.io -d\"" >> /etc/default/docker

Ubuntu 16.04 配置 #

编辑 systemd 的服务配置文件 docker.service

1
sudo vi /etc/systemd/system/multi-user.target.wants/docker.service

ExecStart 中的行尾添加上所需的配置,如:

1
ExecStart=/usr/bin/docker daemon -H fd:// --registry-mirror=https://jxus37ac.mirror.aliyuncs.com

注: Docker 1.12 之后的版本,docker daemon 改为 dockerd

保存退出后,重新加载配置并启动服务:

1
2
sudo systemctl daemon-reload
sudo systemctl restart docker

确认一下配置是否已经生效:

1
sudo ps -ef | grep docker

生效后这里会看到自己的配置。

装完 Docker Toolbox 后发现下载镜像速度太慢,是不是需要修改什么配置文件? #

安装 Docker Toolbox 时,安装程序会使用 docker-machine 为你创建一个默认的虚拟机:

1
docker-machine create -d virtualbox default

这个虚拟机没有加任何参数,因此对于拥有伟大的墙的国内网络来说,有些不方便使用。所以最简单的做法是在安装完 Docker Toolbox 后,删掉默认的虚拟机,然后重新创建该虚拟机,创建时加入有中国特色的配置。

1
2
3
4
5
6
7
docker-machine rm default
docker-machine create -d virtualbox \
--engine-registry-mirror https://jxus37ac.mirror.aliyuncs.com \
--engine-insecure-registry 192.168.99.0/24 \
--engine-storage-driver overlay2 \
default

删除 default 虚拟机的时候要注意,其中镜像、容器等内容都会被删除。

其它问题 #

Docker 资料好少啊?网上的命令怎么不能用? #

首先,做技术工作,请珍惜生命,远离百度;
其次,不翻墙、不用Google、不看英文资料,那请转行,没法混。

然后是回答问题,Docker的资料其实很丰富,特别是官方文档讲解非常详细。

https://docs.docker.com/

另外,Docker有丰富的镜像库,Docker Hub,特别是官方(Official)的镜像可以直接在生产环境中使用,制作比较精良。

https://hub.docker.com/explore/

所有的官方镜像都有 Dockerfile,以及在github上有全部生成镜像的配套文件,遵循了Dockerfile的最佳实践,这些也是很好地学习资料。

另外,在 YouTube 的 Docker 官方频道下有几百个视频讲座,从初级到高级用户都能从里面学到很多东西。

https://www.youtube.com/user/dockerrun

Docker 1.8以后版本都有什么改进么? #

每个版本发布时,官方博客 https://blog.docker.com 都会有专门文章描述这个版本最主要的改进。

Kubernetes 这词咋念啊? #

Kubernetes 的发音:koo-ber-nay'-tace ,这词来自希腊舵手这个词。
但是经常有人念成:koo-ber-net-ees,或者k8sk-eights

http://www.biblestudytools.com/lexicons/greek/nas/kubernetes.html

问一句 Kubernetes 为啥叫 “k8s”? #

是因为发音接近么……好吧,实话说了吧,是因为犯懒,数数 k 和 s 中间多少个字母?8个吧,这个8 的意思就是省略8个字母,懒得敲了…… 其实这类用法很多,比如 i18n (internationalization), l11n (localization) 等等,老外也懒得打字啊。

我的配置文件传群文件了,帮忙看看?这是我的配置…(省略几千字) #

配置文件这类文本东西,应该用这类剪贴板网站,不要使用群文件或者直接大段刷屏,格式混乱而且没有语法高亮: