目录

隐藏
  1. 为什么容器一运行就退出啊?
  2. 都说不要用 root 去运行服务,但我看到的 Dockerfile 都是用 root 去运行,这不安全吧?
  3. Docker 容器如何随系统一同启动?
  4. 容器内的时间和宿主不一致,怎么同步啊?
  5. 我在容器里运行 systemctl start xxx 怎么报错啊?
  6. 如何在 Docker 容器内使用 docker 命令(比如在 Jenkins 容器中)?
  7. 经常在各种 Docker 命令里看到 --label,label 是什么?干什么用的?
  8. 如何动态修改内存限制?
  9. 我想让我的程序平滑退出,为什么截获 SIGTERM 信号不管用啊?
  10. 我用的是阿里云 Ubuntu 14.04 主机,内核还是3.13,怎么办?
为什么容器一运行就退出啊?
这是初学 Docker 常常碰到的问题,此时还以虚拟机来理解 Docker,认为启动 Docker 就是启动虚拟机,也没有搞明白前台和后台的区别。

首先,碰到这类问题应该查日志和容器主进程退出码。

检查容器日志:

docker logs <容器ID>
查看容器退出码:

CONTAINER ID        IMAGE                           COMMAND             CREATED             STATUS                      PORTS                                                                  NAMES
cc2aa3f4745f        ubuntu                          "/bin/bash"         23 hours ago        Exited (0) 22 hours ago                                                                            clever_lewin
25510a2cb171        twang2218/gitlab-ce-zh:8.15.3   "/assets/wrapper"   2 days ago          Exited (127) 2 days ago                                                                            determined_mirzakhani
在 STATUS 一栏中,可以看到退出码是多少。

如果看到了 Exited (127) 那很可能是由于内存超标导致触发 Out Of Memory 然后被强制终止了。
如果看到了 Exited (0),这说明容器主进程正常退出了。
如果是其他情况,应该检查容器日志。
初学 Docker 的人常常会不理解既然正常怎么会退出的意思。不得不在强调一遍,Docker 不是虚拟机,容器只是进程。因此当执行 docker run 的时候,实际所做的只是启动一个进程,如果进程退出了,那么容器自然就终止了。

那么进程为什么会退出?

如果是执行 service nginx start 这类启动后台服务程序的命令,那说明还是把 Docker 当做虚拟机了。Docker 启动的是进程,因此所谓的后台服务应该放到前台,比如应该 nginx -g 'daemon off;' 这样直接前台启动应用才对。
如果发现 COMMAND 一栏是 /bin/bash,那还是说明把 Docker 当虚拟机了。COMMAND 应该是应用程序,而不交互式操作界面,容器不需要交互式操作界面。此外,如果使用 /bin/bash 希望起一个交互式的界面,那么也必须提供给其输入和终端,因此必须加 -it 选项,比如 docker run -it ubuntu /bin/bash

都说不要用 root 去运行服务,但我看到的 Dockerfile 都是用 root 去运行,这不安全吧?
并非所有官方镜像的 Dockerfile 都是用 root 用户去执行的。比如 mysql 镜像的执行身份就是 mysql 用户;redis 镜像的服务运行用户就是 redis;mongo 镜像内的服务执行身份是 mongo 用户;jenkins 镜像内是 jenkins 用户启动服务等等。所以说 “都是用 root 去运行” 是不客观的。

当然,这并不是说在容器内使用 root 就非常危险。容器内的 root 和宿主上的 root 不同,容器内的 root 虽然 uid 也默认为 0,但是却处于一个隔离的命名空间,而且被去掉了大量的特权。容器内的 root 是一个没有什么特权的用户,危险的操作基本都无法执行。

不过,如果用户可以打破这个安全保护,那就是另外一回事了。比如,如果用户挂载了宿主目录给容器,这就是打通了一个容器内的 root 操控宿主的一个通道,使得容器内的 root 可以修改所挂载的目录下的任何文件。

因为当前版本的 Docker 中,默认情况下容器的 user namespace 并未开启,所以容器内的用户和宿主用户共享 uid 空间。容器内的 uid 为 0 的 root,就被系统视为 uid=0 的宿主 root,因此磁盘读写时,具有宿主 root 同等读写权限。这也是为什么一般不推荐挂载宿主目录、特别是挂载宿主系统目录的原因之一。这一切只要定制镜像的时候,容器内不使用 root 启动服务就没这个问题了。

当然,上面说的问题只是默认情况下 user namespace 不会启用的问题。dockerd 有一个 --userns-remap 参数,只要配置了这个参数,就可以确保容器内的 uid 是独立命名空间,容器内的 uid 变到宿主的时候,会被 remap 到另一个范围。因此,容器内的 uid=0 的 root 将完全跟 root 没有任何关系,仅仅是个普通用户而已。

相关信息请参考官方文档:

--userns-remap 的介绍: https://docs.docker.com/engine/reference/commandline/dockerd/#/daemon-user-namespace-options
Docker 安全: https://docs.docker.com/engine/security/security/

Docker 容器如何随系统一同启动?
--restart=always
参考官网文档: https://docs.docker.com/engine/reference/commandline/run/#restart-policies-restart

容器内的时间和宿主不一致,怎么同步啊?
问这个问题的人往往混淆了时间和时区的概念。

时间是从 epoch 到当前的秒数或者毫秒数,全球都一样,这是绝对值;而时区则是由于地理位置差异、行政区划导致各地显示时间的差异。

对于 Docker 容器而言,根本不存在宿主和容器的时间差异问题,因为他们使用的是同一个内核、同一个时钟,二者完全一样,所以根本不存在同步问题。还是那句话 Docker 不是虚拟机。

所看到的差异,如果细心一点,很可能会发现其实根本不是时间同步问题:

$ docker run -it ubuntu bash
root@08c6ad41f343:/# date
Tue Dec 13 01:36:37 UTC 2016
注意到 UTC 了么,这是说使用的是国际标准 0 时区 的时间显示,因此这只是显示所用的时区设置差异问题。而且之前如果稍微注意一下,就会发现所谓时间不一致,实际上是整整差了 8 个小时,还记得中学地理课上讲的中国时区是多少么?是 +8 时区,所以自然和 0 时区 差了 8 个小时。应该很快就意识到是自己的时区设错了(或者偷懒没设)导致。

解决办法很简单,设置时区即可。一般情况直接设置环境变量 TZ 就够了,比如:

$ docker run -it -e TZ=Asia/Shanghai debian bash
root@8e6d6c588328:/# date
Tue Dec 13 09:41:21 CST 2016
看到了么?时区调整到了 CST,也就是China Standard Time - 中国标准时间,因此显示就正常了。

不过并非所有系统都可以如此方便的设置时区。可以直接使用 TZ=Asia/Shanghai 环境变量修改时区的系统有:

centos (5, 6, 7)
debian (7, 8, 9)
fedora (24, 25, 26)
ubuntu (14.04)
而下面的这些系统可能出于镜像体积的考虑,去掉了时区的软件包 tzdata,因此需要在 Dockerfile 中先行安装时区包。

ubuntu: (16.04, 17.04, 17.10) (~15MB)
Dockerfile:
RUN set -xe \
&& apt-get update \
&& apt-get install tzdata locales \
&& rm -rf /var/lib/apt/lists/*
alpine (~1.3MB)
Dockerfile: RUN apk --no-cache add tzdata
opensuse (~12MB)
Dockerfile:
RUN set -xe \
&& zypper --non-interactive refresh \
&& zypper --non-interactive -qn install --no-recommends timezone \
&& zypper --non-interactive clean -a
clearlinux (~280MB)
Dockerfile:
RUN set -xe \
&& swupd bundle-add sysadmin-basic \
&& rm -rf /var/lib/swupd/*
上面列表除了列出系统外,还给出了每个系统需要添加到 Dockerfile 的安装包的命令,以及安装后镜像体积增加的大小。其中 clearlinux 不能单个安装软件包,所以体积增加的有些夸张,因此更好地办法是直接 COPY 时区信息进镜像。

注意:ubuntu:16.04 以后的版本,在 2017年4月10 日以后,已经去除 tzdata,因此要改变其时区需要进行时区安装操作,而不是像以前那样只需配置 TZ 环境变量即可。不过大部分官方镜像是基于 debian 的,因此它们不受影响。

参考 issue:

https://bugs.launchpad.net/cloud-images/+bug/1682622
https://bugs.launchpad.net/cloud-images/+bug/1682305
https://github.com/docker-library/official-images/issues/2863
https://github.com/docker-library/official-images/issues/2856
这仅仅是调整容器内系统环境的时区,大部分程序都会遵循这个标准。但是有些应用并不遵守这类约定,会使用自己的时区设置。

一般应用、服务的配置文件里一般都有时区选项,应该根据自己需求把中国时区配上。

比如,PHP 配置文件中的:

[Date]
date.timezone = Asia/Shanghai
再比如 mysqld 中的参数 --timezone=Asia/Shanghai;Java 的 -Duser.timezone=Asia/Shanghai JVM 参数,都可以指定上层应用时区,而不依赖于系统默认时区,这也是推荐的做法。避免系统部署时受系统时区影响,这在全球云服务器环境中其实很常见,因此尽量在应用层设置好。很多应用都有自己的时区设置,应该去了解一下并且进行设置,不要总用默认值。

一些人在配置服务的时候很懒惰,只要默认能用即可,而不会一一检查每一个配置的默认值是否和自己期望一致,这是很不专业的做法,正是这种不专业才导致了出现了这种问题。所以做事情,一定要让自己以专业的视角和态度看问题。

我在容器里运行 systemctl start xxx 怎么报错啊?
如果在容器内使用 systemctl 命令,经常会发现碰到这样的错误:

Failed to get D-Bus connection: Operation not permitted
这很正常,因为 systemd 是完整系统的服务启动、维护的系统服务程序,而且需要特权去执行。但是容器不是完整系统,既没有配合的服务,也没有特权,所以自然用不了。

如果你碰到这样的问题,只能再次提醒你,Docker 不是虚拟机。试图在容器里执行 systemctl 命令的,大多都是还没有搞明白容器和虚拟机的区别,因为看到了可以有 Shell,就以为这是个虚拟机,试图重复自己在完整系统上的体验。这是用法错误,不要把 Docker 当做虚拟机去用,容器有自己的用法。

Docker 不是虚拟机,容器只是受限进程。

容器内根本不需要后台服务,也不需要服务调度和维护,自然也不需要 systemd。容器只有一个主进程,也就是应用进程。容器的生存周期就是围绕着这个主进程而存在的,所以所试图启动的后台服务,应该改为直接在前台运行,根本不需要也不应该使用 systemctl 命令去在后台加载。日志之类的也是直接从 stdout/stderr 输出,而不是走 journald。

如何在 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 命令),也可以是 其它语言的库

为 Jenkins 添加 Docker 命令行

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

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):

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

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

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

$ docker run --name jenkins \
-d \
-p 8080:8080 \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkins-docker
在 jenkins 容器中,就已经可以执行 docker 命令了,可以通过 docker exec 来验证这个结果:

$ 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
/ $

经常在各种 Docker 命令里看到 --label,label 是什么?干什么用的?
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
在后期维护时,可以直接过滤显示想要的容器,比如我们只想看前端容器运行情况:

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

docker stop $(docker ps -f label=type=frontend)
使用 label 在集群调度中也非常有用。

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

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

docker service create \
--name mysql \
--constraint 'engine.labels.storage == ssd' \
mysql
添加label以及过滤

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

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

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

Docker 引擎
添加:dockerd: https://docs.docker.com/engine/reference/commandline/dockerd/
镜像
添加:
docker build: https://docs.docker.com/engine/reference/commandline/build/
Dockerfile 中的 LABEL(会继承FROM镜像的LABEL): https://docs.docker.com/engine/reference/builder/#/label
过滤:docker images: https://docs.docker.com/engine/reference/commandline/images/#/filtering
容器
添加:docker create: https://docs.docker.com/engine/reference/commandline/create/
除了 --label 外,docker create 还支持使用选项 --label-file 从文件中加载 label
添加:docker run: https://docs.docker.com/engine/reference/commandline/run/#/set-metadata-on-container--l---label---label-file
除了 --label 外,docker run 还支持使用选项 --label-file 从文件中加载 label
过滤:docker ps: https://docs.docker.com/engine/reference/commandline/ps/#/label

添加:docker volume create: https://docs.docker.com/engine/reference/commandline/volume_create/
过滤:docker volume ls: https://docs.docker.com/engine/reference/commandline/volume_ls/#/filtering
网络
添加:docker network create: https://docs.docker.com/engine/reference/commandline/network_create/
过滤:docker network ls: https://docs.docker.com/engine/reference/commandline/network_ls/#/filtering
Swarm 节点
docker node update: https://docs.docker.com/engine/reference/commandline/node_update/#/add-label-metadata-to-a-node
添加:--label-add
删除:--label-rm
过滤:docker node ls: https://docs.docker.com/engine/reference/commandline/node_ls/#/filtering
过滤:docker node ps: https://docs.docker.com/engine/reference/commandline/node_ps/#/label
服务
添加:docker service create: https://docs.docker.com/engine/reference/commandline/service_create/#/set-metadata-on-a-service--l---label
除了 --label 外,还可以通过 --container-label 来添加容器 label
docker service update: https://docs.docker.com/engine/reference/commandline/service_update/
添加容器 label:--container-label-add
删除容器 label:--container-label-rm
添加服务 label:--label-add
删除服务 label:--label-rm
过滤:docker service ls: https://docs.docker.com/engine/reference/commandline/service_ls/#/label
除了上述资源外,docker events 也可以使用 label 过滤结果: https://docs.docker.com/engine/reference/commandline/events/

集群调度约束

一代 Swarm:使用环境变量添加约束
docker run:-e constraint:storage==sdd: https://docs.docker.com/swarm/scheduler/filter/#/how-to-write-filter-expressions
docker-compose.yml:使用 environment 来进行约束: https://docs.docker.com/compose/swarm/#/manual-scheduling
如:

version: "2"
services:
redis:
image: redis
environment:
- "constraint:storage==ssd"
二代 Swarm
docker service create:--constraint value: https://docs.docker.com/engine/reference/commandline/service_create/#/specify-service-constraints---constraint
如下面的例子中,使用 Swarm 节点 的 label 进行约束(注意,这次用的不是引擎的label):

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

如何动态修改内存限制?
Docker 1.10 之后支持动态修改,使用 docker update 命令,如:

docker update -m 300m


我想让我的程序平滑退出,为什么截获 SIGTERM 信号不管用啊?
docker stop, docker service rm 在停止容器时,都会先发 SIGTERM 信号,等待一段时间(默认为 10 秒)后,如果程序没响应,则强行 SIGKILL 杀掉进程。

这样应用进程就有机会平滑退出,在接收到 SIGTERM 后,可以去 Flush 缓存、完成文件读写、关闭数据库连接、释放文件资源、释放锁等等,然后再退出。所以试图截获 SIGTERM 信号的做法是对的。

但是,可能在截获 SIGTERM 时却发现,却发现应用并没有收到 SIGTERM,于是盲目的认为 Docker 不支持平滑退出,其实并非如此。

还记得我们提到过,Docker 不是虚拟机,容器只是受限进程,而一个容器只应该跑一个主进程的说法么?如果你发现你的程序没有截获到 SIGTERM,那就很可能你没有遵循这个最佳实践的做法。因为 SIGTERM 只会发给主进程,也就是容器内 PID 为 1 的进程。

至于说主进程启动的那些子进程,完全看主进程是否愿意转发 SIGTERM 给子进程了。所以那些把 Docker 当做虚拟机用的,主进程跑了个 bash,然后 exec 进去启动程序的,或者来个 & 让程序跑后台的情况,应用进程必然无法收到 SIGTERM。

还有一种可能是在 Dockerfile 中的 CMD 那行用的是 shell 格式写的命令,而不是 exec 格式。还记得前面提到过的 shell 格式的命令,会加一个 sh -c 来去执行么?因此使用 shell 格式写 CMD 的时候,PID 为 1 的进程是 sh,而它不转发信号,所以主程序收不到。

明白了道理,解决方法就很简单,换成 exec 格式,并且将主进程执行文件放在第一位即可。这也是为什么之前推荐 exec 格式的原因之一。

我用的是阿里云 Ubuntu 14.04 主机,内核还是3.13,怎么办?
其实 Ubuntu 14.04 官方维护的内核已经到 4.4 了,可以通过下面的命令升级内核:

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