用户变量

用户变量与数据库连接有关, 这某个连接中声明变量, 在断开连接时这个变量就会消失, 且在这个连接中声明的变量无法在另一个连接中使用.

1
2
3
# 用户变量语法

set @varName=value

声明一个变量 varName, 并将它赋值为 value

mysql 中的变量是动态数据类型, 它的数据类型会根据它的值的类型而变化

系统变量

系统变量中, 又分为 全局变量会话变量

全局变量与会话变量的区别在于, 对全局变量的修改会影响到整个MySQL服务 (所有会话) 而对局部变量的修改只会影响到当前会话的连接

利用select语句我们可以查询单个会话变量或者全局变量的值:

1
2
3
select @@session.sort_buffer_size
select @@global.sort_buffer_size
select @@global.tmpdir

凡是上面提到的session,都可以用local这个关键字来代替。

比如: select @@local.sort_buffer_size

local 是 session的近义词

1
2
3
4
5
6
7
无论是在设置系统变量还是查询系统变量值的时候,只要没有指定到底是全局变量还是会话变量, 都当做会话变量来处理。

比如:
set @@sort_buffer_size = 50000;
select @@sort_buffer_size;

上面都没有指定是GLOBAL还是SESSION,所以全部当做SESSION处理。

全局变量

全局变量在 MySQL 启动的时候由服务器自动初始化他们的值, 这些默认的值可以在 /etc/my.cnf 中修改

查看全局变量

show global variables;

修改全局变量

set global varname = value;set @@global.varname = value;

会话变量

会话变量在每一个数据库连接建立后, 由 MySQL 来初始化. MySQL 会将当前所有的全局变量都复制一份作为会话变量

查看会话变量

show session variables;show variables;

修改会话变量

set session varname = value;set @@session.varname = value;

只读变量

有些系统变量的值是可以利用语句来动态进行更改的,但是有些系统变量的值却是只读的

docker 集群模式的落地方案尝试

最近一直在探索 docker 的集群模式在公司实际环境中的落地方案. 公司现有docker 的使用方式为单机, 且在日志收集, 网络模式, 存储引擎等方面的配置和用法比较分散, 尤其是现有使用的 devicemapper 存储引擎模式, 官方明确表示不适合生产环境中使用.

docker 的集群方案目前比较火热的有三种, 一种是 docker 在1.12版本中集成的 swarmkit, 一种是 Google 开源的 kubernetes, 还有一种是 Apache 开源的 mesos. 虽然 kubernetes 是被认为是轻量级的 PaaS 平台, 但如果考虑在公司落地, kubernetes 对我们来说仍然是”重量级”的产品, 考虑到公司现有基于的架构(IaaS), 最终还是选择了 docker 自家的 swarmkit 集群平台进行 PaaS 平台的初步尝试.

swarmkit 的一大好处是安装好 docker 引擎后, 不需要安装任何其他组件, 只需要开启 docker 的 swarm 模式即可. dev 环境现有的 swarmkit 平台由 3 台入口服务器和6台后端应用服务器组成, 其中入口的3台服务器也可以当做应用服务器来部署 docker 应用, 加起来整个平台有9个节点可以分布式的部署 docker 应用.

平台搭建好后, 我首先拿 Apache 服务做测试, 使用 Apache 镜像运行了一个静态页面, 指定后端启动6个容器提供服务, 服务启动后, 可以通过3台入口服务器的任何一个入口进行访问, 请求会被打到后端的 Apache 容器中. 在这个模式下, 我们老雪进行压力测试, 最终在并发1000, 循环100次的量级下, 错误率达到将近50%的情况下结束测试, 寻找瓶颈. 经过排查, 瓶颈不是平台的吞吐量到达上线, 也不是容器处理的请求到达上限, 而是宿主机的日志收集导致宿主机的 IO wait 飙升, 在严苛的测试要求下(500ms的响应时间), 导致大量的请求由于宿主机的 IO wait 而导致响应超时. 我们当初已这样的模式进行测试的目的其实是测试平台的吞吐量, 以及平台网络的稳定性, 没想到这样的量级下,这两点都没有任何问题, 反而是 IO 拖了后腿.

在北京三区生产环境中, 每台 Nginx 网络的瞬时最大吞吐量为30MB左右, 在 swarmkit 平台测试的一个入口的网络瞬时吞吐量也达到了将近20MB, 由于大量的 IO wait, 导致没有测试到入口的实际最大承载量. 但是按照现有三个入口的架构来看, 每个入口20MB 的流量, 加起来也可以承受60MB的吞吐能力, 相当于将近500Mb 带宽的水平, 而三区生产环境的公网 IP 也只有300Mb 的带宽, 在平台稳定性, 以及虚拟网络稳定性及吞吐量上来看, swarmkit 就已经完全符合我们生产环境入口量级的吞吐能力.

接下来的测试中, 上线了用户体系的一个服务, 后端仍旧为6个节点提供服务, 在压力测试中, 基本上所有的错误都来自数据库连接的错误

接下来我们打算上线用户体系第二个服务的时候遇到了问题, 该服务需要向 Zookeeper 注册信息, 而在集群中, 整个集群是一个大型的 VLAN 网络, 每个容器拥有三个 IP, 一个是 docker 桥接给每个容器的 IP, 一个是 VLAN 网络的 IP, 一个是前端用于负载均衡用的 VIP(虚 IP), 当容器内的服务向Zookeeper 注册时, 默认拿的是自己主机上 hosts 中 localhost 对应的 IP 地址, 而且最重要的是, 不管这三个拿哪个 IP 地址, 都只能从内往外连通, 外部主机不可能主动连接进来. 这也就导致了容器内的服务向 Zookeeper 注册时的 IP, 在其他消费者拿到此 IP 的时候, 发现这个 IP 根本就连不上.

对于上面的问题, 我们想了很多解决办法, 我们想过通过路由的方式, 让集群外部主机连通集群内部的网络, 但是这样做的后果是又多增加了一个不稳定因素, 集群外所有需要连进来的流量都会通过我们自己的一台网关服务器做转发, 这相当于我们自己维护了一个虚拟交换机, 增加了两个环境之间交互的稳定性的风险.

还有一个办法是把 docker 集群模式关闭, 当做单机来使用, 但是又发现一个问题, 我们测试的这个服务, 在dubbo 向 Zookeeper 注册时, 默认监听了自己的20880端口(记不清了, 好像是这个) 不管是使用网桥模式映射出去, 还是使用
host 模式让他监听宿主机的端口, 问题都是一个: 这一台 docker 宿主机上, 只能启动一个监听20880的服务, 也就是说, 在发布应用容器的时候, 我们需要人为的去判断, 或是用其他方式去判断目标 docker 主机上是否已经存在与自己端口冲突的服务, 这无疑也是加大了 docker 使用难度, 而且这种模式明显是把 docker 当做虚拟机来使用, 仍然是 IaaS 平台的老思路, 违背了 docker 倡导的使用规则.

由于Zookeeper 的原因, 到目前为止, 对 docker 集群平台(PaaS) 平台的探索将放慢脚步, 也希望在以后的一段时间能找到行之有效的完美解决方案, 如果大家有什么想法, 可以沟通一下, 让我开阔一下眼界.

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
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org

rpm -Uvh https://www.elrepo.org/elrepo-release-7.0-3.el7.elrepo.noarch.rpm

# 查看可以安装的内核版本
yum --disablerepo="*" --enablerepo="elrepo-kernel" list available

# 安装 LTS 版本的内核
yum --enablerepo=elrepo-kernel install kernel-lt -y

# 或者安装最新版本的内核
yum --enablerepo=elrepo-kernel install kernel-ml-devel kernel-ml

# 查看内核列表
awk -F\' '$1=="menuentry " {print i++ " : " $2}' /etc/grub2.cfg
# 新安装的内核会插入在当前使用内核的前面

# 查看当前使用的内核
grub2-editenv list

#修改内核启动顺序,按照上面命令的回显, 指定启动使用第0号内核版本
grub2-set-default 0

reboot

uname -a

参考文档

第一种方式

/etc/sysconfig/modules/ 下创建 *.modules

1
2
3
4
5
> vim /etc/sysconfig/modules/my.modules

modprobe overlay

> chmod 755 /etc/sysconfig/modules/my.modules

第二种方式

/etc/rc.modules 里面加上 modprobe fuse, 没有的话创建该文件

1
2
3
> vim /etc/rc.modules

modprobe overlay

reboot

1
2
➜  ~ lsmod | grep over
overlay 42451 0

第三种方式

1
2
3
echo "overlay" > /etc/modules-load.d/overlay.conf

reboot

通过调整swappiness的值, 可以调整系统使用 swap 的频率

该值越小, 表示越大限度的使用物理内存, 最小值=0

该值越大, 表示越积极的使用 swap 交换分区, 最大值=100

查看 swappiness 值

cat /proc/sys/vm/swappiness

centos 中默认为10, Ubuntu 中默认为60

临时修改 (重启失效)

sysctl vm.swappiness=59

永久修改

echo "vm.swappiness=59" >> /etc/sysctl.conf

  • Docker 管理命令
    • 容器管理
    • 镜像管理
    • 网络管理
    • 系统管理
    • 数据卷管理
    • 快照管理
    • 插件管理
    • 其他
    • swarmkit 管理
      • 模式管理
      • 节点管理
      • 秘钥管理
      • 服务管理
      • 服务栈管理

网络管理 network

Docker 的网络模式繁多, 截止至当前版本(17.04.0-ce), Docker 内置了5种网络引擎供大家使用

  • bridge 默认的网桥+NAT模式
  • host 主机模式(性能损耗最少, 但是有安全性问题, 因为容器内部可以直接修改宿主机网卡配置文件)
  • macvlan 引入之初在于解决本宿主机的容器互联问题(1.13版本之后默认集成进来的网络插件)
  • null 不设置任何网络引擎
  • overlay 跨主机互联, 集群模式下使用较多

create

创建网络一般用于对容器内网 IP 地址段有要求的情况, 一般在集群模式下创建自定义 overlay 网络的情况较多

1
root@ubuntu:~# docker network create --driver overlay my-network

在集群模式中创建 overlay 网络后, 创建的服务可以指定加入到新的网络中

1
root@ubuntu:~# docker service ceate --replicas 3 --network my-network --name my-web nginx

connect/disconnect

将容器加入/离开指定的网络

1
2
docker network connect [OPTIONS] NETWORK CONTAINER
docker network disconnect [OPTIONS] NETWORK CONTAINER

ls/inspect

查看网络配置列表/查看网络详细信息

1
2
docker network ls
docker network inspect [OPTIONS] NETWORK [NETWORK...]

rm/prune

删除指定网络/删除没有引用的网络

1
2
docker network rm NETWORK [NETWORK...]
docker network prune [OPTIONS]

  • Docker 管理命令
    • 容器管理
    • 镜像管理
    • 网络管理
    • 系统管理
    • 数据卷管理
    • 快照管理
    • 插件管理
    • 其他
    • swarmkit 管理
      • 模式管理
      • 节点管理
      • 秘钥管理
      • 服务管理
      • 服务栈管理

镜像管理 image

build

根据 dockerfile 构建镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root@ubuntu:~# docker image build -t centos2 .
Sending build context to Docker daemon 19.46kB
Step 1/2 : FROM centos
---> a8493f5f50ff
Step 2/2 : LABEL author "lvrui"
---> Running in 9a996772de96
---> 61b7efba9133
Removing intermediate container 9a996772de96
Successfully built 61b7efba9133
root@ubuntu:~# cat dockerfile
FROM centos
LABEL author="lvrui"
root@ubuntu:~# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
centos2 latest 61b7efba9133 About a minute ago 192MB
nginx2 0.1 7cb5220c81d5 2 hours ago 204MB
centos latest a8493f5f50ff 37 hours ago 192MB
nginx latest 5766334bdaa0 41 hours ago 183MB
hello-world latest 48b5124b2768 2 months ago 1.84kB

-t 的作用是打标签, 可以直接打成 Registry 的形式, 方便直接 push 到 Registry

如果没有打赏 Registry 地址也没有关系,后期可以通过 tag 命令修改

history

显示镜像的历史构建信息

1
2
3
4
5
6
7
8
9
10
11
12
13
root@ubuntu:~# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
centos2 latest 61b7efba9133 2 minutes ago 192MB
nginx2 0.1 7cb5220c81d5 2 hours ago 204MB
centos latest a8493f5f50ff 37 hours ago 192MB
nginx latest 5766334bdaa0 41 hours ago 183MB
hello-world latest 48b5124b2768 2 months ago 1.84kB
root@ubuntu:~# docker image history centos2
IMAGE CREATED CREATED BY SIZE COMMENT
61b7efba9133 2 minutes ago /bin/sh -c #(nop) LABEL author=lvrui 0B
a8493f5f50ff 37 hours ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 37 hours ago /bin/sh -c #(nop) LABEL name=CentOS Base ... 0B
<missing> 37 hours ago /bin/sh -c #(nop) ADD file:807143da05d7013... 192MB

import/save/load

  • import 导入镜像, 导入 export 出来的镜像
  • save 导出镜像
  • load 导入镜像, 导入 save 出来的镜像

inspect

查看镜像详细信息

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
root@ubuntu:~# docker image inspect centos2
[
{
"Id": "sha256:61b7efba9133e4ceaf4046e76a2f455a1204a8a794f16920c7543010e2759323",
"RepoTags": [
"centos2:latest"
],
"RepoDigests": [],
"Parent": "sha256:a8493f5f50ffda70c2eeb2d09090debf7d39c8ffcd63b43ff81b111ece6f28bf",
"Comment": "",
"Created": "2017-04-08T09:12:50.750877048Z",
"Container": "9a996772de96183b48460aa1f02b907d1e8d97f8d322cc41aff909d2a8a74b61",
"ContainerConfig": {
"Hostname": "f85c553c1496",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"LABEL author=lvrui"
],
"Image": "sha256:a8493f5f50ffda70c2eeb2d09090debf7d39c8ffcd63b43ff81b111ece6f28bf",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": [],
"Labels": {
"author": "lvrui",
"build-date": "20170406",
"license": "GPLv2",
"name": "CentOS Base Image",
"vendor": "CentOS"
}
},
"DockerVersion": "17.04.0-ce",
"Author": "",
"Config": {
"Hostname": "f85c553c1496",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/bash"
],
"Image": "sha256:a8493f5f50ffda70c2eeb2d09090debf7d39c8ffcd63b43ff81b111ece6f28bf",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": [],
"Labels": {
"author": "lvrui",
"build-date": "20170406",
"license": "GPLv2",
"name": "CentOS Base Image",
"vendor": "CentOS"
}
},
"Architecture": "amd64",
"Os": "linux",
"Size": 192481139,
"VirtualSize": 192481139,
"GraphDriver": {
"Data": null,
"Name": "aufs"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:36018b5e978717a047892794aebab513ba6856dbe1bdfeb478ca1219df2c7e9c"
]
}
}
]

ls

作用等同于 docker images 查看镜像列表

1
2
3
4
5
6
7
root@ubuntu:~# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
centos2 latest 61b7efba9133 3 hours ago 192MB
nginx2 0.1 7cb5220c81d5 5 hours ago 204MB
centos latest a8493f5f50ff 40 hours ago 192MB
nginx latest 5766334bdaa0 44 hours ago 183MB
hello-world latest 48b5124b2768 2 months ago 1.84kB

prune

删除没有被引用的镜像

1
root@ubuntu:~# docker image prune

tag/pull/push

  • tag: 打标签
  • pull: 从远程仓库 Registry 拉取镜像
  • push: 从本地上传镜像

从registry中拉取镜像:

1
$ sudo docker pull registry.cn-beijing.aliyuncs.com/lvreg/webconsole:[镜像版本号]

将镜像推送到registry:

1
2
3
$ sudo docker login --username=lvrui03@126.com registry.cn-beijing.aliyuncs.com
$ sudo docker tag [ImageId] registry.cn-beijing.aliyuncs.com/lvreg/webconsole:[镜像版本号]
$ sudo docker push registry.cn-beijing.aliyuncs.com/lvreg/webconsole:[镜像版本号]

注意: 如果 Registry 有密码, 还需要先 login 到镜像仓库才可以上传镜像

rm

删除指定的镜像

1
root@ubuntu:~# docker image rm centos2:latest

通过 dockerfile 可以制作镜像, 通过优化 dockerfile 中的指令, 可以减少镜像的体积. 按照一些规范制作 dockerfile, 可以增加 dockerfile 的可读性和可维护性

基本原则

  • 容器的生命周期是短暂的(无状态容器)
    通过 dockerfile 制作出的镜像, 从镜像启动容器, 这些容器的生命周期应尽量短暂. 这里的短暂意味着容器随时可以被停止和删除. 通过简单的配置可以立即启动一个新的容器
  • 尽量使用干净的目录去制作镜像, 避免不必要的性能损耗, 必要时可以使用.dockerignore文件排除不需要扫描的目录
  • 只安装需要的包
    为了减少镜像的体积和编译时间, 应该避免安装额外的, 不需要的包
  • 每个容器只运行一个进程
  • 减少镜像层
    dockerfile 中的指令会生成新的镜像层, 一个镜像最多有127层
  • 把多个参数排在不同的行中提高可读性\

注意: 编译缓存的问题
针对 ADD 和 COPY 命令, docker 会检查镜像层中所有源文件的元数据和文件内容. 其中检查元数据的时候, 不会检查最后修改时间和访问时间.
docker 在镜像缓存中寻找镜像层时, 不会检查文件. 例如, 在执行 RUN yum update 时, docker 仅仅会比较 RUN 命令的本身.

FROM 指令最佳实践

尽量使用官方镜像作为基础镜像. 如果考虑镜像的大小, 可以使用 Debian 的官方镜像, 该镜像体积小而且 Docker 对他进行了优化, store.docker.com 中有很多官方镜像都是基于 Debian 的镜像打造

RUN 指令最佳实践

  • 从可读性的角度考虑, 使用 RUN 命令时, 应使用 \ 将指令分成多行
  • 避免更新基础镜像中的基础软件包, 避免执行类似于 yum updateapt upgrade的命令. 在没有使用 --privileged的运行容器中, 基础镜像中的很多基础包是不能升级的.
  • 由于镜像的缓存机制, 在安装软件包时, 更新软件仓库索引和安装软件包必须在同一个 RUN 里执行, 否则可能会导致安装失败
1
2
3
4
5
# ✔️正确写法
RUN apt update && apt install -y \
dstat \
htop \
lsof
1
2
3
4
5
6
# ❌错误写法
RUN apt update
RUN apt install -y \
dstat \
htop \
lsof

由于镜像的缓存机制, 只验证的 RUN 后面的命令, 会导致执行过一次 apt update 之后, 再次遇到更新时将会使用缓存镜像, 从而导致以后安装不到最新的软件包

使用RUN apt update && apt install -y \的方式可以保证每次编译镜像时, 都是安装的最新的包. 这种技术叫做”缓存失效”

  • 尽量在一条 RUN 指令中包含所有需要的安装包. 如果安装包很多, 建议以首字母进行排序,每行只列出一个安装包, 方便后期维护.
  • 在安装命令的最后, 应该清理安装缓存, 减少镜像体积. 执行 ... && yum clean all... && rm -fr /var/lib/apt/lists/*

CMD & ENTRYPOINT 指令最佳实践

CMD

CMD 指令设置镜像中的默认启动命令和参数. 容器启动之后, 如果没有加入任何启动命令(也就是在镜像参数之后没有添加任何内容) 则默认执行镜像中 CMD 设置的默认的启动命令

  • 设置启动命令时, 应该尽量使用 JSON 格式 CMD ["command", "arg1", "arg2"]
    例如 Apache 的启动方式: CMD ["apache2", "-DFOREGROUND"]

  • 如果开发者和使用者都不是很熟悉 CMD 和 ENTRYPOINT 的工作原理的情况下, 尽量避免这两个指令配合使用
    例如 Django 的启动方式: CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

  • 相反, 如果开发者和使用者都很熟悉 CMD 和 ENTRYPOINT 的工作原理, 推荐 CMD 作为 ENTRYPOINT 的参数来配套使用

ENTRYPOINT

当需要把容器当做一个命令行工具使用时, 推荐通过 ENTRYPOINT 指令设置镜像的入口程序

  • 当启动主程序之前还需要执行大量的前置操作时, 可以将 ENTRYPOINT 的入口指令设置为一个脚本 entrypoint.sh

例如 postgres 的官方用法:

1
2
3
...
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"]

docker-entrypoint.sh ⬇️

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#!/bin/bash
set -e

# usage: file_env VAR [DEFAULT]
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
exit 1
fi
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
export "$var"="$val"
unset "$fileVar"
}

if [ "${1:0:1}" = '-' ]; then
set -- postgres "$@"
fi

# allow the container to be started with `--user`
if [ "$1" = 'postgres' ] && [ "$(id -u)" = '0' ]; then
mkdir -p "$PGDATA"
chown -R postgres "$PGDATA"
chmod 700 "$PGDATA"

mkdir -p /var/run/postgresql
chown -R postgres /var/run/postgresql
chmod g+s /var/run/postgresql

# Create the transaction log directory before initdb is run (below) so the directory is owned by the correct user
if [ "$POSTGRES_INITDB_XLOGDIR" ]; then
mkdir -p "$POSTGRES_INITDB_XLOGDIR"
chown -R postgres "$POSTGRES_INITDB_XLOGDIR"
chmod 700 "$POSTGRES_INITDB_XLOGDIR"
fi

exec gosu postgres "$BASH_SOURCE" "$@"
fi

if [ "$1" = 'postgres' ]; then
mkdir -p "$PGDATA"
chown -R "$(id -u)" "$PGDATA" 2>/dev/null || :
chmod 700 "$PGDATA" 2>/dev/null || :

# look specifically for PG_VERSION, as it is expected in the DB dir
if [ ! -s "$PGDATA/PG_VERSION" ]; then
file_env 'POSTGRES_INITDB_ARGS'
if [ "$POSTGRES_INITDB_XLOGDIR" ]; then
export POSTGRES_INITDB_ARGS="$POSTGRES_INITDB_ARGS --xlogdir $POSTGRES_INITDB_XLOGDIR"
fi
eval "initdb --username=postgres $POSTGRES_INITDB_ARGS"

# check password first so we can output the warning before postgres
# messes it up
file_env 'POSTGRES_PASSWORD'
if [ "$POSTGRES_PASSWORD" ]; then
pass="PASSWORD '$POSTGRES_PASSWORD'"
authMethod=md5
else
# The - option suppresses leading tabs but *not* spaces. :)
cat >&2 <<-'EOWARN'
****************************************************
WARNING: No password has been set for the database.
This will allow anyone with access to the
Postgres port to access your database. In
Docker's default configuration, this is
effectively any other container on the same
system.
Use "-e POSTGRES_PASSWORD=password" to set
it in "docker run".
****************************************************
EOWARN

pass=
authMethod=trust
fi

{
echo
echo "host all all all $authMethod"
} >> "$PGDATA/pg_hba.conf"

# internal start of server in order to allow set-up using psql-client
# does not listen on external TCP/IP and waits until start finishes
PGUSER="${PGUSER:-postgres}" \
pg_ctl -D "$PGDATA" \
-o "-c listen_addresses='localhost'" \
-w start

file_env 'POSTGRES_USER' 'postgres'
file_env 'POSTGRES_DB' "$POSTGRES_USER"

psql=( psql -v ON_ERROR_STOP=1 )

if [ "$POSTGRES_DB" != 'postgres' ]; then
"${psql[@]}" --username postgres <<-EOSQL
CREATE DATABASE "$POSTGRES_DB" ;
EOSQL
echo
fi

if [ "$POSTGRES_USER" = 'postgres' ]; then
op='ALTER'
else
op='CREATE'
fi
"${psql[@]}" --username postgres <<-EOSQL
$op USER "$POSTGRES_USER" WITH SUPERUSER $pass ;
EOSQL
echo

psql+=( --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" )

echo
for f in /docker-entrypoint-initdb.d/*; do
case "$f" in
*.sh) echo "$0: running $f"; . "$f" ;;
*.sql) echo "$0: running $f"; "${psql[@]}" -f "$f"; echo ;;
*.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${psql[@]}"; echo ;;
*) echo "$0: ignoring $f" ;;
esac
echo
done

PGUSER="${PGUSER:-postgres}" \
pg_ctl -D "$PGDATA" -m fast -w stop

echo
echo 'PostgreSQL init process complete; ready for start up.'
echo
fi
fi

exec "$@"

CMD 和 ENTRYPOINT的区别

  • 当 dockerfile 中指定了 ENTRYPOINT 的时候, docker run 如果在镜像之后添加的指令, 那么这些指令将被当做 ENTRYPOINT 的参数执行
  • 如果 dockerfile 中同时有 CMD 和 ENTRYPOINT 指令, 当 CMD 指令可执行时, 它将在 ENTRYPOINT 之前运行; 如果 CMD 不是可执行的命令, 则将作为 ENTRYPOINT 的命令参数追加

EXPOSE 最佳实践

  • EXPOSE 用来声明未来容器内需要监听的端口, 在 bridge 模式下, 这些容器内部的端口会映射到宿主机的端口上, 建议在容器内部不要更改应用原生的端口号

  • EXPOSE 中只能指定未来容器内部需要暴露的端口, 不能指定未来容器外部与内部端口之间的映射关系, 比如设置 EXPOSE 8800:80 是没有任何意义的

ENV 最佳实践

  • 通过环境变量来配置服务的相关设置, 通过这种方式可以方便的在不同环境中修改配置, 快速启动服务, 加快开发, 测试, 部署的流程
  • 设置系统环境变量, 比如二进制包安装的 nginx, 在 ENV 设置 PATH
  • 也可以借鉴 LABEL 的使用哲学, 标记一些版本号或版本编码信息

ADD 与 COPY 最佳实践

ADD 与 COPY 都是将外部文件拷贝到镜像内部的指令, 相比之下可能 ADD 的功能更加强大一下, 但是我的建议如下:

  • 尽量不要拷贝远程文件, 这样也就用不着 ADD 的功能, 用 COPY 就可以了
  • 如果压缩包拷贝进镜像后, 不希望这个压缩包被自动解压缩, 用 COPY 就对了. 反之如果希望拷贝进镜像之后就自动解压做, 那就用 ADD 拷贝进去

如果涉及到远程文件, 建议使用 RUN curlRUN wget 命令替代 ADD

VOLUME 最佳实践

VOLUME 的作用是持久化容器内的文件, 声明某个目录需要在宿主机挂载后, 当 docker run 起镜像时, docker 会在宿主机默认目录下随机生成一个文件夹挂载到容器内部的指定文件夹.

当该容器被删除时, 宿主机上的这个文件夹并不会被删除. 当删除容器时加入了-v 参数, 那么对应的数据卷就会被删除

USER 最佳实践

如果容器中的应用程序运行时不需要特殊的权限, 可以通过 USER 指令把应用程序的所有者设置为非 root 用户. 如果该用户不存在, 首先需要使用 RUN 命令在镜像中创建用户.

  • 如果在每次编译镜像时, 对用户的 UID/GID 有要求需要保持一致, 应该在新建用户和组的时候指定 UID和 GID
  • 在镜像中避免使用sudo 命令. 应为该命令使用的 TTY 不确定, 对接收信号量也会造成影响. 如果确实需要使用 sudo 功能, 则可是使用 gosu 命令替代
  • 可以用 root 用户初始化一个 daemon, 然后用非 root 用户启动这个 daemon
  • 为了减少镜像体积, 应该避免不必要的用户切换

WORKDIR 最佳实践

  • 尽量使用绝对路径
  • 切换目录的时候尽量使用 WORKDIR, 而不是使用 RUN cd /dir

gosu 工具

gosu 是一个 golang 语言开发的工具, 用来取代 shell 中的 sudo 命令. su 和 sudo 命令有一些缺陷, 主要是会引起不确定的 TTY, 对信号量的转发也存在问题. 如果仅仅为了使用特定的用户运行程序, 使用 su 或 sudo 显得太重了, 为此 gosu 应运而生.

gosu 直接借用了 libcontainer 在容器中启动应用程序的原理, 使用 /etc/passwd 处理应用程序. gosu 首先找出指定的用户或用户组, 然后切换到该用户或用户组. 接下来, 使用 exec 启动应用程序. 到此为止, gosu 完成了它的工作, 不会参与到应用程序后面的声明周期中. 使用这种方式避免了 gosu 处理 TTY 和转发信号量的问题, 把这两个工作直接交给了应用程序去完成

Dockerfile是由一系列命令和参数构成的脚本,一个Dockerfile里面包含了构建整个image的完整命令。Docker通过docker build执行Dockerfile中的一系列命令自动构建image

用法

dockerfile 使用 docker build 命令来生成 image 镜像, docker build的语法如下:

1
Usage:	docker build [OPTIONS] PATH | URL | -
  • PATH 是本地文件路径
  • URL 是 git repository 的路径

构建镜像时, docker 将会递归地读取路径下的所有文件. 因此, PATH包括任何子目录,URL包括repository及submodules

官方推荐: 在构建镜像的时候, 最好使用干净的目录进行构建, 减少不必要的索引性能损耗, 提高构建效率

如果在特殊环境的限制下, 一定要在有众多干扰文件的目录下构建镜像, 可以使用 .dockerignore 文件来屏蔽索引这些文件

你可以使用如下方式进行构建

1
2
3
4
5
6
7
8
9
10
11
# 最简单的执行方式
$ docker build .

# 如果文件名不是dockerfile, 那么需要显示的精确指定文件
$ docker build -f /path/to/a/Dockerfile .

# 顺便打个标签🏷
$ docker build -t shykes/myapp .

# 打多个标签
$ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest .

只要有可能,Docker将重新使用中间images(缓存),以显着加速docker build过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker build -t svendowideit/ambassador .
Sending build context to Docker daemon 15.36 kB
Step 1 : FROM alpine:3.2
---> 31f630c65071
Step 2 : MAINTAINER SvenDowideit@home.org.au
---> Using cache
---> 2a1c91448f5f
Step 3 : RUN apk update && apk add socat && rm -r /var/cache/
---> Using cache
---> 21ed6e7fbb73
Step 4 : CMD env | grep _TCP= | (sed 's/.*_PORT_\([0-9]*\)_TCP=tcp:\/\/\(.*\):\(.*\)/socat -t 100000000 TCP4-LISTEN:\1,fork,reuseaddr TCP4:\2:\3 \&/' && echo wait) | sh
---> Using cache
---> 7ea8aef582cc
Successfully built 7ea8aef582cc

构建成功后,就可以准备Pushing a repository to its registry

Format 语法格式

Dockerfile的格式如下:

1
2
# Comment
INSTRUCTION arguments
  • Line1 第一行是解析器指令
  • INSTRUCTION 是不区分大小写的,不过建议大写. 这里写 dockerfile 中支持的指令集
  • arguments 这里写对应的命令
  • dockerfile 中的第一个指令必须是FORM 指定构建镜像的Base Image, 这个 Base Image 基础镜像可以是一个系统的镜像比如CentOS Ubuntu等, 也可以是一个应用镜像, 用来做二次定制, 比如 nginx httpd mysql
  • dockerfile中以#开头的行都将视为注释,除非是 Parser directives 解析器指令, 而且 dockerfile 中不支持连续注释
1
2
# Comment
RUN echo 'we are running some # of cool things'

ParserDirectives 解析器指令

解析器指令是可选的, 并且影响处理 dockerfile 中后续的执行方式. 解析器的指令不会向构建中添加图层, 并且不会显示在构建的步骤当中. 解析器的指令是以 # directive = value 的形式写成一种特殊类型的注释, 且单个指令只能使用一次.(到目前为止 2017/04/19 dockerfile 只支持一种解析器指令)
当 docker 遇到普通注释行或空行或是已经被执行过的解析器指令, docker 都不会再寻找任何解析器的指令. 因此, 所有的解析器指令都必须位于 dockerfile 的最顶端

1
2
3
4
# escape=`

FROM windowsservercore
...

目前解析器指令只支持:

  • escape

escape

escape 作为 dockerfile 解析器目前为止唯一支持的指令, 目前有以下两种用法:

1
2
3
# escape=\ (backslash)
或者
# escape=` (backtick)

escape 的作用是声明 dockerfile 中的转义符, 在不指定的默认情况下, 转义符是反斜杠 \ 但是如果是在 Windows 中, 反斜杠是正常的路径分隔符, 不属于转义的范畴, 这种情况下就需要使用两个反斜杠 \\ 来表示一个路径中的反斜杠 \

转义字符既用于转义行中的字符,也用于转义换行符. 这允许Dockerfile指令跨越多行. 注意,不管escape解析器指令是否包括在Dockerfile中,在RUN命令中不执行转义,除非在行的末尾, 用来转义换行符

请考虑以下示例,这将在Windows上以非显而易见的方式失败。第二行末尾的第二个\将被解释为换行符,而不是从第一个\转义的目标。类似地,假设第三行结尾处的\实际上作为一条指令处理,它将被视为行继续。这个dockerfile的结果是第二行和第三行被认为是单个指令:

1
2
3
4
5
6
7
8
9
10
11
12
FROM windowsservercore
COPY testfile.txt c:\\
RUN dir c:\

# 结果:
PS C:\John> docker build -t cmd .
Sending build context to Docker daemon 3.072 kB
Step 1 : FROM windowsservercore
---> dbfee88ee9fd
Step 2 : COPY testfile.txt c:RUN dir c:
GetFileAttributesEx c:RUN: The system cannot find the file specified.
PS C:\John>

上述的一个解决方案是使用/作为COPY指令和dir的目标. 这种语法, 最好的情况是混乱, 因为它在Windows上是不平常的路径, 最坏的情况下, 错误倾向, 因为并不是Windows上的所有命令支持/作为路径分隔符.

通过添加转义解析器指令,下面的Dockerfile在Windows上使用文件路径的自然平台语义成功:

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
# escape=`

FROM windowsservercore
COPY testfile.txt c:\
RUN dir c:\

# 结果
PS C:\John> docker build -t succeeds --no-cache=true .
Sending build context to Docker daemon 3.072 kB
Step 1 : FROM windowsservercore
---> dbfee88ee9fd
Step 2 : COPY testfile.txt c:\
---> 99ceb62e90df
Removing intermediate container 62afbe726221
Step 3 : RUN dir c:\
---> Running in a5ff53ad6323
Volume in drive C has no label.
Volume Serial Number is 1440-27FA

Directory of c:\

03/25/2016 05:28 AM <DIR> inetpub
03/25/2016 04:22 AM <DIR> PerfLogs
04/22/2016 10:59 PM <DIR> Program Files
03/25/2016 04:22 AM <DIR> Program Files (x86)
04/18/2016 09:26 AM 4 testfile.txt
04/22/2016 10:59 PM <DIR> Users
04/22/2016 10:59 PM <DIR> Windows
1 File(s) 4 bytes
6 Dir(s) 21,252,689,920 bytes free
---> 2569aa19abef
Removing intermediate container a5ff53ad6323
Successfully built 2569aa19abef
PS C:\John>

ENV 环境变量替换

环境变量使用ENV命令声明, 使用$variable_name${variable_name}来调用, 带大括号的语法是被用来解决不带空格的变量名字问题, 例如:${foo}_bar

${variable_name}语法还支持以下指定的一些标准bash修饰符:

  • ${variable:-word}表示如果设置了variable, 则结果将是该值; 如果variable未设置, 那么word将是结果
  • ${variable:+word}表示如果设置了variable, 那么word将是结果, 否则结果是空字符串

在所有情况下, word可以是任何字符串, 包括额外的环境变量

可以通过在变量之前添加\来转义: \$foo\${foo}, 分别转换为$foo${foo}

示例(解析的表示显示在#后面):

1
2
3
4
5
FROM busybox
ENV foo /bar
WORKDIR ${foo} # WORKDIR /bar
ADD . $foo # ADD . /bar
COPY \$foo /quux # COPY $foo /quux

Dockerfile中的以下指令列表支持环境变量:

  • ADD
  • COPY
  • ENV
  • EXPOSE
  • LABEL
  • USER
  • WORKDIR
  • VOLUME
  • STOPSIGNAL

以及:

  • ONBUILD(当与上面支持的指令之一组合时)

注意: 在1.4之前, ONBUILD指令不支持环境变量, 即使与上面列出的任何指令相结合

ENV的使用中, 要特别注意以下情况:

1
2
3
ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc

最终的结果为:

  • def 的值为 hello
  • ghi 的值为 bye

为什么def的值不是bye呢? 因为第一行设置abc变量的值为hello后, 到第二行执行的时候, 首先进行变量的渲染(替换), 然后再执行环境变量的设置

.dockerignore

docker执行构建后, 会首先去根目录中查找.dockerignore文件, 由于在构建的时候, docker 会递归索引当前目录下的所有文件, 为了避免索引不必要的目录, 可以使用.dockerignore文件声明不需要索引的目录

  • # 注释
  • * 任意多个字符
  • ? 任意一个字符
  • ! 异常模式, 用于排除例外
1
2
3
4
# comment
*/temp*
*/*/temp*
temp?
1
2
*.md
!README.md

FORM

1
2
3
4
5
FROM <image>
# 或则
FROM <image>:<tag>
# 或则
FROM <image>@<digest>

FROM指令为后续指令设置Base Image. 因此, 有效的Dockerfile必须具有FROM作为其第一条指令. image可以是任何有效的image, 可以从镜像仓库中拉取镜像.

  • FROM必须是Dockerfile中的第一个非注释指令
  • FROM可以在单个Dockerfile中多次出现,以创建多个镜像。每个新的FROM命令之前会输出最一个image ID
  • tag或digest是可选的. 如果省略其中任何一个, 构建器将默认使用latest. 如果构建器与tag值不匹配, 则构建器将返回错误

RUN

RUN 指令有两种运行方式:

  • RUN (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)
  • RUN [“executable”, “param1”, “param2”] (exec form)

RUN指令将在当前image之上的新层中执行任何命令, 并提交结果. 生成的已提交image将用于Dockerfile中的下一步

exec形式使得可以避免shell字符串变化, 以及使用不包含指定的shell可执行文件的基本image来运行RUN命令

在shell形式中,可以使用\(反斜杠)将单个RUN指令继续到下一行, 这也是大部分 dockerfile 使用的形式

RUN命令的使用中, 需要注意一下几点:

  • 要使用不同的shell,而不是’/bin/sh’,请使用在所需shell中传递的exec形式。例如,RUN [“/bin/bash”,“-c”,“echo hello”]
  • exec形式作为JSON数组解析,这意味着你必须在单词之外必须要使用双引号" 而不是单引号'
  • 与 shell 执行的方式不同, exec 执行方式不调用命令 shell. 这意味着正常的 shell 处理不会发生. 例如, RUN ["echo","$HOME"]不会在$HOME上进行可变替换. 如果你想要shell处理,那么使用shell形式或直接执行一个shell,例如, RUN ["sh","-c","echo $HOME"]
  • 在JSON形式中, 有必要转义反斜杠. 这在Windows上特别相关, 其中反斜杠是路径分隔符. 因为不是有效的JSON, 并且以意外的方式失败, 以下行将被视为shell形式: RUN ["c:\windows\system32\tasklist.exe"] 此示例的正确语法为: RUN ["c:\\windows\\system32\\tasklist.exe"]

用于RUN指令的高速缓存在下一次构建期间不会自动失效. 用于诸如RUN apt-get dist-upgrade之类的指令的高速缓存将在下一次构建期间被重用. 可以通过使用--no-cache标志来使用于RUN指令的高速缓存无效, 例如 docker build --no-cache

Known issues(RUN

Issue 783是关于在使用AUFS文件系统时可能发生的文件权限问题。例如,您可能会在尝试rm文件时注意到它。对于具有最近aufs版本的系统(即,可以设置dirperm1安装选项),docker将尝试通过使用dirperm1选项安装image来自动解决问题。

MAINTAINER 作者信息(已废弃)

1
MAINTAINER <name>

MAINTAINER指令允许您设置生成的images的作者字段

在新的 docker 版本中, 该指令将被LABEL替代

1
LABEL maintainer "SvenDowideit@home.org.au"

CMD

CMD指令三种形式:

  • CMD [“executable”,”param1”,”param2”] (exec格式, 首选形式)
  • CMD [“param1”,”param2”] (作为 ENTRYPOINT 指令的默认参数)
  • CMD command param1 param2 (shell 格式)

在Dockerfile中只能有一个CMD指令. 如果列出了多个CMD, 则只有最后一个CMD生效

CMD的主要目的是为执行容器提供默认值. 这些默认值可以包括可执行文件, 或者它们可以省略可执行文件, 在这种情况下, 你还必须指定ENTRYPOINT指令

当以shell或exec格式使用时,CMD指令设置运行image时要执行的命令

如果使用CMD的shell形式,那么将在/bin/sh -c中执行:

1
2
FROM ubuntu
CMD echo "This is a test." | wc -

如果你想运行你的没有shell,那么你必须将该命令表示为一个JSON数组,并给出可执行文件的完整路径。此数组形式是CMD的首选格式。任何其他参数必须单独表示为数组中的字符串(注意使用双引号):

1
2
FROM ubuntu
CMD ["/usr/bin/wc","--help"]

如果你希望你的容器每次运行相同的可执行文件,那么你应该考虑使用ENTRYPOINT结合CMD

如果用户指定docker run参数,那么它们将覆盖CMD中指定的默认值

注意:

  • 不要将RUN和CMD混淆. RUN实际上运行一个命令并提交结果;CMD在构建时不执行任何操作, 但指定了image的预期命令
  • 如果使用CMD为ENTRYPOINT指令提供默认参数, CMD和ENTRYPOINT指令都应以JSON数组格式指定
  • exec形式作为JSON数组解析, 这意味着您必须在单词之外使用双引号"而不是单引号'
  • 与shell表单不同, exec表单不调用命令shell. 这意味着正常的shell处理不会发生. 例如, CMD ["echo","$HOME"] 不会在$HOME上进行可变替换. 如果你想要shell处理, 那么使用shell形式或直接执行一个shell, 例如: CMD ["sh","-c","echo $HOME"]

LABEL

1
2
3
4
5
LABEL <key>=<value> <key>=<value> <key>=<value> ...

LABEL <key>=<value>
LABEL <key>=<value>
LABEL <key>=<value>

LABEL 指令向image添加元数据. LABEL是键值对. 要在LABEL值中包含空格, 需要使用引号和反斜杠, 就像在命令行解析中一样

1
2
3
4
5
LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."

一个image可以有多个label. 要指定多个label, Docker建议在可能的情况下将标签合并到单个LABEL指令中. 每个LABEL指令产生一个新层, 如果使用许多标签, 可能会导致镜像的效率低下. 该示例产生单个镜像层

1
LABEL multi.label1="value1" multi.label2="value2" other="value3"

上面的也可写为:

1
2
3
LABEL multi.label1="value1" \
multi.label2="value2" \
other="value3"

如果Docker遇到已经存在的label/key,则新值将覆盖具有相同键的任何先前标签

EXPOSE

1
EXPOSE <port> [<port>...]

EXPOSE指令通知Docker容器在运行时监听指定的网络端口. EXPOSE不使主机的容器的端口可访问, 为此, 必须使用-p标志发布一系列端口, 或者使用-P标志发布所有暴露的端口。可以公开一个端口号,并用另一个端口号在外部发布

这里指定的所有端口都是容器内部声明容器运行后要监听的端口, 不能在这里定义宿主机与容器之间端口的对应关系! 比如一个 nginx 的镜像, 内部监听80端口, 在容器运行之后, 你想映射到8080端口, 此时在 dockerfile 中写 8080:80 是没有任何意义的, 镜像(dockerfile)里的声明的端口与容器运行指定的端口是分开的, 不能在 dockerfile 中指定未来容器运行时映射的端口

ENV

1
2
ENV <key> <value>
ENV <key>=<value> ... (首选形式)
  • 第一种形式只能设置一个环境变量
  • 第二种形式可以在一个高速缓存层中设置多个变量, 即使是一个变量要设置也推荐使用这种模式来设置
1
2
3
4
5
6
ENV myName="John Doe" myDog=Rex\ The\ Dog \
myCat=fluffy
# 和
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy

将在最终容器中产生相同的结果,但第一种形式是优选的,因为它产生单个高速缓存层

ADD

两种形式:

1
2
ADD <src>... <dest>
ADD ["<src>",... "<dest>"] (对于包含空格的路径,此形式是必需的)

ADD 指令从复制新文件, 目录或远程文件URL, 并将它们添加到容器的文件系统, 路径

可以指定多个资源, 但如果它们是文件或目录, 那么它们必须是相对于正在构建的源目录

每个可能包含通配符, 匹配将使用Go的filepath.Match规则完成. 例如:

1
2
ADD hom* /mydir/        # adds all files starting with "hom"
ADD hom?.txt /mydir/ # ? is replaced with any single character, e.g., "home.txt"

是绝对路径或相对于WORKDIR的路径, 源将在目标容器中复制到其中

1
2
ADD test relativeDir/          # adds "test" to `WORKDIR`/relativeDir/
ADD test /absoluteDir/ # adds "test" to /absoluteDir/

所有新文件和目录都使用UID和GID为0创建

ADD遵守以下规则:

  • 路径必须在构建的上下文中, 不能ADD ../something /something 因为docker构建的第一步是发送上下文目录(和子目录)到docker守护进程.

  • 如果是URL并且不以尾部斜杠结尾,则从URL下载文件并将其复制到

  • 如果是URL并且以尾部斜杠结尾,则从URL中推断文件名,并将文件下载到/ 例如, ADD http://example.com/foobar /会创建文件/foobar 网址必须有一个非普通的路径, 以便在这种情况下可以发现一个适当的文件名http://example.com不会工作

  • 如果是目录, 则复制目录的整个内容, 包括文件系统元数据
    注意:目录本身不被复制,只是其内容

  • 如果是识别的压缩格式(identity,gzip,bzip2或xz)的本地tar存档,则将其解包为目录. 来自远程URL的资源不会解压缩. 当目录被复制或解压缩时, 它具有与tar -x相同的行为
    注意:文件是否被识别为识别的压缩格式, 仅基于文件的内容, 而不是文件的名称. 例如, 如果一个空文件以.tar.gz结尾, 则不会被识别为压缩文件, 并且不会生成任何解压缩错误消息, 而是将该文件简单地复制到目的地

  • 如果是任何其他类型的文件, 它会与其元数据一起单独复制. 在这种情况下, 如果以尾部斜杠/结尾,它将被认为是一个目录, 并且的内容将被写在/base()

  • 如果直接或由于使用通配符指定了多个资源, 则必须是目录, 并且必须以斜杠/结尾

  • 如果不以尾部斜杠结尾,它将被视为常规文件,的内容将写在

  • 如果不存在, 则会与其路径中的所有缺少的目录一起创建

COPY

两种形式:

1
2
COPY <src>... <dest>
COPY ["<src>",... "<dest>"] (this form is required for paths containing whitespace)

基本和ADD类似,不过COPY的不能为URL

与 ADD 不同的一点是, 遇到压缩文件, COPY 不会去检测文件的压缩属性, 不会做任何的解压缩操作, 会直接把源文件原封不动的拷贝到目标地址

ENTRYPOINT

两种形式:

1
2
ENTRYPOINT [“executable”, “param1”, “param2”] (exec 形式, 首选)
ENTRYPOINT command param1 param2 (shell 形式)

ENTRYPOINT允许您配置容器, 运行执行的可执行文件

例如, 以下将使用其默认内容启动nginx, 监听端口80:

1
docker run -i -t --rm -p 80:80 nginx

docker run <image>的命令行参数将附跟在 exec 形式的ENTRYPOINT中的所有元素之后, 并将覆盖使用CMD指定的所有元素. 这允许将参数传递到入口点, 即docker run <image> -d将把-d参数传递给入口点. 您可以使用docker run --entrypoint标志覆盖ENTRYPOINT指令

shell 形式防止使用任何CMD或运行命令行参数, 但是缺点是ENTRYPOINT将作/bin/sh -c的子命令启动, 它不传递信号. 这意味着可执行文件将不是容器的PID 1, 并且不会接收Unix信号, 因此你的可执行文件将不会从docker stop <container>接收到SIGTERM

只有Dockerfile中最后一个ENTRYPOINT指令会有效果

Exec form ENTRYPOINT example

可以使用ENTRYPOINT的exec形式设置相当稳定的默认命令和参数, 然后使用任一形式的CMD设置更可能更改的其他默认值

1
2
3
FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

以下Dockerfile显示使用ENTRYPOINT在前台运行Apache, 作为PID 1

1
2
3
4
5
FROM debian:stable
RUN apt-get update && apt-get install -y --force-yes apache2
EXPOSE 80 443
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"]
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]

如果需要为单个可执行文件编写启动脚本,可以使用exec和gosu命令确保最终可执行文件接收到Unix信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
set -e

if [ "$1" = 'postgres' ]; then
chown -R postgres "$PGDATA"

if [ -z "$(ls -A "$PGDATA")" ]; then
gosu postgres initdb
fi

exec gosu postgres "$@"
fi

exec "$@"
  • set -e : 在脚本的执行中任何一行出现执行失败都立即退出脚本
  • set -eo pipefail : 在管道的使用中, 任意一个环节出现问题都立即退出
  • set – postgres “$@” : 将后面的每一个值依次放入 $1 $2 $3 …
  • $@ : 传给脚本的所有参数的列表

上面脚本的意思是, 首先判断第一个参数是不是预期中的可执行命令, 如果第一个参数是预期中的可执行命令, 那么后面参数默认均为第一个(参数)命令的参数; 如果第一个参数不是预期中的可行性命令, 那么就认为这是一个全新的命令要执行, 所以使用exec $@完整地执行命令

如果需要在关闭时进行一些额外的清理(或与其他容器通信)或者协调多个可执行文件, 需要确保ENTRYPOINT脚本接收到Unix信号, 传递它们, 然后做一些更多的善后工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh
# Note: I've written this using sh so it works in the busybox container too

# USE the trap if you need to also do manual cleanup after the service is stopped,
# or need to start multiple services in the one container
trap "echo TRAPed signal" HUP INT QUIT TERM

# start service in background here
/usr/sbin/apachectl start

echo "[hit enter key to exit] or run 'docker stop <container>'"
read

# stop service and clean up here
echo "stopping apache"
/usr/sbin/apachectl stop

echo "exited $0"

Shell form ENTRYPOINT example

你可以为ENTRYPOINT指定一个纯字符串,它将在/bin/sh -c中执行。这种形式将使用shell处理来替换shell环境变量,并且将忽略任何CMDdocker run命令行参数。要确保docker stop将正确地控制ENTRYPOINT可执行文件,必须用exec启动它:

1
2
FROM ubuntu
ENTRYPOINT exec top -b

运行此image时,您将看到单个PID 1进程:

1
2
3
4
5
6
$ docker run -it --rm --name test top
Mem: 1704520K used, 352148K free, 0K shrd, 0K buff, 140368121167873K cached
CPU: 5% usr 0% sys 0% nic 94% idle 0% io 0% irq 0% sirq
Load average: 0.08 0.03 0.05 2/98 6
PID PPID USER STAT VSZ %VSZ %CPU COMMAND
1 0 root R 3164 0% 0% top -b

使用docker stop可以干净的退出:

1
2
3
4
5
$ /usr/bin/time docker stop test
test
real 0m 0.20s
user 0m 0.02s
sys 0m 0.04s

如果忘记将exec添加到您的ENTRYPOINT的开头:

1
2
3
FROM ubuntu
ENTRYPOINT top -b
CMD --ignored-param1

然后,你可以运行它(给它一个名称为下一步):

1
2
3
4
5
6
7
$ docker run -it --name test top --ignored-param2
Mem: 1704184K used, 352484K free, 0K shrd, 0K buff, 140621524238337K cached
CPU: 9% usr 2% sys 0% nic 88% idle 0% io 0% irq 0% sirq
Load average: 0.01 0.02 0.05 2/101 7
PID PPID USER STAT VSZ %VSZ %CPU COMMAND
1 0 root S 3168 0% 0% /bin/sh -c top -b cmd cmd2
7 1 root R 3164 0% 0% top -b

您可以从top的输出中看到指定的ENTRYPOINT不是PID 1。

如果你运行docker停止测试,容器将不会完全退出, 超时后停止命令将强制发送SIGKILL:

1
2
3
4
5
6
7
8
9
10
$ docker exec -it test ps aux
PID USER COMMAND
1 root /bin/sh -c top -b cmd cmd2
7 root top -b
8 root ps aux
$ /usr/bin/time docker stop test
test
real 0m 10.19s
user 0m 0.04s
sys 0m 0.03s

Understand how CMD and ENTRYPOINT interact

CMDENTRYPOINT指令定义在运行容器时执行什么命令, 有如下使用规则

  • Dockerfile应该至少指定一个CMDENTRYPOINT命令
  • 当使用容器作为可执行文件时,应该定义ENTRYPOINT
  • CMD应该用作定义ENTRYPOINT命令的默认参数或在容器中执行ad-hoc命令的一种方法
  • 当运行带有替代参数的容器时,CMD将被覆盖

下表显示了对不同ENTRYPOINT/CMD组合执行的命令:

no ENTRYPOINT ENTRYPOINT exec_enty p1_entry ENTRYPOINT [“exec_entry”,“p1_entry”]
No CMD error, not allowed /bin/sh -c exec_entry p1_entry exec_entry p1_entry
CMD [“exec_cmd”,“p1_cmd”] exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_cmd p1_cmd exec_entry p1_entry exec_cmd p1_cmd
CMD [“p1_cmd”, “p2_cmd”] p1_cmd p2_cmd /bin/sh -c exec_entry p1_entry p1_cmd p2_cmd exec_entry p1_entry p1_cmd p2_cmd
CMD exec_cmd p1_cmd /bin/sh -c exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd

VOLUME

1
VOLUME ["/data"]

VOLUME指令创建具有指定名称的挂载点,并将其标记为从本机主机或其他容器保留外部挂载的卷。该值可以是JSON数组VOLUME ["/var/log/"]或具有多个参数的纯字符串,例如VOLUME /var/logVOLUME /var/log /var/db

1
2
3
4
FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

USER

1
USER daemon

USER指令设置运行image时使用的用户名或UID, 以及Dockerfile中的任何RUN,CMDENTRYPOINT指令

WORKDIR

1
WORKDIR /path/to/workdir

WORKDIR指令为Dockerfile中的任何RUNCMDENTRYPOINTCOPYADD指令设置工作目录。如果WORKDIR不存在,它将被创建,即使它没有在任何后续的Dockerfile指令中使用

它可以在一个Dockerfile中多次使用. 如果提供了相对路径, 它将相对于先前WORKDIR指令的路径. 例如:

1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

在这个Dockerfile中的最终pwd命令的输出是/a/b/c

WORKDIR指令可以解析先前使用ENV设置的环境变量。您只能使用在Dockerfile中显式设置的环境变量. 例如:

1
2
3
ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd

pwd命令在该Dockerfile中输出的最后结果是/path/$DIRNAME

ARG

1
ARG <name>[=<default value>]

ARG指令定义一个变量, 用户可以使用docker build命令使用--build-arg <varname> = <value>标志,在构建时将其传递给构建器. 如果用户指定了一个未在Dockerfile中定义的构建参数, 构建将输出错误

1
One or more build-args were not consumed, failing build.

Dockerfile作者可以通过指定ARG一个或多个变量, 通过多次指定ARG来定义单个变量. 例如, 一个有效的Dockerfile

1
2
3
4
FROM busybox
ARG user1
ARG buildno
...

Dockerfile作者可以可选地指定ARG指令的默认值:

1
2
3
4
FROM busybox
ARG user1=someuser
ARG buildno=1
...

如果ARG值具有缺省值,并且如果在构建时没有传递值,则构建器使用缺省值。

ARG变量定义从在Dockerfile中定义的行开始生效,而不是从命令行或其他地方的参数使用。例如,考虑这个Dockerfile:

1
2
3
4
5
1 FROM busybox
2 USER ${user:-some_user}
3 ARG user
4 USER $user
...

用户构建此文件如下:

1
$ docker build --build-arg user=what_user Dockerfile

第2行的USER将评估为some_user,因为用户变量在后续行3上定义。第4行的USER在定义用户时估计为what_user,在命令行中传递what_user值。在通过ARG指令定义之前,变量的任何使用都将导致空字符串。

警告:不建议使用build-time变量来传递诸如github密钥, 用户凭证等密码. 构建时变量值使用docker history命令对镜像的任何用户可见

可以使用ARGENV指令来指定RUN指令可用的变量。使用ENV指令定义的环境变量总是覆盖同名的ARG指令。思考这个Dockerfile带有ENVARG指令。

1
2
3
4
1 FROM ubuntu
2 ARG CONT_IMG_VER
3 ENV CONT_IMG_VER v1.0.0
4 RUN echo $CONT_IMG_VER

然后,假设此image是使用此命令构建的:

1
$ docker build --build-arg CONT_IMG_VER=v2.0.1 Dockerfile

在这种情况下,RUN指令使用v1.0.0而不是用户传递的ARG设置:v2.0.1此行为类似于shell脚本,其中本地作用域变量覆盖作为参数传递或从环境继承的变量,从其定义点。

使用上述示例,但使用不同的ENV规范,您可以在ARGENV指令之间创建更有用的交互:

1
2
3
4
1 FROM ubuntu
2 ARG CONT_IMG_VER
3 ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0}
4 RUN echo $CONT_IMG_VER

ARG指令不同, ENV值始终保留在image中. 考虑一个没有-build-arg标志的docker构建:

1
$ docker build Dockerfile

使用这个Dockerfile示例,CONT_IMG_VER仍然保留在映像中,但它的值将是v1.0.0,因为它是ENV指令在第3行中的默认设置。

此示例中的变量扩展技术允许您从命令行传递参数,并通过利用ENV指令将它们持久保存在最终image中

Docker有一组预定义的ARG变量,您可以在Dockerfile中使用相应的ARG指令。

  • HTTP_PROXY
  • http_proxy
  • HTTPS_PROXY
  • https_proxy
  • FTP_PROXY
  • ftp_proxy
  • NO_PROXY
  • no_proxy

要使用这些,只需在命令行使用标志传递它们:

1
--build-arg <varname>=<value>

Impact on build caching

ARG变量不会持久化到构建的image中,因为ENV变量是。但是,ARG变量会以类似的方式影响构建缓存。如果一个Dockerfile定义一个ARG变量,它的值不同于以前的版本,那么在它的第一次使用时会出现一个“cache miss”,而不是它的定义。特别地,在ARG指令之后的所有RUN指令都隐式地使用ARG变量(作为环境变量),因此可能导致高速缓存未命中。

例如,考虑这两个Dockerfile:

1
2
3
1 FROM ubuntu
2 ARG CONT_IMG_VER
3 RUN echo $CONT_IMG_VER
1
2
3
1 FROM ubuntu
2 ARG CONT_IMG_VER
3 RUN echo hello

如果在命令行上指定--build-arg CONT_IMG_VER = <value>,则在这两种情况下,第2行的规范不会导致高速缓存未命中;行3确实导致高速缓存未命中。ARG CONT_IMG_VER导致RUN行被标识为与运行CONT_IMG_VER = <value> echo hello相同,因此如果<value>更改,我们将得到高速缓存未命中。

考虑在同一命令行下的另一个示例:

1
2
3
4
1 FROM ubuntu
2 ARG CONT_IMG_VER
3 ENV CONT_IMG_VER $CONT_IMG_VER
4 RUN echo $CONT_IMG_VER

在此示例中,高速缓存未命中发生在第3行。由于变量在ENV中的值引用ARG变量并且该变量通过命令行更改,因此发生了未命中。在此示例中,ENV命令使image包括该值。

如果ENV指令覆盖同名的ARG指令,就像这个Dockerfile:

1
2
3
4
1 FROM ubuntu
2 ARG CONT_IMG_VER
3 ENV CONT_IMG_VER hello
4 RUN echo $CONT_IMG_VER

第3行不会导致高速缓存未命中,因为CONT_IMG_VER的值是一个常量(hello)因此,RUN(第4行)上使用的环境变量和值在构建之间不会更改

ONBUILD

1
ONBUILD [INSTRUCTION]

ONBUILD指令在image被用作另一个构建的基础时,向image添加要在以后执行的trigger指令。trigger将在下游构建的上下文中执行,就好像它已经在下游Dockerfile中的1FROM1指令之后立即插入。

任何构建指令都可以注册为trigger。

如果您正在构建将用作构建其他image的基础的图像,例如应用程序构建环境或可以使用用户特定配置自定义的后台驻留程序,这将非常有用。

例如,如果您的image是可重用的Python应用程序构建器,则需要将应用程序源代码添加到特定目录中,并且可能需要在此之后调用构建脚本。你不能只是调用ADDRUN现在,因为你还没有访问应用程序源代码,它将是不同的每个应用程序构建。您可以简单地为应用程序开发人员提供一个样板Dockerfile以将其复制粘贴到其应用程序中,但这是低效,容易出错,并且很难更新,因为它与应用程序特定的代码混合。

解决方案是使用ONBUILD来注册提前指令,以便稍后在下一个构建阶段运行。

以下是它的工作原理:

  1. 当遇到ONBUILD指令时,构建器会向正在构建的image的元数据添加trigger。该指令不会另外影响当前构建。
  2. 在构建结束时,所有trigger的列表存储在image清单中的OnBuild键下。可以使用docker inspect命令检查它们。
  3. 稍后,可以使用FROM指令将image用作新构建的基础。作为处理FROM指令的一部分,下游构建器会查找ONBUILDtriggers,并按照它们注册的顺序执行它们。如果任何触发器失败,则FROM指令中止,这又导致构建失败。如果所有触发器都成功,则FROM指令完成并且构建如常继续。触发器在执行后从最终image中清除。换句话说,它们不会被“grand-children”构建继承。 例如,您可以添加如下: [...] ONBUILD ADD . /app/src ONBUILD RUN /usr/local/bin/python-build --dir /app/src [...]> 警告:不允许使用ONBUILD ONBUILD链接ONBUILD指令。 > 警告ONBUILD指令可能不会触发FROMMAINTAINER指令。

STOPSIGNAL

1
STOPSIGNAL signal

STOPSIGNAL指令设置将发送到容器以退出的系统调用信号。该信号可以是与内核系统调用表中的位置匹配的有效无符号数,例如9,或者是SIGNAME格式的信号名称,例如SIGKILL

HEALTHCHECK

两种形式:

  • HEALTHCHECK [OPTIONS] CMD command (通过在容器中运行命令来检查容器运行状况)
  • HEALTHCHECK NONE (禁用从基本映像继承的任何运行状况检查)

HEALTHCHECK指令告诉Docker如何测试容器以检查它是否仍在工作。这可以检测到诸如Web服务器被卡在无限循环中并且无法处理新连接的情况,即使服务器进程仍在运行。

当容器指定了healthcheck时,除了其正常状态外,它还具有健康状态。此状态最初开始。 每当健康检查通过,它变得健康(无论之前的状态)。在一定数量的连续故障后,它变得不健康。

在CMD之前可以出现的选项有:

  • --interval=DURATION (default: 30s)
  • --timeout=DURATION (default: 30s)
  • --retries=N (default: 3)

运行状况检查将首先在容器启动后运行interval秒,然后在每次上次检查完成后再次运行interval秒。

如果检查的单次运行所花费的时间超过timeout秒数,则该检查被认为已失败。

它需要retries连续的健康检查的故障,容器被认为是不健康的。

在Dockerfile中只能有一个HEALTHCHECK指令。如果您列出多个,则只有最后一个HEALTHCHECK将生效。

CMD关键字之后的命令可以是shell命令(例如HEALTHCHECK CMD /bin/check-running)或exec数组(如同其他Dockerfile命令一样;详情参见ENTRYPOINT)。

命令的退出状态表示容器的运行状况。 可能的值为:

  • 0: success - the container is healthy and ready for use
  • 1: unhealthy - the container is not working correctly
  • 2: reserved - do not use this exit code

例如,要每五分钟检查一次Web服务器能够在三秒钟内为网站的主页提供服务:

1
2
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/ || exit 1

为了帮助调试失败的探测器,命令在stdout或stderr上写入的任何输出文本(UTF-8编码)将存储在运行状况状态,并可以使用docker inspect查询。这样的输出应该保持短路(只存储当前的4096个字节)。

当容器的运行状况发生更改时,将生成具有新状态的health_status事件。

HEALTHCHECK功能在Docker 1.12中添加。

SHELL

1
SHELL ["executable", "parameters"]

SHELL指令允许用于命令的shell形式的默认shell被覆盖。 Linux上的默认shell是["/bin/sh","-c"],在Windows上是["cmd","/S","/C"]SHELL指令必须以JSON格式写在Dockerfile中。

SHELL指令在Windows上特别有用,其中有两个常用的和完全不同的本机shell:cmdpowershell,以及包括sh的备用Shell。

SHELL指令可以多次出现。每个SHELL指令覆盖所有先前的SHELL指令,并影响所有后续指令。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM windowsservercore

# Executed as cmd /S /C echo default
RUN echo default

# Executed as cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# Executed as cmd /S /C echo hello
SHELL ["cmd", "/S"", "/C"]
RUN echo hello

以下指令可能受SHELL指令的影响,当它们的shell形式用于Dockerfile:RUNCMDENTRYPOINT

以下示例是Windows上的常见模式,可以使用SHELL指令进行简化:

1
2
3
...
RUN powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
...

docker调用的命令将是:

1
cmd /S /C powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"

这是低效的,有两个原因。首先,有一个不必要的cmd.exe命令处理器(也称为shell)被调用。其次,shell中的每个RUN指令都需要一个额外的powershell -command

为了更有效率,可以采用两种机制之一。 一种是使用JSON形式的RUN命令,如:

1
2
3
...
RUN ["powershell", "-command", "Execute-MyCmdlet", "-param1 \"c:\\foo.txt\""]
...

虽然JSON形式是明确的,并且不使用不必要的cmd.exe,但它需要通过双引号和转义更详细。 备用机制是使用SHELL指令和shell形式,为Windows用户提供更自然的语法,特别是与escape 解析指令结合使用时:

1
2
3
4
5
6
7
# escape=`

FROM windowsservercore
SHELL ["powershell","-command"]
RUN New-Item -ItemType Directory C:\Example
ADD Execute-MyCmdlet.ps1 c:\example\
RUN c:\example\Execute-MyCmdlet -sample 'hello world'

结果是:

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
PS E:\docker\build\shell> docker build -t shell .
Sending build context to Docker daemon 3.584 kB
Step 1 : FROM windowsservercore
---> 5bc36a335344
Step 2 : SHELL powershell -command
---> Running in 87d7a64c9751
---> 4327358436c1
Removing intermediate container 87d7a64c9751
Step 3 : RUN New-Item -ItemType Directory C:\Example
---> Running in 3e6ba16b8df9


Directory: C:\


Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 6/2/2016 2:59 PM Example


---> 1f1dfdcec085
Removing intermediate container 3e6ba16b8df9
Step 4 : ADD Execute-MyCmdlet.ps1 c:\example\
---> 6770b4c17f29
Removing intermediate container b139e34291dc
Step 5 : RUN c:\example\Execute-MyCmdlet -sample 'hello world'
---> Running in abdcf50dfd1f
Hello from Execute-MyCmdlet.ps1 - passed hello world
---> ba0e25255fda
Removing intermediate container abdcf50dfd1f
Successfully built ba0e25255fda
PS E:\docker\build\shell>

SHELL指令还可以用于修改外壳操作的方式。例如,在Windows上使用SHELL cmd /S /C /V:ON|OFF,可以修改延迟的环境变量扩展语义。

SHELL指令也可以在Linux上使用,如果需要一个替代shell,如zshcshtcsh和其他。

SHELL功能在Docker 1.12中添加