Docker教程

百科:

操作系统层虚拟化

Docker

官方:

https://www.docker.com/

https://hub.docker.com/

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

https://docs.docker.com/reference/

教程:

Docker — 从入门到实践

Docker中文指南

Docker 教程 | 菜鸟教程

Docker 命令大全 - 菜鸟教程

阮一峰 Docker 入门教程

“纯洁的微笑” Docker 入门教程: Docker 一Docker 二Docker 三

博文:

docker中宿主机与容器(container)互相拷贝传递文件的方法

Docker容器日志查看与清理

请习惯使用 –helptab 命令补全

官方文档

https://docs.docker.com/get-docker/

镜像

官方镜像

https://download.docker.com/

阿里云 docker-ce镜像

https://mirrors.aliyun.com/docker-ce/

image-20200510171636610

在线安装

https://developer.aliyun.com/mirror/docker-ce

Ubuntu 安装 Docker CE

https://docs.docker.com/engine/install/ubuntu/ 推荐

https://github.com/yeasy/docker_practice/blob/master/install/ubuntu.md

卸载旧版本

旧版本的 Docker 称为 docker 或者 docker-engine,使用以下命令卸载旧版本:

1
2
3
$ sudo apt-get remove docker \
docker-engine \
docker.io

使用 APT 安装

由于 apt 源使用 HTTPS 以确保软件下载过程中不被篡改。因此,我们首先需要添加使用 HTTPS 传输的软件包以及 CA 证书。

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
# 更新apt软件包索引
$ sudo apt-get update
# 由于apt源使用HTTPS以确保软件下载过程中不被篡改。因此,添加使用HTTPS传输的软件包以及CA证书。
$ sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common

# 添加软件源的GPG密钥
$ curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -

# 向source.list中添加Docker软件源。
# 由于我的ubuntu是20.04,当时还没有出对应的Docker软件源,所以使用18.04(版本代号bionic)的软件源
$ sudo add-apt-repository \
"deb [arch=amd64] https://mirrors.aliyun.com/docker-ce/linux/ubuntu \
bionic \
stable"

# 使用官方源
# $ sudo add-apt-repository \
# "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
# $(lsb_release -cs) \
# stable"

# 安装 Docker CE
$ sudo apt-get update
$ sudo apt-get install docker-ce

Centos 安装 Docker CE

https://docs.docker.com/engine/install/centos/ 推荐

https://github.com/yeasy/docker_practice/blob/master/install/centos.md

卸载旧版本

旧版本的 Docker 称为 docker 或者 docker-engine,使用以下命令卸载旧版本:

1
2
3
4
5
6
7
8
9
10
$ sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine

使用 yum 安装

执行以下命令安装依赖包:

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
# 安装依赖包
$ sudo yum install -y yum-utils \
device-mapper-persistent-data \
lvm2

# 添加国内源
$ sudo yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

# 官方源
# $ sudo yum-config-manager \
# --add-repo \
# https://download.docker.com/linux/centos/docker-ce.repo

# 如果需要测试版本的 Docker CE 请使用以下命令
$ sudo yum-config-manager --enable docker-ce-test
# 如果需要每日构建版本的 Docker CE 请使用以下命令
$ sudo yum-config-manager --enable docker-ce-nightly

# 更新yum软件源缓存,并安装docker-ce
$ sudo yum makecache fast
$ sudo yum install docker-ce

# 开启Docker服务
$ sudo service docker start

离线安装

下载软件包

https://linux.cn/article-7937-1.html

Yumdownloader 是一款简单,但是却十分有用的命令行工具,它可以一次性下载任何 RPM 软件包及其所有依赖包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 安装Yumdownloader工具
$ yum install yum-utils

# 添加国内Docker源
$ sudo yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

# 更新yum软件源缓存
$ sudo yum makecache fast

# 下载 Docker CE 和依赖的软件包
$ mkdir -p /opt/dockerInstallPackage
$ cd /opt/dockerInstallPackage
$ yumdownloader docker-ce # 下载一个软件包。默认情况下,Yumdownloader将会下载软件包到当前目录
$ yumdownloader --resolve docker-ce # 同时下载依赖包。注意,本地已经安装的依赖包并不会下载。

# 将所有软件包打包
$ tar cvzf ~/docker_pkg.tar.gz *

安装

1
2
3
4
5
6
7
8
9
10
$ cd /opt/dockerInstallPackage
$ rpm -ivh --replacefiles --replacepkgs *.rpm

# 开机自启动
$ systemctl enable docker.service
# 启动Docker
$ systemctl start docker.service

# 检查版本
$ docker version

镜像加速器

https://github.com/yeasy/docker_practice/blob/master/install/mirror.md

/etc/docker/daemon.json 中写入如下内容:(需保证docker为启动状态)

https://help.aliyun.com/document_detail/60750.html

1
2
3
4
5
6
7
{
"registry-mirrors": [
"https://zofui6f4.mirror.aliyuncs.com",
"https://hub-mirror.c.163.com",
"https://mirror.baidubce.com"
]
}

注意:daemon.json 文件内容不能为空白,否则启动失败 unable to configure the Docker daemon with file /etc/docker/daemon.json: EOF,除非删除文件或文件包含一对大括号 {}

然后重新启动服务:

1
2
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

检查加速器是否生效:

1
2
3
4
5
6
7
$ docker info # 保证docker为启动状态

# 结尾看到如下内容,说明配置成功。
Registry Mirrors:
https://zofui6f4.mirror.aliyuncs.com/
https://hub-mirror.c.163.com/
https://mirror.baidubce.com/

基本概念

Docker 包括三个基本概念

理解了这三个概念,就理解了 Docker 的整个生命周期。

使用镜像

查找镜像

可以从 Docker Hub 网站来搜索镜像,Docker Hub 网址为: https://hub.docker.com/

也可以使用 docker search 命令来搜索镜像

1
$ docker search ubuntu

image-20210329113910157

NAME: 镜像仓库源的名称。

​ 名为 ubuntu 的镜像,是由 Docker 公司创建、验证、支持、提供的基础镜像 (又叫根镜像)。往往使用单个单词作为名字。

​ 名为 dorowu/ubuntu-desktop-lxde-vnc 的镜像,是由 Docker Hub 的注册用户创建并维护的,往往带有用户名称前缀。比如 ansible 用户。

DESCRIPTION: 镜像的描述

STARS: 类似 Github 里面的 star,表示点赞、喜欢的意思

OFFICIAL: 是否 docker 官方发布

AUTOMATED: 自动构建

拉取镜像

1
2
3
4
5
$ docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
$ docker pull --help

$ docker pull ubuntu # 未指定版本,默认使用ubuntu:latest镜像
$ docker pull docker.io/library/ubuntu

具体的选项可以通过 docker pull --help 命令看到,这里我们说一下镜像名称的格式。

  • Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub(docker.io)。
  • 仓库名:如之前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。
1
2
3
4
5
6
7
8
9
10
11
$ docker pull ubuntu:18.04 # 没有给出Docker镜像仓库地址和仓库名,默认docker.io/library/
18.04: Pulling from library/ubuntu
f22ccc0b8772: Pull complete # 镜像是由多层存储所构成,会一层层下载,并给出每一层ID的前12位
3cf8fb62ba5f: Pull complete
e80c964ece6a: Pull complete
Digest: sha256:fd25e706f3dea2a5ff705dbc3353cf37f08307798f3e360a13e9385840f73fb3 # 镜像完整的sha256摘要
Status: Downloaded newer image for ubuntu:18.04
docker.io/library/ubuntu:18.04 # 最后一行输出镜像的完整名称

$ docker pull ubuntu:bionic
$ docker pull ubuntu:21.04

按照上述命令操作时,你看到的层 ID 以及 sha256 摘要和这里的不一样。因为官方镜像一直在维护更新,修复后再以原来的标签发布,确保任何使用这个标签的用户可以获得更安全、更稳定的镜像。

列出镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker image ls --help

Usage: docker image ls [OPTIONS] [REPOSITORY[:TAG]]

List images

Aliases:
ls, list

Options:
-a, --all Show all images (default hides intermediate images)
--digests Show digests
-f, --filter filter Filter output based on conditions provided
--format string Pretty-print images using a Go template
--no-trunc Don't truncate output
-q, --quiet Only show image IDs

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ docker images # 列出所有顶层镜像
$ docker images -a # 列出所有镜像。包括其它镜像所依赖的无标签的"中间层镜像"和"虚悬镜像"
$ docker image ls # 等于 docker images
$ docker image ls -a # 等于 docker images -a
$ docker image ls ubuntu # 根据仓库名列出镜像
$ docker image ls ubuntu:18.04 # 列出特定仓库名和标签的镜像

$ docker image ls -f since=ubuntu:18.04 # 使用过滤器,显示ubuntu:18.04之后建立的镜像。since可换成before
$ docker image ls -q # 只列出镜像ID

# Go的模板语法:自己组织列和标题
$ docker image ls --format "{{.ID}}: {{.Repository}}" # 直接列出镜像结果,并且只包含镜像ID和仓库名
$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" # 以表格等距显示,并且有标题行

--filter 配合 -q 将产生出指定范围的 ID 列表,然后送给另一个 docker 命令作为参数,从而对这组实体批量进行某种操作。这种做法非常常见,不仅仅是镜像,将来我们会在各个命令中看到这类搭配以完成很强大的功能。因此每次在文档看到过滤器后,可以多注意一下它们的用法。

1
2
3
4
5
$ docker image ls -a --digests # 显示摘要
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
ubuntu 21.04 sha256:b6dc45a852dc83fa0e7504e9d68b9b0084eefb8aeb5f295f276bf99f5c033490 de35fa744ddc 4 months ago 79.6MB
ubuntu 18.04 sha256:fd25e706f3dea2a5ff705dbc3353cf37f08307798f3e360a13e9385840f73fb3 2c047404e52d 4 months ago 63.3MB
ubuntu bionic sha256:fd25e706f3dea2a5ff705dbc3353cf37f08307798f3e360a13e9385840f73fb3 2c047404e52d 4 months ago 63.3MB

各个选项说明:

  • **REPOSITORY:**镜像的仓库名
  • **TAG:**镜像的标签
  • **IMAGE ID:**镜像ID
  • **CREATED:**镜像创建时间
  • **SIZE:**镜像大小

同一仓库名可以有多个 TAG,代表这个仓库源的不同个版本,使用 REPOSITORY:TAG 来定义不同的镜像。

镜像 ID 是镜像的唯一标识,一个镜像可以对应多个 标签ubuntu:18.04ubuntu:bionic 拥有相同的 ID,因为它们对应的是同一个镜像。

镜像体积

1、本地标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。因为 Docker Hub 中显示的体积是压缩后的体积,而 docker image ls 显示的是镜像下载到本地后展开的大小,准确说,是展开后各层所占空间的总和

2、docker image ls 列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多

1
2
3
4
5
6
$ docker system df # 查看镜像、容器、数据卷所占用的空间
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 2 0 142.8MB 142.8MB (100%)
Containers 0 0 0B 0B
Local Volumes 0 0 0B 0B
Build Cache 0 0 0B 0B

虚悬镜像

由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none> 的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image)

1
2
3
4
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
mongo 3.2 fe9198c04d62 5 days ago 342 MB
<none> <none> 00285df0df87 5 days ago 342 MB

这个镜像原本是有镜像名和标签的,原来为 mongo:3.2,随着官方镜像维护更新,重新 docker pull mongo:3.2 时,mongo:3.2 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 <none>docker build 也同样可以导致这种现象。

1
2
3
4
$ docker image ls -f dangling=true # 列出虚悬镜像(dangling image)
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB
$ docker image prune # 删除虚悬镜像(已无价值)

既没有仓库名,也没有标签,均为 <none> 的镜像,不一定是虚悬镜像,也有可能是中间层镜像。

中间层镜像

为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像

与虚悬镜像不同,这些无标签的镜像是中间层镜像,是其它镜像所依赖的镜像。不应该删除,否则会导致上层镜像因为依赖丢失而出错。也没必要删除,相同的层只会存一遍。

只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除。

运行容器

使用版本为18.04的ubuntu系统镜像来运行容器,命令如下:

1
2
$ docker run -it --rm ubuntu:18.04 bash # 指定版本标签。当在本地主机上使用一个不存在的镜像时Docker就会自动下载这个镜像。
$ cat /etc/os-release # Linux 常用的查看当前系统版本的命令

docker run 就是运行容器的命令,这里简要的说明一下上面用到的参数:

参数说明:

  • -i: 交互式操作,让容器的标准输入保持打开。
  • -t: 让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上。
  • –rm: 容器退出或停止运行后随之将其删除。默认情况下,退出的容器并不会立即删除,除非手动 docker rm
  • ubuntu:18.04: 这是指用 ubuntu 18.04 版本镜像为基础来启动容器。
  • /bin/bash:放在镜像名后的是 命令,这里我们希望有个交互式 Shell,因此用的是 bash

退出终端或容器,ctrl+d 或 输入exit

删除镜像

1
2
3
4
5
6
7
8
9
10
11
12
docker image rm --help

Usage: docker image rm [OPTIONS] IMAGE [IMAGE...]

Remove one or more images

Aliases:
rm, rmi, remove

Options:
-f, --force Force removal of the image
--no-prune Do not delete untagged parents

示例:

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
$ docker rmi ubuntu:15.10 # 根据标签删除

$ docker image rm -f 镜像短ID/镜像长ID/镜像名/镜像摘要

$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 18.04 4e5021d210f6 12 days ago 64.2MB
ubuntu latest 4e5021d210f6 12 days ago 64.2MB
ubuntu 16.04 77be327e4b63 5 weeks ago 124MB
$ docker image rm 4e5021d210f6 # 根据镜像ID删除
Error response from daemon: conflict: unable to delete 4e5021d210f6 (must be forced) - image is referenced in multiple repositories
$ docker image rm ubuntu:18.04 # 根据标签删除
Untagged: ubuntu:18.04
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 4e5021d210f6 12 days ago 64.2MB
ubuntu 16.04 77be327e4b63 5 weeks ago 124MB
$ docker image rm 4e5021d210f6 # 根据镜像ID删除
Untagged: ubuntu:latest
Untagged: ubuntu@sha256:bec5a2727be7fff3d308193cfde3491f8fba1a2ba392b7546b43a051853a341d
Deleted: sha256:4e5021d210f65ebe915670c7089120120bc0a303b90208592851708c1b8c04bd
Deleted: sha256:1d9112746e9d86157c23e426ce87cc2d7bced0ba2ec8ddbdfbcc3093e0769472
Deleted: sha256:efcf4a93c18b5d01aa8e10a2e3b7e2b2eef0378336456d8653e2d123d6232c1e
Deleted: sha256:1e1aa31289fdca521c403edd6b37317bf0a349a941c7f19b6d9d311f59347502
Deleted: sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 16.04 77be327e4b63 5 weeks ago 124MB

# 更精确的是使用 "镜像摘要" 删除镜像
$ docker image ls --digests
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
ubuntu 16.04 sha256:e9938f45e51d9ff46e2b05a62e0546d0f07489b7f22fbc5288defe760599e38a 77be327e4b63 5 weeks ago 124MB
$ docker image rm ubuntu@sha256:e9938f45e51d9ff46e2b05a62e0546d0f07489b7f22fbc5288defe760599e38a
Untagged: ubuntu@sha256:e9938f45e51d9ff46e2b05a62e0546d0f07489b7f22fbc5288defe760599e38a
$ docker image ls --digests # 删除后发现,镜像还在,但摘要信息没有了
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
ubuntu 16.04 <none> 77be327e4b63 5 weeks ago 124MB

Untagged 和 Deleted

删除行为分为两类,一类是 Untagged,另一类是 Deleted

镜像的唯一标识是其 ID (或摘要)。当一个镜像 ID 对应多个标签时,根据 ID 删除镜像不会成功,需要先取消镜像的标签,这就是看到的 Untagged 的信息。只要还有其他标签指向这个镜像,那么 Delete 行为就不会发生。所以并非所有的 docker image rm 都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。

当镜像的所有标签都被取消了,它也就失去了存在的意义,便会出发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层

除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。容器是以镜像为基础,再加一层容器存储层,组成多层存储结构运行。如果该镜像被容器所依赖,那么删除该镜像必然会导致故障。应该先删除容器,再删除镜像。

批量删除

使用 docker image ls -q 配合使用 docker image rm

1
2
3
$ docker image rm $(docker image ls -q redis) # 删除所有仓库名为 redis 的镜像

$ docker image rm $(docker image ls -q -f before=ubuntu:16.04) # 删除所有在ubuntu:16.04之前的镜像

构建镜像

镜像是容器的基础,每次执行 docker run 的时候都会指定哪个镜像作为容器运行的基础。当来自于 Docker Hub 的镜像无法满足需求时,就需要定制镜像。

两种方式:

  • 修改已创建的容器(修改容器的存储层),并提交。
  • 使用 Dockerfile 指令来创建一个新的镜像。

利用 commit 理解镜像构成

注意:如果您是初学者,您可以暂时跳过后面的内容,直接学习 容器 一节。

注意: docker commit 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 docker commit 定制镜像,定制镜像应该使用 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
$ docker run --name webserver -d -p 80:80 nginx # 用nginx镜像启动一个容器,命名为webserver
$ docker exec -it webserver bash # 进入容器
# 修改主页内容,修改了容器的文件,也就是改动了容器的存储层
root@ff8700bb348f:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@ff8700bb348f:/# exit
exit
$ docker diff webserver # 查看具体改动
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
C /root
A /root/.bash_history
C /run
A /run/nginx.pid

现在我们定制好了变化,我们希望能将其保存下来形成镜像。

当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。

Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。

docker commit 的语法格式为:

1
docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

将容器保存为镜像:

1
2
3
4
5
6
7
8
9
$ docker commit --help

$ docker commit \
--message "修改了默认网页" \
--author "zhaolq <hello@zhaolq.com>" \
webserver \
nginx:v2

$ docker commit -m="修改了默认网页" -a="zhaolq <hello@zhaolq.com>" ff8700bb348f zhaolq/nginx:v2

各个参数说明:

  • -m: 提交的描述信息
  • -a: 指定镜像作者
  • ff8700bb348f: 容器 ID,指定要提交的容器。也可以使用容器的名称webserver。
  • zhaolq/nginx:v2: 指定要创建的目标镜像的 repository 和 tag。

查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ docker image ls nginx # 列出新定制的镜像
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx v2 57e58770b1dc 12 seconds ago 133MB
nginx latest f6d0b4767a6c 2 months ago 133MB
$ docker history nginx:v2 # 查看镜像内的历史记录
IMAGE CREATED CREATED BY SIZE COMMENT
57e58770b1dc About a minute ago nginx -g daemon off; 1.3kB 修改了默认网页
f6d0b4767a6c 2 months ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
<missing> 2 months ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
<missing> 2 months ago /bin/sh -c #(nop) EXPOSE 80 0B
<missing> 2 months ago /bin/sh -c #(nop) ENTRYPOINT ["/docker-entr… 0B
<missing> 2 months ago /bin/sh -c #(nop) COPY file:0fd5fca330dcd6a7… 1.04kB
<missing> 2 months ago /bin/sh -c #(nop) COPY file:0b866ff3fc1ef5b0… 1.96kB
<missing> 2 months ago /bin/sh -c #(nop) COPY file:e7e183879c35719c… 1.2kB
<missing> 2 months ago /bin/sh -c set -x && addgroup --system -… 63.7MB
<missing> 2 months ago /bin/sh -c #(nop) ENV PKG_RELEASE=1~buster 0B
<missing> 2 months ago /bin/sh -c #(nop) ENV NJS_VERSION=0.5.0 0B
<missing> 2 months ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.19.6 0B
<missing> 2 months ago /bin/sh -c #(nop) LABEL maintainer=NGINX Do… 0B
<missing> 2 months ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 months ago /bin/sh -c #(nop) ADD file:422aca8901ae3d869… 69.2MB
$ docker run --name web2 -d -p 81:80 zhaolq/nginx:v2 # 使用新定制的镜像运行一个容器

至此,我们完成了定制镜像,使用 docker commit 命令手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储有了更直观的感觉。

慎用 docker commit

使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。

首先,如果仔细观察之前的 docker diff webserver 的结果,会发现除了真正想要修改的 /usr/share/nginx/html/index.html 文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,将会导致镜像极为臃肿

此外,使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为 黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体的操作。这种黑箱镜像的维护工作是非常痛苦的。

镜像使用的是分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。这会让镜像更加臃肿。

使用 Dockerfile 构建镜像

从刚才的 docker commit 的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

编写Dockerfile

Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

还以定制 nginx 镜像为例,这次使用 Dockerfile 定制。

1
2
3
4
5
6
$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile
$ vim Dockerfile
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
Ubuntu的Dockerfile

https://hub.docker.com/_/ubuntu

FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。

FROM 指定 基础镜像,一个 DockerfileFROM必备指令,并且必须是第一条指令(有异议,因为ARG指令可以放到FROM前面)。

Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginxredismongomysqlhttpdphptomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 nodeopenjdkpythonrubygolang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntudebiancentosfedoraalpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。

Docker 还存在一个特殊的镜像,名为 scratch 。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像

1
2
FROM scratch # 表示一个空白的镜像,不以任何镜像为基,接下来所写的指令将作为镜像第一层开始存在。
...

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令

RUN 指令是用来执行命令行命令的,其格式有两种:

  • shell 格式RUN <命令>,就像直接在命令行中输入的命令一样。
1
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:

1
2
3
4
5
6
7
8
9
FROM debian:stretch

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install

Dockerfile 中每一个指令都会建立一层,RUN 也不例外。

每一个 RUN 的行为:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。

Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。

Dockerfile 正确的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM debian:stretch

RUN buildDeps='gcc libc6-dev make wget' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
&& rm -rf /var/lib/apt/lists/* \
&& rm redis.tar.gz \
&& rm -r /usr/src/redis \
&& apt-get purge -y --auto-remove $buildDeps

首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这应该是一层的事情。

这不是在写 Shell 脚本,而是在定义每一层该如何构建。

Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。

每一层构建的最后一定要清理掉无关文件,避免镜像臃肿 。这是很重要的一步,因为镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加 真正需要添加的东西,任何无关的东西都应该在每一层构建的最后清理掉

构建镜像

docker build 构建

格式:docker build [选项] <上下文路径/URL/->

Dockerfile 文件所在目录执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker build --help

$ docker build -t nginx:v3 . # 注意这里的"."表示上下文路径。未指定Dockerfile文件时,默认将上下文路径下名为Dockerfile的文件作为Dockerfile
$ docker build -t nginx:v3 -f /home/mynginx/Dockerfile /home/mynginx/ # 指定Dockerfile

Sending build context to Docker daemon 2.048kB # 发送上下文内容
Step 1/2 : FROM nginx
---> f6d0b4767a6c # nginx:latest镜像ID
Step 2/2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
---> Running in 12e58f75c0a9 # 启动一个容器执行所要求的命令(echo命令)
Removing intermediate container 12e58f75c0a9 # 移除中间容器。疑问:感觉应该先提交再删除,不咋清楚为啥会这样?
---> 24a53bbd421a # 提交这一层,最后的镜像ID为24a53bbd421a
Successfully built 24a53bbd421a
Successfully tagged nginx:v3

$ docker run --name web3 -d -p 80:80 nginx:v3
镜像构建上下文(Context)

docker build 命令最后有一个 . 表示当前目录。并不是指定 Dockerfile 路径,而是指定上下文路径,其中包括构建镜像所需的一切文件。docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

docker build 的工作原理:Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。

docker build 命令构建镜像,其实是在 Docker 引擎中构建(服务端)。引入上下文概念,在构建时,用户会指定构建镜像上下文的路径, docker build 命令得知这个路径,会将路径下的所有内容打包,上传给 Docker 引擎,Docker 引擎展开这个上下文包就会获得构建镜像所需的一切文件。这样我们就可以通过 COPY 指令、ADD 指令将这些本地文件复制进镜像。

1
COPY ./package.json /app/ # 复制 上下文(context) 目录下的 package.json 文件到 /app 目录

注意:

​ 千万不要将硬盘根目录用做上下文路径,如果那样,会让 docker build 打包整个硬盘,发送一个几十 GB 的东西给 Docker 引擎,极为缓慢而且很容易构建失败,这显然是错误使用。如果目录下有些东西确实不希望构建时传给 Docker 引擎,可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。可以用 -f ../MyDockerfile.php 参数指定某个文件作为 Dockerfile。当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。

镜像层数
1
2
3
4
$ vim Dockerfile
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
COPY ./package.json /app/ # 复制 上下文(context) 目录下的 package.json

利用上述 Dockerfile 构建镜像会建立三层(三个)镜像:

​ 1、官方的 nginx 镜像。
​ 2、RUN 指令镜像。
​ 3、COPY 指令镜像。

3层镜像依赖2层镜像,2层镜像依赖1层镜像。

删除3层镜像时,会触发依赖镜像的删除(1、2层镜像),但是实际上1层镜像并没有删除,因为还有标签指向该镜像,那就是官方的标签 nginx:latest 。如果先取消官方镜像的标签,再删除3层镜像,1、2层镜像(依赖镜像)就会跟随一起删除。

从URL构建

1、用 Git repo 构建

1
$ docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world

这行命令指定了构建所需的 Git repo、分支 master,构建目录 /amd64/hello-world/,然后 Docker 就会自己去 git clone 这个项目、切换到指定分支、并进入到指定目录后开始构建。

2、用给定的 tar 压缩包构建

1
$ docker build http://server/context.tar.gz

如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。

从标准输入构建

1、从标准输入中读取 Dockerfile 进行构建

1
docker build - < Dockerfile

1
cat Dockerfile | docker build -

如果标准输入传入的是文本文件,则将其视为 Dockerfile,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile 的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 COPY 进镜像之类的事情。

2、从标准输入中读取上下文压缩包进行构建

1
$ docker build - < context.tar.gz

如果发现标准输入的文件格式是 gzipbzip2 以及 xz 的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。

Dockerfile 指令详解

我们已经介绍了 FROMRUN,还提及了 COPY, ADD,其实 Dockerfile 功能很强大,它提供了十多个指令。

COPY 复制文件

格式:

  • COPY [--chown=<user>:<group>] <源路径>... <目标路径>
  • COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。

COPY 指令将从构建上下文目录中 <源路径> 的文件或目录复制到新的一层的镜像内的 <目标路径> 位置。比如:

1
COPY package.json /usr/src/app/

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

1
2
3
4
COPY hom* /mydir/
COPY hom?.txt /mydir/
COPY a.txt b.txt c.txt /mydir/ # 将前三个文件复制到/mydir/目录下
COPY nginx-1.18.0.tar.gz /mydir/ # 不会自动解压,ADD指定可以自动解压部分格式

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留,比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

1
2
3
4
COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/

如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。

ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

比如 <源路径> 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令会自动解压缩这个压缩文件到 <目标路径> 去。

在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu 中:

1
2
3
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD 命令了。

在 Docker 官方的 Dockerfile 最佳实践文档 中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。

另外需要注意的是,ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。

因此在 COPYADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

1
2
3
4
ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/

CMD 容器启动命令

CMD 指令的格式和 RUN 相似,也是两种格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
  • 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

CMD 指令如果写了多次,只有最后一个生效。

之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数CMD 指令就是用于指定容器主进程的启动命令

在运行时可以指定新的命令来替代镜像中设置的默认命令

1
2
$ docker run -it ubuntu # ubuntu镜像默认的CMD是/bin/bash,所以会直接进入bash
$ docker run -it ubuntu cat /etc/os-release # 用`cat /etc/os-release`命令替换默认的`/bin/bash`命令,输出系统版本信息

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

1
CMD echo $HOME

在实际执行中,会将其变更为:

1
CMD [ "sh", "-c", "echo $HOME" ]

sh命令

环境变量会被 shell 进行解析处理

前台执行和后台执行的问题

Docker 不是虚拟机,容器中的应用都应该以前台执行。而不是像虚拟机、物理机里面那样,用 systemd 去启动后台服务,容器内没有后台服务的概念

一些初学者将 CMD 写为:

1
CMD service nginx start

然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。

对于容器而言,其启动程序就是容器主进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

而使用 service nginx start 命令,则是希望 upstart 来以后台守护进程形式启动 nginx 服务。而 CMD service nginx start 会被理解为 CMD [ "sh", "-c", "service nginx start"],因此主进程实际上是 sh。当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

1
CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT 入口点

入口点

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。

ENTRYPOINT 在运行时也可以替代,通过 docker run 的参数 --entrypoint 来指定。

当指定 ENTRYPOINTCMD 不再直接运行其命令,CMD 的内容将作为参数传给 ENTRYPOINT 指令。换句话说实际执行时,将变为:

1
<ENTRYPOINT> "<CMD>"

CMD 指令如果写了多次,只有最后一个生效。

那么有了 CMD 后,为什么还要有 ENTRYPOINT 呢?这种 <ENTRYPOINT> "<CMD>" 有什么好处么?让我们来看几个场景。

场景一:让镜像变成像命令一样使用

假设需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

1
2
3
4
5
FROM ubuntu:18.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://myip.ipip.net" ]

假如使用 docker build -t myip:v1 . 来构建镜像的话,如果需要查询当前公网 IP,只需要执行:

1
2
$ docker run --rm myip:v1
当前 IP:8.129.8.11 来自于:中国 广东 深圳 阿里云/电信/联通/移动/教育网

CMD 中可以看到实质的命令是 curl,那么如果希望显示 HTTP 头信息,就需要加上 -i 参数。

1
2
$ docker run --rm myip:v1 -i
docker: Error response from daemon: OCI runtime create failed: container_linux.go:367: starting container process caused: exec: "-i": executable file not found in $PATH: unknown.

可以看到可执行文件找不到的报错。 CMD 容器启动命令 说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s http://myip.ipip.net 后面。而 -i 根本不是命令,所以自然找不到。

1
$ docker run myip curl -s http://myip.ipip.net -i # 这显然不是很好的解决方案

使用 ENTRYPOINT 可以解决这个问题,重新用 ENTRYPOINT 来实现这个镜像:

1
2
3
4
5
FROM ubuntu:18.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]

再次使用 docker run --rm myip:v1 -i 就成功了。因为当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到了我们预期的效果。

场景二:应用运行前的准备工作

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。

比如 mysql 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。

此外,可能希望避免使用 root 用户去启动服务,从而提高安全性,而在启动服务前还需要以 root 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root 身份执行,方便调试等。

这些准备工作是和容器 CMD 无关的,无论 CMD 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 <CMD>)作为命令,在脚本最后执行。比如官方镜像 redis 中就是这么做的:

1
2
3
4
5
6
7
8
FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]

可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINTdocker-entrypoint.sh 脚本。进入redis容器可查看全部 docker-entrypoint.sh 脚本内容如下:

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

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

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

exec "$@"

Shell内建命令

docker与gosu

https://github.com/tianon/gosu

Linux set命令 - 菜鸟教程

Shell 传递参数 - 菜鸟教程

Shell 基本运算符 - 菜鸟教程

Linux id命令 - 菜鸟教程

该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行。比如:

1
2
$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)

ENV 设置环境变量

格式有两种:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,还是运行时的应用,都可以直接使用这里定义的环境变量。

1
2
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"

ARG 构建参数

格式:ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时不会存在这些环境变量。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

灵活的使用 ARG 指令,能够在不修改 Dockerfile 的情况下,构建出不同的镜像。

ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中。

1
2
3
4
5
6
7
8
9
10
# 只在 FROM 中生效
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine

# 要想在 FROM 之后使用,必须再次指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}

# 后面的 FROM 指令可以使用${DOCKER_USERNAME}
FROM ${DOCKER_USERNAME}/alpine

VOLUME 挂载为匿名卷

docker volumes 中 -v 和 -mount 区别

https://docs.docker.com/storage/volumes/

格式为:

  • VOLUME ["<路径1>", "<路径2>"...]
  • VOLUME <路径>

容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。

为了防止运行时用户忘记将动态文件所保存目录挂载为卷Dockerfile,可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载也不会向容器存储层写入大量数据

1
VOLUME /data # 容器内数据卷的路径

这里的 /data 目录就会在容器运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。

由于匿名挂载的时候只指定了 容器内数据卷的路径,那么到底挂载到 宿主机 的哪个路径,可以使用以下命令查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d9ec1b9f548f b29265c0674f "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 0.0.0.0:8889->6379/tcp magical_kepler
$ docker inspect d9ec1b9f548f
[
{
...... # 省略
# 注意:Mounts是一个数组,因为可以挂载多个匿名卷
"Mounts": [
{
"Type": "volume",
"Name": "45e207ba329fbd2f7d09ced2bc474c9204a5a6747a72560a4faf7783761a936a",
"Source": "/var/lib/docker/volumes/45e207ba329fbd2f7d09ced2bc474c9204a5a6747a72560a4faf7783761a936a/_data",
"Destination": "/data",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
]
...... #省略
}
]

Mounts 中可以看到 DestinationSource 分别就是 容器内数据卷路径 和 宿主机容器卷路径。

–运行容器时挂载–

匿名挂载(匿名卷):在进行数据卷挂载的时候不指定 宿主机数据卷的路径-v 命令之后直接跟上 容器内数据卷的路径会覆盖 VOLUME 定义的匿名卷。

具名挂载(命名卷):在进行数据卷挂载的时候既指定 宿主机数据卷的路径,又指定 容器内数据卷的路径

1
2
3
4
5
6
# 匿名挂载(匿名卷),会覆盖 VOLUME 定义的匿名卷。
$ docker run -d -v /data IMAGE_ID

# 具名挂载(命名卷), -v 宿主机数据卷的路径:容器内数据卷的路径
$ docker run -d -v /home/mydata:/data IMAGE_ID
$ docker run -d -p 6379:6379 -v /home/mydata:/data IMAGE_ID

在这行命令中,就使用了 /home/mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷挂载配置。

EXPOSE 暴露端口

格式为 EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。

在 Dockerfile 中写入这样的声明有两个好处:

  • 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;
  • 在运行时使用随机端口映射时,也就是 docker run -P 时(P 大写),会自动随机映射 EXPOSE 的端口。

EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口>区别

-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问。而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

下面使用随机宿主端口映射到 EXPOSE 暴露的端口:

1
2
3
4
5
6
7
8
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis 6.2.1 b29265c0674f 3 days ago 105MB
$ docker run -d -P --name=redis redis:6.2.1
02873235266e4f0574c5a1376f92ca0afe8ac7c1065c975d21ab4e8ebab1793f
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
02873235266e redis:6.2.1 "docker-entrypoint.s…" 4 seconds ago Up 3 seconds 0.0.0.0:49154->6379/tcp redis

WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录做为以后各层的当前目录,如该目录不存在,WORKDIR 会帮助建立目录。

1
2
RUN cd /app
RUN echo "hello" > world.txt

以上 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件。原因很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。

每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

改变以后各层的工作目录的位置:

1
2
3
WORKDIR /app

RUN echo "hello" > world.txt

如果 WORKDIR 指令使用的相对路径,那么所切换的路径与之前的 WORKDIR 有关:

1
2
3
4
5
WORKDIR /a
WORKDIR b
WORKDIR c

RUN pwd

RUN pwd 的工作目录为 /a/b/c

USER 指定当前用户

格式:USER <用户名>[:<用户组>]

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

注意,USER 只是帮助切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

1
2
3
RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu,阅读本文 ENTRYPOINT 入口点 - 场景二 以更好的了解gosu。

1
2
3
4
5
6
7
8
# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]

HEALTHCHECK 健康检查

格式:

  • HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令。命令格式分为 shell 格式和 exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。
  • HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

CMD, ENTRYPOINT 一样,HEALTHCHECK 如果写了多个,只有最后一个生效。

HEALTHCHECK 指令是告诉 Docker 怎样判断容器状态是否正常。这是 Docker 1.12 引入的新指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否正常,从而真实的反应容器实际状态。

三种状态:

  • starting:初始状态
  • healthy:检查成功
  • unhealthy:连续失败指定次数后视为失败

HEALTHCHECK 支持下列选项:

  • --interval=<检查间隔>:两次健康检查的间隔,默认为 30 秒;
  • --timeout=<检查命令运行时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
  • --retries=<重试次数>:当连续失败指定次数后,则将容器状态视为 unhealthy默认 3 次

假设有个最简单的 Web 服务镜像,希望增加健康检查来判断其 Web 服务是否在正常工作,可以用 curl 来帮助判断,其 DockerfileHEALTHCHECK 写法:

1
2
3
4
FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
CMD curl -fs http://localhost/ || exit 1

这里设置了使用 curl -fs http://localhost/ || exit 1 作为健康检查命令,每 5 秒检查一次,健康检查命令超过 3 秒没响应就视为失败。

构建镜像:

1
$ docker build -t myweb:v1 .

启动容器:

1
$ docker run -d --name web -p 80:80 myweb:v1

查看状态:

1
2
3
4
5
6
7
8
9
10
# 初始状态为 (health: starting)
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ba6fc1e74956 myweb:v1 "/docker-entrypoint.…" 2 seconds ago Up 1 second (health: starting) 0.0.0.0:8080->80/tcp web
# 等待几秒钟后,再次查看,健康状态变为 (healthy)
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ba6fc1e74956 myweb:v1 "/docker-entrypoint.…" 8 seconds ago Up 7 seconds (healthy) 0.0.0.0:8080->80/tcp web

# 如果健康检查连续失败超过了重试次数,状态就会变为 (unhealthy)。

为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ docker inspect --format '{{json .State.Health}}' web | python -m json.tool
{
"FailingStreak": 0,
"Log": [
{
"End": "2021-03-31T14:56:59.605914326+08:00",
"ExitCode": 0,
"Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
"Start": "2021-03-31T14:56:59.532338081+08:00"
}
],
"Status": "healthy"
}

ONBUILD 为他人作嫁衣裳

格式:ONBUILD <其它指令>

ONBUILD 是一个特殊的指令,后面跟其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。

示例:https://github.com/yeasy/docker_practice/blob/master/image/dockerfile/onbuild.md

LABEL 为镜像添加元数据

LABEL 指令用来给镜像以键值对的形式添加一些元数据(metadata)。

1
LABEL <key>=<value> <key>=<value> <key>=<value> ...

我们还可以用一些标签来申明镜像的作者、文档地址等:

1
2
3
LABEL org.opencontainers.image.authors="zhaolq"

LABEL org.opencontainers.image.documentation="https://blog.zhaolq.com/"

具体可以参考 https://github.com/opencontainers/image-spec/blob/master/annotations.md

SHELL 指令

格式:SHELL ["executable", "parameters"]

1
2
3
4
5
6
7
8
SHELL` 指令可以指定 `RUN` `ENTRYPOINT` `CMD` 指令的 shell,Linux 中默认为 `["/bin/sh", "-c"]
SHELL ["/bin/sh", "-c"]

RUN lll ; ls

SHELL ["/bin/sh", "-cex"]

RUN lll ; ls

两个 RUN 运行同一命令,第二个 RUN 运行的命令会打印出每条命令并当遇到错误时退出。

ENTRYPOINT CMD 以 shell 格式指定时,SHELL 指令所指定的 shell 也会成为这两个指令的 shell

1
2
3
4
5
6
7
8
SHELL ["/bin/sh", "-cex"]

# /bin/sh -cex "nginx"
ENTRYPOINT nginx
SHELL ["/bin/sh", "-cex"]

# /bin/sh -cex "nginx"
CMD nginx

参考文档

多阶段构建

构建多种系统架构支持的镜像

其它制作镜像的方式

除了标准的使用 Dockerfile 生成镜像的方法外,由于各种特殊需求和历史原因,还提供了一些其它方法用以生成镜像。

从 rootfs(跟文件系统) 压缩包导入

请阅读下文中 导出和导入容器 小节。

Docker 镜像的导入和导出

docker savedocker load 命令,用以将镜像保存为归档文件,然后传输到另一个服务器上,再加载进来。这是在没有 Docker Registry 时的做法,现在已经不推荐,镜像迁移应该使用 Docker Registry,无论是直接使用 Docker Hub 还是使用内网私有 Registry 都可以。

docker save 导出镜像存储文件(将镜像保存为归档文件):

1
2
3
4
5
6
7
$ docker images ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 21.04 de35fa744ddc 4 months ago 79.6MB
ubuntu 18.04 2c047404e52d 4 months ago 63.3MB
ubuntu bionic 2c047404e52d 4 months ago 63.3MB
$ docker save ubuntu:21.04 -o ubuntu-21.04.tar # 可以是任意后缀名,但文件的本质都是归档文件
$ docker save ubuntu:21.04 | gzip > ubuntu-21.04.tar # 使用 gzip 压缩,大大节省了占用空间

注意:如果同名则会覆盖(没有警告)

 docker load 导入镜像存储文件到本地镜像库

1
2
$ docker load -i ubuntu-21.04.tar
Loaded image: ubuntu:21.04

如果我们结合这两个命令以及 ssh 甚至 pv 的话,利用 Linux 强大的管道,我们可以写一个命令完成从一个机器将镜像迁移到另一个机器,并且带进度条的功能:

1
$ docker save <镜像名> | bzip2 | pv | ssh <用户名>@<主机名> 'cat | docker load'

操作容器

启动容器

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(exited)的容器重新启动。

新建并启动

阅读上文 使用镜像 - 运行容器

当利用 docker run 来创建容器时,Docker 在后台运行的标准操作包括:

  • 检查本地是否存在指定的镜像,不存在就从 registry 下载
  • 利用镜像创建并启动一个容器
  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  • 从地址池配置一个 ip 地址给容器
  • 执行用户指定的应用程序
  • 执行完毕后容器被终止

启动已终止的容器

docker container start

终止容器

docker container stop

此外,当 Docker 容器中指定的应用终结时,容器也自动终止。

docker container restart 命令会将一个运行态的容器终止,然后再重新启动它。

后台运行

如果不使用 -d 参数运行容器,容器会把输出的结果 (STDOUT) 打印到宿主机上。

1
2
3
4
5
$ docker run --name helloworld --rm ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
hello world
hello world
hello world
...

如果使用 -d 参数运行容器,容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上(输出结果可以用 docker logs 查看)。

1
2
$ docker run --name helloworld -d --rm ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
21d72e9bc931a4565a29c2490d1f6ba621bbc9fad6585fbe72b69638ac582138

注: 容器是否会长久运行,是和 docker run 指定的命令有关,和 -d 参数无关。

使用 -d 参数启动后会返回一个唯一的 id,也可以通过 docker container ls 命令来查看容器信息。

1
2
3
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
21d72e9bc931 ubuntu:18.04 "/bin/sh -c 'while t…" 17 seconds ago Up 16 seconds helloworld

通过 docker container logs 命令获取容器的输出信息:

1
2
3
4
$ docker container logs [container ID or NAMES]
hello world
hello world
...

进入容器

有些时候需要进入正在后台运行的容器进行操作,可以使用 docker attachdocker exec 命令。

推荐使用 docker exec ,因为从这个 stdin 中 exit,不会导致容器停止。

attach 命令

1
2
3
4
5
6
7
8
9
10
11
$ docker run -dit ubuntu:18.04
5a8e0c1b1bc28abe0f6067872abb19af9c86418e0836facd486e190904c2275f
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5a8e0c1b1bc2 ubuntu:18.04 "/bin/bash" 5 seconds ago Up 5 seconds elated_lederberg
$ docker attach 5a8e
root@5a8e0c1b1bc2:/# exit
exit
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5a8e0c1b1bc2 ubuntu:18.04 "/bin/bash" 50 seconds ago Exited (0) 7 seconds ago elated_lederberg

注意: 如果从这个 stdin 中 exit,会导致容器停止。

exec 命令

只用 -i 参数时,由于没有分配伪终端,界面没有 Linux 命令提示符,但命令执行结果仍然可以返回。Tab 键不会自动填充。

-i -t 参数一起使用时,界面有 Linux 命令提示符Tab会自动填充

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker run -dit ubuntu:18.04
5a8e0c1b1bc28abe0f6067872abb19af9c86418e0836facd486e190904c2275f
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5a8e0c1b1bc2 ubuntu:18.04 "/bin/bash" 5 seconds ago Up 5 seconds elated_lederberg

$ docker exec -i 5a8e bash
ls
bin
boot
dev
...

$ docker exec -it 5a8e bash
root@5a8e0c1b1bc2:/#

导出和导入容器

导出容器快照

docker export 将容器的文件系统导出为 tar 存档到当前目录无论容器是否运行

1
2
3
4
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ff8700bb348f nginx "/docker-entrypoint.…" 2 days ago Up 2 days 0.0.0.0:80->80/tcp webserver
$ docker export ff8700bb348f > nginx.tar

导入容器快照

docker import 导入容器快照到本地镜像库

1
2
3
4
5
6
7
8
$ docker import nginx.tar test/nginx:v1.0 # 从本地获取文件并导入
$ docker import http://IP地址/nginx.tar.gz test/nginx:v1.0 # 从远程Web获取文件并导入
$ cat nginx.tar | docker import - test/nginx:v1.0 # 从标准输入中获取文件并导入

sha256:ab49349d50dc20f3ded114a8242ab94a283c9c5f93c84c049b60bc5b6afaa3aa
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test/nginx v1.0 ab49349d50dc 9 seconds ago 131MB

注:

用户既可以使用 docker load导入镜像存储文件到本地镜像库,也可以使用 docker import导入容器快照到本地镜像库

两者区别在于:

1、归档文件不同。docker load 用来导入由 docker save 导出的镜像存储文件docker import 用来导入由 docker export 导出的容器快照

2、文件大小不同。容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态);镜像存储文件将保存完整记录,体积也要大。

3、从容器快照文件导入时可以重新指定标签等元数据信息。

相同点:

​ 都是导入到本地镜像库。

​ 即便原来的镜像有依赖,导出再导入后,都只有一个镜像。

删除容器

1
2
3
4
$ docker container rm [container ID or NAMES] # 删除一个处于终止状态的容器
$ docker rm [container ID or NAMES] # 简写

$ docker rm -f [container ID or NAMES] # 删除一个运行中的容器

清理所有处于终止状态的容器:

1
$ docker container prune

访问仓库

仓库(Repository)是集中存放镜像的地方。

注册服务器(Registry)是管理仓库的具体服务器,每个服务器上可以有多个仓库,而每个仓库下面有多个镜像。

从这方面来说,仓库可以被认为是一个具体的项目或目录。例如对于仓库地址 docker.io/ubuntu 来说,docker.io 是注册服务器地址,ubuntu 是仓库名。

Docker Hub

登录登出

docker login YourDomainName OR HostIP

docker logout

推送镜像

docker push

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 18.04 2c047404e52d 4 months ago 63.3MB
$ docker tag ubuntu:18.04 hellozhaolq/ubuntu:18.04 # 必须重打标签,否则没有权限。hellozhaolq为Docker账号用户名
$ docker images
hellozhaolq/ubuntu 18.04 2c047404e52d 4 months ago 63.3MB
ubuntu 18.04 2c047404e52d 4 months ago 63.3MB
$ docker push hellozhaolq/ubuntu:18.04
The push refers to repository [docker.io/hellozhaolq/ubuntu]
fe6d8881187d: Pushed
23135df75b44: Pushed
b43408d5f11b: Pushed
18.04: digest: sha256:a7fa45fb43d471f4e66c5b53b1b9b0e02f7f1d37a889a41bbe1601fac70cb54e size: 943
$ docker search hellozhaolq
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
# 搜索不到,不知为何,但是可以pull

自动构建

网络钩子 - 维基百科

自动构建(Automated Builds)功能对于需要经常升级镜像内程序来说,十分方便。

有时候,用户构建了镜像,安装了某个软件,当软件发布新版本则需要手动更新镜像。

而自动构建允许用户通过 Docker Hub 指定跟踪一个目标网站(支持 GitHubBitBucket)上的项目,一旦项目发生新的提交 (commit)或者创建了新的标签(tag),Docker Hub 会自动构建镜像并推送到 Docker Hub 中。

1、创建 GitHub 仓库: docker-automated-builds-test ,包含 Dockerfile 文件:

1
2
3
4
FROM ubuntu:18.04
COPY EchoContent /home/
WORKDIR /home/
CMD cat EchoContent

image-20210402145935031

2、Docker Hub 关联账户。在 Docker Hub 上点击右上角头像,在账号设置(Account Settings)中关联(Linked Accounts)目标网站。例如关联GitHub,成功关联后会在 Acccount settings - Applications 看到授权的OAuth应用 Docker Hub Builder

3、创建 Docker Hub 仓库: docker-automated-builds-test

4、配置自动构建。在 Docker Hub 仓库 Builds 选项卡中选取一个目标网站中的项目,指定 Dockerfile 的位置,最后点击 Sava And Build 自动构建。这时,目标网站仓库的 settings 中已经配置了 WebhooksDeploy keys

image-20210402153201618

5、查看构建结果:

image-20210402154047211

配置完成之后,一旦GitHub仓库中的文件有更新,Docker Hub上的镜像构建就会自动触发(使用Webhooks),从而保证镜像始终都是最新的。

私有仓库 docker-registry

请使用 Nexus3.x 的私有仓库

docker-registry 是官方提供的工具,可以用于构建私有的镜像仓库(本地仓库)。本文内容基于 docker-registry v2.x 版本。

安装运行

使用官方的 registry 镜像来启动私有仓库:

1
$ docker run -d -p 5000:5000 --restart=always --name registry registry

默认情况下,仓库会被创建在容器的 /var/lib/registry 目录下,可以通过 -v 参数来将仓库路径挂载到本地的 /opt/data/registry 目录:

1
2
3
4
$ docker run -d \
-p 5000:5000 \
-v /opt/data/registry:/var/lib/registry \
registry

推送镜像到本地仓库

例如私有仓库地址为 127.0.0.1:5000,需先使用 docker tag 来标记一个镜像,然后就可以推送到仓库了。

1
2
3
4
5
6
7
8
9
$ docker images -a
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest f643c72bc252 4 months ago 72.9MB
$ docker tag ubuntu:latest 127.0.0.1:5000/ubuntu:latest # 将ubuntu:latest这个镜像标记为127.0.0.1:5000/ubuntu:latest
$ docker image ls -a
REPOSITORY TAG IMAGE ID CREATED SIZE
127.0.0.1:5000/ubuntu latest f643c72bc252 4 months ago 72.9MB
ubuntu latest f643c72bc252 4 months ago 72.9MB
$ docker push 127.0.0.1:5000/ubuntu:latest # 推送标记的镜像

可以看到 {"repositories":["ubuntu"]},表明镜像已经被成功推送了。

搜索本地仓库镜像

浏览器访问 http://127.0.0.1:5000/v2/_catalog 或使用命令 curl

1
2
$ curl 127.0.0.1:5000/v2/_catalog # 查看仓库中的镜像
{"repositories":["ubuntu"]}

拉取本地仓库镜像

先删除已有镜像,再尝试从私有仓库中拉取这个镜像。

1
2
$ docker rmi 127.0.0.1:5000/ubuntu:latest
$ docker pull 127.0.0.1:5000/ubuntu:latest

配置非 https 仓库地址

若想让本网段的其他主机也能把镜像推送到私有仓库,就要使用例如 172.27.13.27:5000 这样的内网地址做为私有仓库地址:

1
2
3
4
$ docker tag nginx:latest 172.27.13.27:5000/nginx:latest
$ docker push 172.27.13.27:5000/nginx:latest
The push refers to repository [172.27.13.27:5000/nginx]
Get https://172.27.13.27:5000/v2/: http: server gave HTTP response to HTTPS client

会发现推送失败。这是因为 Docker 默认不允许非 HTTPS 方式推送镜像。可以通过 Docker 的配置选项来取消这个限制,或者查看下一节配置能够通过 HTTPS 访问的私有仓库。

对于使用 systemd 的系统,请在 /etc/docker/daemon.json 中写入如下内容(如果文件不存在请新建该文件)

1
2
3
4
5
6
7
8
9
10
{
"registry-mirrors": [
"https://zofui6f4.mirror.aliyuncs.com",
"https://hub-mirror.c.163.com",
"https://mirror.baidubce.com"
],
"insecure-registries": [
"172.27.13.27:5000"
]
}

注意:该文件必须符合 json 规范,否则 Docker 将不能启动。

然后重新启动服务:

1
2
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

检查是否生效:

1
2
3
4
5
6
$ docker info # 保证docker为启动状态

# 结尾看到如下内容,说明配置成功。
Insecure Registries:
172.27.13.27:5000
127.0.0.0/8

私有仓库高级配置

https://github.com/yeasy/docker_practice/blob/master/repository/registry_auth.md

Nexus3.x 的私有仓库

https://www.sonatype.com/product/repository-oss

使用 Docker 官方的 Registry 创建的仓库面临一些维护问题。比如某些镜像删除以后空间默认不会回收,需要一些命令去回收空间然后重启 Registry。在企业中把内部的一些工具包放入 Nexus 中是比较常见的做法,最新版本 Nexus3.x 全面支持 Docker 的私有镜像。所以使用 Nexus3.x 一个软件来管理 Docker , Maven , Yum , PyPI 等是一个明智的选择。

启动 Nexus 容器

参考 https://hub.docker.com/r/sonatype/nexus3

1
2
3
4
5
$ docker volume create --name nexus-data # 创建一个数据卷,在 /var/lib/docker/volumes 下
$ docker run -d -p 8081:8081 -p 5001:5001 \
--name nexus3 \
-v nexus-data:/nexus-data \
sonatype/nexus3

这里的 5001 端口映射出来供后面使用。

首次运行需等待 3-5 分钟。

查看滚动日志,输出以下内容表示启动成功,可以使用浏览器打开 http://YourIP:8081 访问 Nexus

1
2
3
4
5
6
7
8
$ docker logs -f nexus3

2021-04-06 04:27:00,455+0000 INFO [jetty-main-1] *SYSTEM org.sonatype.nexus.bootstrap.jetty.JettyServer -
-------------------------------------------------

Started Sonatype Nexus OSS 3.29.2-02

-------------------------------------------------

获取初始密码 (默认帐号是 admin ),首次登录会提示更改初始密码:

1
$ docker exec nexus3 cat /nexus-data/admin.password

登录后,点击页面上方的齿轮按钮 image-20210406135103964 按照下面的方法进行设置。

创建仓库

创建一个私有仓库的方法: Repository->Repositories 点击右边菜单 Create repository 选择 docker (hosted)

  • Name: 仓库的名称
  • HTTP: 仓库单独的访问端口(例如:5001记得添加端口映射
  • Hosted -> Deployment pollcy: 请选择 Allow redeploy 否则无法上传 Docker 镜像。

其它的仓库创建方法请各位自己摸索,还可以创建一个 docker (proxy) 类型的仓库链接到 DockerHub 上。再创建一个 docker (group) 类型的仓库把刚才的 hostedproxy 添加在一起。主机在访问的时候默认下载私有仓库中的镜像,如果没有将链接到 DockerHub 中下载并缓存到 Nexus 中。

注意:这里的 5001 端口需要对外映射,否则后面无法使用 docker login HostIP:5001 登录,最简单的方式就是删除容器,重新启动一个。由于已经挂载了 /nexus-data 目录,所以一切设置都不会丢失。更复杂操作参考 docker容器添加对外映射端口

添加访问权限

菜单 Security->Realms 把 Docker Bearer Token Realm 移到右边的框中保存。

添加角色

菜单 Security->Roles->Create rolePrivlleges 选项搜索 docker 把相应的规则移动到右边的框中然后保存。

添加用户

菜单 Security->Users->Create local userRoles 选项中选中刚才创建的规则移动到右边的窗口保存。

NGINX 加密代理

证书的生成请参见 私有仓库高级配置 里面证书生成一节。

NGINX 示例配置如下

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
upstream register
{
server "YourHostName OR IP":5001; # 端口为上面添加私有镜像仓库时设置的 HTTP 选项的端口号
check interval=3000 rise=2 fall=10 timeout=1000 type=http;
check_http_send "HEAD / HTTP/1.0\r\n\r\n";
check_http_expect_alive http_4xx;
}

server {
server_name YourDomainName; # 如果没有DNS服务器做解析,请删除此选项使用本机IP地址访问
listen 443 ssl;

ssl_certificate key/example.crt;
ssl_certificate_key key/example.key;

ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
large_client_header_buffers 4 32k;
client_max_body_size 300m;
client_body_buffer_size 512k;
proxy_connect_timeout 600;
proxy_read_timeout 600;
proxy_send_timeout 600;
proxy_buffer_size 128k;
proxy_buffers 4 64k;
proxy_busy_buffers_size 128k;
proxy_temp_file_write_size 512k;

location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://register;
proxy_read_timeout 900s;

}
error_page 500 502 503 504 /50x.html;
}

访问镜像仓库

如果不启用 SSL 加密可以通过 前面章节 的方法添加非 https 仓库地址到 Docker 的配置文件中然后重启 Docker。

使用 SSL 加密以后程序需要访问就不能采用修改配置的方式了。具体方法如下:

1
2
3
$ openssl s_client -showcerts -connect YourDomainName OR HostIP:443 </dev/null 2>/dev/null|openssl x509 -outform PEM >ca.crt
$ cat ca.crt | sudo tee -a /etc/ssl/certs/ca-certificates.crt
$ systemctl restart docker

使用 docker login YourDomainName OR HostIP 进行测试,用户名密码填写上面 Nexus 中设置的。

数据管理

docker volumes 中 -v 和 -mount 区别

https://docs.docker.com/storage/volumes/

这一章介绍如何在 Docker 内部以及容器之间管理数据,在容器中管理数据主要有两种方式:

  • 数据卷(Volumes)
  • 挂载主机目录 (Bind mounts)

数据卷

数据卷一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性

  • 数据卷 可以在容器之间共享和重用
  • 数据卷 的修改会立马生效
  • 数据卷 的更新,不会影响镜像
  • 数据卷 默认会一直存在,即使容器被删除

注意:数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会复制到数据卷中(仅数据卷为空时会复制)。

创建一个数据卷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ docker volume create my-vol
$ docker volume ls # 查看所有数据卷
DRIVER VOLUME NAME
local my-vol
local nexus-data

$ docker volume inspect my-vol # 查看指定数据卷的信息
[
{
"CreatedAt": "2021-04-06T16:10:26+08:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
"Name": "my-vol",
"Options": {},
"Scope": "local"
}
]

启动一个挂载数据卷的容器

docker run 命令时,使用 --mount 标记来将 数据卷 挂载到容器里。一次 docker run 中可以挂载多个 数据卷

下面创建一个名为 web 的容器,并加载一个 数据卷 到容器的 /usr/share/nginx/html 目录。

1
2
3
4
5
$ docker run -d -P \
--name web \
# -v my-vol:/usr/share/nginx/html \
--mount source=my-vol,target=/usr/share/nginx/html \
nginx:alpine

查看数据卷的具体信息

1
$ docker inspect web

数据卷 的信息在 “Mounts” Key 下面

1
2
3
4
5
6
7
8
9
10
11
12
"Mounts": [
{
"Type": "volume",
"Name": "my-vol",
"Source": "/var/lib/docker/volumes/my-vol/_data",
"Destination": "/usr/share/nginx/html",
"Driver": "local",
"Mode": "z",
"RW": true,
"Propagation": ""
}
]

删除数据卷

1
$ docker volume rm my-vol

数据卷 是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷。如果需要在删除容器的同时移除数据卷,使用 docker rm -v 命令

1
$ docker rm -fv web # 本人测试无效

无主的数据卷可能会占据很多空间,要清理请使用以下命令

1
$ docker volume prune

挂载主机目录

挂载一个主机目录作为数据卷

使用 --mount 标记可以指定挂载一个本地主机的目录到容器中去。

1
2
3
4
5
6
# 加载主机的 /src/webapp 目录到容器的 /usr/share/nginx/html目录
$ docker run -d -P \
--name web \
# -v /src/webapp:/usr/share/nginx/html \
--mount type=bind,source=/src/webapp,target=/usr/share/nginx/html \
nginx:alpine

这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。

本地目录的路径必须是绝对路径,使用 -v 参数时 Docker 会自动创建文件夹,使用 --mount 参数时如果本地目录不存在,Docker 会报错。

Docker 挂载主机目录的默认权限是 读写。用户可以通过增加 readonly 指定为 只读

1
2
3
4
5
6
7
8
9
10
11
$ docker run -d -P \
--name web \
# -v /src/webapp:/usr/share/nginx/html:ro \ # ro是readonly简写
--mount type=bind,source=/src/webapp,target=/usr/share/nginx/html,readonly \
nginx:alpine

# 这时如果在容器内 /usr/share/nginx/html 目录新建文件,会显示如下错误
[root@1da1fc9f10f4 html]# pwd
/usr/share/nginx/html
[root@1da1fc9f10f4 html]# touch new.txt
touch: new.txt: Read-only file system

查看数据卷的具体信息

1
$ docker inspect web

挂载主机目录 的信息在 “Mounts” Key 下面

1
2
3
4
5
6
7
8
9
10
"Mounts": [
{
"Type": "bind",
"Source": "/src/webapp",
"Destination": "/usr/share/nginx/html",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
]

挂载一个本地主机文件作为数据卷

--mount 标记也可以从主机挂载单个文件到容器中

1
2
3
4
5
6
7
8
9
$ docker run --rm -it \
# -v $HOME/.bash_history:/root/.bash_history \
--mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \
ubuntu:18.04 \
bash

root@2affd44b4667:/# history
1 ls
2 diskutil list

这样就可以记录容器输入过的命令了。

使用网络

Docker 允许通过外部访问容器或容器互联的方式来提供网络服务。

外部访问容器

容器中可以运行一些网络应用,要让外部也可以访问这些应用,可以通过 -P-p 参数来指定端口映射。

当使用 -P 标记时,Docker 会随机映射一个端口到内部容器开放的网络端口。使用 docker container ls 可以看到随机端口。docker logs 查看访问记录。

-p 则可以指定要映射的端口,并且,在一个指定端口上只可以绑定一个容器。支持的格式有 ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort

映射所有接口地址

使用 hostPort:containerPort 格式将本地 80 端口映射到容器 80 端口:

1
$ docker run -d -p 80:80 nginx:alpine

此时默认会绑定本地所有接口上的所有地址。

映射到指定地址的指定端口

使用 ip::containerPort 绑定 localhost 的任意端口到容器的 80 端口,本地主机会自动分配一个端口。

1
$ docker run -d -p 127.0.0.1::80 nginx:alpine

还可以使用 udp 标记来指定 udp 端口

1
$ docker run -d -p 127.0.0.1:80:80/udp nginx:alpine

查看映射端口配置

1
2
$ docker port fa 80
0.0.0.0:32768

注意:

  • 容器有自己的内部网络和 ip 地址(使用 docker inspect 查看,Docker 还可以有一个可变的网络配置。)
  • -p 标记可以多次使用来绑定多个端口

例如

1
2
3
4
$ docker run -d \
-p 80:80 \
-p 443:443 \
nginx:alpine

容器互联

如果有 Docker 使用经验,可能已经习惯了使用 --link 参数来使容器互联。

随着 Docker 网络的完善,强烈建议大家将容器加入自定义的 Docker 网络来连接多个容器,而不是使用 --link 参数。

新建网络

1
2
# 创建一个新的 Docker 网络
$ docker network create -d bridge my-net

-d 参数指定 Docker 网络类型,有 bridge overlay。其中 overlay 网络类型用于 Swarm mode,在本小节中可以忽略它。

连接容器

运行一个容器并连接到新建的 my-net 网络

1
$ docker run -it --rm --name busybox1 --network my-net busybox sh

打开新的终端,再运行一个容器并加入到 my-net 网络

1
$ docker run -it --rm --name busybox2 --network my-net busybox sh

再打开一个新的终端查看容器信息

1
2
3
4
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b47060aca56b busybox "sh" 11 minutes ago Up 11 minutes busybox2
8720575823ec busybox "sh" 16 minutes ago Up 16 minutes busybox1

下面通过 ping 来证明 busybox1 容器和 busybox2 容器建立了互联关系。

busybox1 容器输入以下命令

1
2
3
4
/ # ping busybox2
PING busybox2 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.072 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.118 ms

同理在 busybox2 容器执行 ping busybox1,也会成功连接到。

这样,busybox1 容器和 busybox2 容器建立了互联关系。

Docker Compose

如果有多个容器之间需要互相连接,推荐使用 Docker Compose

配置 DNS

如何自定义配置容器的主机名和 DNS 呢?秘诀就是 Docker 利用虚拟文件来挂载容器的 3 个相关配置文件。

在容器中使用 mount 命令可以看到挂载信息:

1
2
3
4
$ mount
/dev/sda1 on /etc/resolv.conf type ext3 (rw,seclabel,relatime,data=ordered)
/dev/sda1 on /etc/hostname type ext3 (rw,seclabel,relatime,data=ordered)
/dev/sda1 on /etc/hosts type ext3 (rw,seclabel,relatime,data=ordered)

这种机制可以让宿主主机 DNS 信息发生更新后,所有 Docker 容器的 DNS 配置通过 /etc/resolv.conf 文件立刻得到更新。

配置全部容器的 DNS ,也可以在 /etc/docker/daemon.json 文件中增加以下内容来设置。

1
2
3
4
5
6
{
"dns" : [
"114.114.114.114",
"114.114.115.115"
]
}

这样每次启动的容器 DNS 自动配置为 114.114.114.114114.114.115.115。使用以下命令来证明其已经生效。

1
2
3
$ docker run -it --rm ubuntu:18.04  cat etc/resolv.conf
nameserver 114.114.114.114
nameserver 8.8.8.8

如果用户想要手动指定容器的配置,可以在使用 docker run 命令启动容器时加入如下参数:

-h HOSTNAME 或者 --hostname=HOSTNAME 设定容器的主机名,它会被写到容器内的 /etc/hostname/etc/hosts。但它在容器外部看不到,既不会在 docker container ls 中显示,也不会在其他的容器的 /etc/hosts 看到。

--dns=IP_ADDRESS 添加 DNS 服务器到容器的 /etc/resolv.conf 中,让容器用这个服务器来解析所有不在 /etc/hosts 中的主机名。

--dns-search=DOMAIN 设定容器的搜索域,当设定搜索域为 .example.com 时,在搜索一个名为 host 的主机时,DNS 不仅搜索 host,还会搜索 host.example.com

注意:如果在容器启动时没有指定最后两个参数,Docker 会默认用主机上的 /etc/resolv.conf 来配置容器。

高级网络配置

注意:本章属于 Docker 高级配置,初学者可以暂时跳过本章节,直接学习 Docker Compose 一节。

当 Docker 启动时,会自动在主机上创建一个 docker0 虚拟网桥,实际上是 Linux 的一个 bridge,可以理解为一个软件交换机。它会在挂载到它的网口之间进行转发。

同时,Docker 随机分配一个本地未占用的私有网段(在 RFC1918 中定义)中的一个地址给 docker0 接口。比如典型的 172.17.42.1,掩码为 255.255.0.0。此后启动的容器内的网口也会自动分配一个同一网段(172.17.0.0/16)的地址。

当创建一个 Docker 容器的时候,同时会创建了一对 veth pair 接口(当数据包发送到一个接口时,另外一个接口也可以收到相同的数据包)。这对接口,一端在容器内,即 eth0;另一端在本地并被挂载到 docker0 网桥,名称以 veth 开头(例如 vethAQI2QT)。通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。Docker 就创建了在主机和所有容器之间一个虚拟共享网络。

Docker 网络

接下来的部分将介绍在一些场景中,Docker 所有的网络定制配置,以及通过 Linux 命令来调整、补充、甚至替换 Docker 默认的网络配置。

快速配置指南

下面是一个跟 Docker 网络相关的命令列表。

其中有些命令选项只有在 Docker 服务(即 dockerd --help )启动的时候才能配置,而且不能马上生效。

  • -b BRIDGE--bridge=BRIDGE 指定容器挂载的网桥
  • --bip=CIDR 定制 docker0 的掩码
  • -H SOCKET...--host=SOCKET... Docker 服务端接收命令的通道
  • --icc=true|false 是否支持容器之间进行通信
  • --ip-forward=true|false 请看下文容器之间的通信
  • --iptables=true|false 是否允许 Docker 添加 iptables 规则
  • --mtu=BYTES 容器网络中的 MTU

下面2个命令选项既可以在启动服务时指定,也可以在启动容器时指定。在 Docker 服务(即 dockerd --help )启动的时候指定则会成为默认值,后面执行 docker run 时可以覆盖设置的默认值。

  • --dns=IP_ADDRESS... 使用指定的DNS服务器
  • --dns-search=DOMAIN... 指定DNS搜索域

最后这些选项只有在 docker run 执行时使用,因为它是针对容器的特性内容。

  • -h HOSTNAME--hostname=HOSTNAME 配置容器主机名
  • --link=CONTAINER_NAME:ALIAS 添加到另一个容器的连接
  • --net=bridge|none|container:NAME_or_ID|host 配置容器的桥接模式
  • -p SPEC--publish=SPEC 映射容器端口到宿主主机
  • -P or --publish-all=true|false 映射容器所有端口到宿主主机

容器访问控制

容器的访问控制,主要通过 Linux 上的 iptables 防火墙来进行管理和实现。iptables 是 Linux 上默认的防火墙软件,在大部分发行版中都自带。

容器访问外部网络

容器要想访问外部网络,需要本地系统的转发支持。在Linux 系统中,检查转发是否打开。

1
2
$sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

如果为 0,说明没有开启转发,则需要手动打开。

1
$sysctl -w net.ipv4.ip_forward=1

如果在启动 Docker 服务(即 dockerd --help )的时候设定 --ip-forward=true, Docker 就会自动设定系统的 ip_forward 参数为 1。

容器之间访问

容器之间相互访问,需要两方面的支持。

  • 容器的网络拓扑是否已经互联。默认情况下,所有容器都会被连接到 docker0 网桥上。
  • 本地系统的防火墙软件 – iptables 是否允许通过。

访问所有端口

当启动 Docker 服务(即 dockerd --help )的时候,默认会添加一条转发策略到本地主机 iptables 的 FORWARD 链上。策略为通过(ACCEPT)还是禁止(DROP)取决于配置--icc=true(缺省值)还是 --icc=false。当然,如果手动指定 --iptables=false 则不会添加 iptables 规则。

可见,默认情况下,不同容器之间是允许网络互通的。如果为了安全考虑,可以在 /etc/docker/daemon.json 文件中配置 {"icc": false} 来禁止它。

访问指定端口

在通过 -icc=false 关闭网络访问后,还可以通过 --link=CONTAINER_NAME:ALIAS 选项来访问容器的开放端口。

例如,在启动 Docker 服务(即 dockerd --help )时,可以同时使用 icc=false --iptables=true 参数来关闭允许相互的网络访问,并让 Docker 可以修改系统中的 iptables 规则。

此时,系统中的 iptables 规则可能是类似

1
2
3
4
5
6
$ sudo iptables -nL
...
Chain FORWARD (policy ACCEPT)
target prot opt source destination
DROP all -- 0.0.0.0/0 0.0.0.0/0
...

之后,启动容器(docker run)时使用 --link=CONTAINER_NAME:ALIAS 选项。Docker 会在 iptable 中为 两个容器分别添加一条 ACCEPT 规则,允许相互访问开放的端口(取决于 Dockerfile 中的 EXPOSE 指令)。

当添加了 --link=CONTAINER_NAME:ALIAS 选项后,添加了 iptables 规则。

1
2
3
4
5
6
7
$ sudo iptables -nL
...
Chain FORWARD (policy ACCEPT)
target prot opt source destination
ACCEPT tcp -- 172.17.0.2 172.17.0.3 tcp spt:80
ACCEPT tcp -- 172.17.0.3 172.17.0.2 tcp dpt:80
DROP all -- 0.0.0.0/0 0.0.0.0/0

注意:--link=CONTAINER_NAME:ALIAS 中的 CONTAINER_NAME 目前必须是 Docker 分配的名字,或使用 --name 参数指定的名字。主机名则不会被识别。

映射容器端口到宿主主机的实现

默认情况下,容器可以主动访问到外部网络的连接,但是外部网络无法访问到容器。

容器访问外部实现

容器所有到外部网络的连接,源地址都会被 NAT 成本地系统的 IP 地址。这是使用 iptables 的源地址伪装操作实现的。

查看主机的 NAT 规则。

1
2
3
4
5
6
$ sudo iptables -t nat -nL
...
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 !172.17.0.0/16
...

其中,上述规则将所有源地址在 172.17.0.0/16 网段,目标地址为其他网段(外部网络)的流量动态伪装为从系统网卡发出。

MASQUERADE 跟传统 SNAT 的好处是它能动态从网卡获取地址。

外部访问容器实现

容器允许外部访问,可以在 docker run 时候通过 -p-P 参数来启用。

不管用那种办法,其实也是在本地的 iptable 的 nat 表中添加相应的规则。

使用 -P 时:

1
2
3
4
5
$ iptables -t nat -nL
...
Chain DOCKER (2 references)
target prot opt source destination
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:49153 to:172.17.0.2:80

使用 -p 80:80 时:

1
2
3
4
$ iptables -t nat -nL
Chain DOCKER (2 references)
target prot opt source destination
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80

注意:

  • 这里的规则映射了 0.0.0.0,意味着将接受主机来自所有接口的流量。用户可以通过 -p IP:host_port:container_port-p IP::port 来指定允许访问容器的主机上的 IP、接口等,以制定更严格的规则。
  • 如果希望永久绑定到某个固定的 IP 地址,可以在 Docker 配置文件 /etc/docker/daemon.json 中添加如下内容。
1
2
3
{
"ip": "0.0.0.0"
}

配置 docker0 网桥

Docker 服务(即 dockerd --help )默认会创建一个 docker0 网桥(其上有一个 docker0 内部接口),它在内核层连通了其他的物理或虚拟网卡,这就将所有容器和本地主机都放到同一个物理网络。

Docker 默认指定了 docker0 接口 的 IP 地址和子网掩码,让主机和容器之间可以通过网桥相互通信,它还给出了 MTU(接口允许接收的最大传输单元),通常是 1500 Bytes(或宿主主机网络路由上支持的默认值)。这些值都可以在服务启动的时候进行配置。

  • --bip=CIDR IP 地址加掩码格式,例如 192.168.1.5/24
  • --mtu=BYTES 覆盖默认的 Docker mtu 配置

也可以在配置文件中配置 DOCKER_OPTS,然后重启服务。

由于目前 Docker 网桥是 Linux 网桥,用户可以使用 brctl show 来查看网桥和端口连接信息。

1
2
3
4
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02428680967e no veth7a59900
veth7bd0625

注:brctl 命令需要安装网桥管理工具包 bridge-utils,Debian、Ubuntu 使用 sudo apt-get install bridge-utils 来安装,Red Hat、CentOS 使用 yum install bridge-utils -y

每次创建一个新容器的时候,Docker 从可用的地址段中选择一个空闲的 IP 地址分配给容器的 eth0 端口。使用本地主机上 docker0 接口的 IP 作为所有容器的默认网关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ docker run -dit --name centos centos:centos8
$ docker exec -it centos bash
[root@32bdc88226eb /]# ip addr show eth0
27: eth0@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:05 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.5/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
[root@32bdc88226eb /]# ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
27: eth0@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:05 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.5/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
[root@32bdc88226eb /]# ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.5

自定义网桥

Linux ip 命令 - 菜鸟教程

除了默认的 docker0 网桥,用户也可以指定网桥来连接各个容器。

在启动 Docker 服务(即 dockerd --help )的时候,使用 -b BRIDGE--bridge=BRIDGE 来指定使用的网桥。

如果服务已经运行,那需要先停止服务,并删除旧的网桥。

1
2
3
4
$ sudo systemctl stop docker
$ brctl show
$ sudo ip link set dev docker0 down
$ sudo brctl delbr docker0

然后创建一个网桥 bridge0

1
2
3
$ sudo brctl addbr bridge0
$ sudo ip addr add 192.168.5.1/24 dev bridge0
$ sudo ip link set dev bridge0 up

查看确认网桥创建并启动。

1
2
3
4
5
$ ip addr show bridge0
4: bridge0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state UP group default
link/ether 66:38:d0:0d:76:18 brd ff:ff:ff:ff:ff:ff
inet 192.168.5.1/24 scope global bridge0
valid_lft forever preferred_lft forever

在 Docker 配置文件 /etc/docker/daemon.json 中添加如下内容,即可将 Docker 默认桥接到创建的网桥上。

1
2
3
{
"bridge": "bridge0",
}

然后重新启动 Docker 服务:

1
2
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

新建一个容器,可以看到它已经桥接到了 bridge0 上。

在容器中可以使用 ip addrip route 命令来查看 IP 地址配置和路由信息。

问题

系统重启网桥 bridge0 就不存在了,导致docker服务无法启动。如何持久化网桥?

编辑网络配置文件

Docker 1.2.0 开始支持在运行中的容器里编辑 /etc/hosts, /etc/hostname/etc/resolv.conf 文件。

但是这些修改是临时的,只在运行的容器中保留,容器终止或重启后并不会被保存下来,也不会被 docker commit 提交。

创建一个点到点连接

Docker Buildx

https://github.com/yeasy/docker_practice/blob/master/buildx/README.md

Docker Buildx 是一个 docker CLI 插件,其扩展了 docker 命令,支持 Moby BuildKit 提供的功能。提供了与 docker build 相同的用户体验,并增加了许多新功能。

该功能仅适用于 Docker v19.03+ 版本

Docker Compose

Docker Compose 是 Docker 官方编排(Orchestration)项目之一,负责快速的部署分布式应用。

Compose 简介

Compose 项目是 Docker 官方的开源项目,负责实现对 Docker 容器集群的快速编排。从功能上看,跟 OpenStack 中的 Heat 十分类似。

其代码目前在 https://github.com/docker/compose 上开源。

Compose 定位是 「定义和运行多个 Docker 容器的应用(Defining and running multi-container Docker applications)」,其前身是开源项目 Fig。

通过第一部分中的介绍,我们知道使用一个 Dockerfile 模板文件,可以让用户很方便的定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。

Compose 恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。

Compose 中有两个重要的概念:

  • 服务 (service):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。
  • 项目 (project):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义。

Compose 的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。

Compose 项目由 Python 编写,实现上调用了 Docker 服务提供的 API 来对容器进行管理。因此,只要所操作的平台支持 Docker API,就可以在其上利用 Compose 来进行编排管理。

版本

对于 Docker Engine、Docker Compose、Compose file 三者的版本关系有点模糊。

https://docs.docker.com/engine/release-notes/

https://docs.docker.com/compose/release-notes/

Compose file format 版本和 Docker Engine 版本对应关系:

https://docs.docker.com/compose/compose-file/compose-versioning/

安装与卸载

Compose 支持 Linux、macOS、Windows 10 三大平台。

Compose 可以通过 Python 的包管理工具 pip 进行安装,也可以直接下载编译好的二进制文件使用,甚至能够直接在 Docker 容器中运行。

Docker Desktop for Mac/Windows 自带 docker-compose 二进制文件,安装 Docker 之后可以直接使用。

1
2
$ docker-compose --version
docker-compose version 1.27.4, build 40524192

Linux 系统请使用以下介绍的方法安装。

二进制包

在 Linux 上的也安装十分简单,从 官方 GitHub Release 处直接下载编译好的二进制文件即可。

例如,在 Linux 64 位系统上直接下载对应的二进制包:

1
2
3
4
5
6
$ sudo curl -L https://github.com/docker/compose/releases/download/${version}/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

# 国内用户可以使用以下方式加快下载
$ sudo curl -L https://download.fastgit.org/docker/compose/releases/download/${version}/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

$ sudo chmod +x /usr/local/bin/docker-compose

PIP 安装

注: x86_64 架构的 Linux 建议按照上边的方法下载二进制包进行安装,如果您计算机的架构是 ARM (例如,树莓派),再使用 pip 安装。

这种方式是将 Compose 当作一个 Python 应用来从 pip 源中安装。

执行安装命令:

1
$ sudo pip install -U docker-compose

可以看到类似如下输出,说明安装成功。

1
2
3
4
Collecting docker-compose
Downloading docker-compose-1.27.4.tar.gz (149kB): 149kB downloaded
...
Successfully installed docker-compose cached-property requests texttable websocket-client docker-py dockerpty six enum34 backports.ssl-match-hostname ipaddress

bash 补全命令

1
$ curl -L https://raw.githubusercontent.com/docker/compose/1.27.4/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose

卸载

如果是二进制包方式安装的,删除二进制文件即可。

1
$ sudo rm /usr/local/bin/docker-compose

如果是通过 pip 安装的,则执行如下命令即可删除。

1
$ sudo pip uninstall docker-compose

使用

术语

首先介绍几个术语。

  • 服务 (service):一个应用容器,实际上可以运行多个相同镜像的实例。
  • 项目 (project):由一组关联的应用容器组成的一个完整业务单元。

可见,一个项目可以由多个服务(容器)关联而成,Compose 面向项目进行管理。

场景

最常见的项目是 web 网站,该项目应该包含 web 应用和缓存。

下面我们用 Python 来建立一个能够记录页面访问次数的 web 网站。

web 应用

新建文件夹,在该目录中编写 app.py 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
count = redis.incr('hits')
return 'Hello World! 该页面已被访问 {} 次。\n'.format(count)

if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)

Dockerfile

编写 Dockerfile 文件,内容为

1
2
3
4
5
FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD ["python", "app.py"]

docker-compose.yml

编写 docker-compose.yml 文件,这个是 Compose 使用的主模板文件。

1
2
3
4
5
6
7
8
9
10
version: '3'
services:

web:
build: .
ports:
- "5000:5000"

redis:
image: "redis:alpine"

运行 compose 项目

1
2
$ docker-compose up
$ docker-compose up -d # 后台运行

此时访问本地 5000 端口,每次刷新页面,计数就会加 1。

Compose 命令说明

命令对象与格式

对于 Compose 来说,大部分命令的对象既可以是项目本身,也可以指定为项目中的服务或者容器。如果没有特别的说明,命令对象将是项目,这意味着项目中所有的服务都会受到命令影响。

执行 docker-compose [COMMAND] --help 或者 docker-compose help [COMMAND] 可以查看具体某个命令的使用格式。

docker-compose 命令的基本的使用格式是

1
docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]

命令选项

  • -f, --file FILE 指定使用的 Compose 模板文件,默认为 docker-compose.yml,可以多次指定。
  • -p, --project-name NAME 指定项目名称,默认将使用所在目录名称作为项目名。
  • --verbose 输出更多调试信息。
  • -v, --version 打印版本并退出。

命令使用说明

help

获得一个命令的帮助。

version

格式为 docker-compose version

打印版本信息。

config

验证 Compose 文件格式是否正确,若正确则显示配置,若格式错误显示错误原因。

build

格式为 docker-compose build [options] [SERVICE...]

构建(重新构建)项目中的服务容器。

服务容器一旦构建后,将会带上一个标记名,例如对于 web 项目中的一个 db 容器,可能是 web_db。

可以随时在项目目录下运行 docker-compose build 来重新构建服务。

选项包括:

  • --force-rm 删除构建过程中的临时容器。
  • --no-cache 构建镜像过程中不使用 cache(这将加长构建过程)。
  • --pull 始终尝试通过 pull 来获取更新版本的镜像。

up

格式为 docker-compose up [options] [SERVICE...]

该命令十分强大,它将尝试自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一系列操作。

链接的服务都将会被自动启动,除非已经处于运行状态。

可以说,大部分时候都可以直接通过该命令来启动一个项目。

默认情况,docker-compose up 启动的容器都在前台,控制台将会同时打印所有容器的输出信息,可以很方便进行调试。

当通过 Ctrl-C 停止命令时,所有容器将会停止。

如果使用 docker-compose up -d,将会在后台启动并运行所有的容器。一般推荐生产环境下使用该选项。

默认情况,如果服务容器已经存在,docker-compose up 将会尝试停止容器,然后重新创建(保持使用 volumes-from 挂载的卷),以保证新启动的服务匹配 docker-compose.yml 文件的最新内容。如果用户不希望容器被停止并重新创建,可以使用 docker-compose up --no-recreate。这样将只会启动处于停止状态的容器,而忽略已经运行的服务。如果用户只想重新部署某个服务,可以使用 docker-compose up --no-deps -d <SERVICE_NAME> 来重新创建服务并后台停止旧服务,启动新服务,并不会影响到其所依赖的服务。

选项:

  • -d 在后台运行服务容器。
  • --no-color 不使用颜色来区分不同的服务的控制台输出。
  • --no-deps 不启动服务所链接的容器。
  • --force-recreate 强制重新创建容器,不能与 --no-recreate 同时使用。
  • --no-recreate 如果容器已经存在了,则不重新创建,不能与 --force-recreate 同时使用。
  • --no-build 不自动构建缺失的服务镜像。
  • -t, --timeout TIMEOUT 停止容器时候的超时(默认为 10 秒)。

down

此命令将会停止 up 命令所启动的容器,并移除网络。

run

格式为 docker-compose run [options] [-p PORT...] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]

在指定服务上执行一个命令。

选项:

  • -d 后台运行容器。
  • --name NAME 为容器指定一个名字。
  • --entrypoint CMD 覆盖默认的容器启动指令。
  • -e KEY=VAL 设置环境变量值,可多次使用选项来设置多个环境变量。
  • -u, --user="" 指定运行容器的用户名或者 uid。
  • --no-deps 不自动启动关联的服务容器。
  • --rm 运行命令后自动删除容器,d 模式下将忽略。
  • -p, --publish=[] 映射容器端口到本地主机。
  • --service-ports 配置服务端口并映射到本地主机。
  • -T 不分配伪 tty,意味着依赖 tty 的指令将无法运行。

例如:

1
$ docker-compose run ubuntu ping docker.com

将会启动一个 ubuntu 服务容器,并执行 ping docker.com 命令。

默认情况下,如果存在关联,则所有关联的服务将会自动被启动,除非这些服务已经在运行中。

该命令类似启动容器后运行指定的命令,相关卷、链接等等都将会按照配置自动创建。

两个不同点:

  • 给定命令将会覆盖原有的自动运行命令;
  • 不会自动创建端口,以避免冲突。

如果不希望自动启动关联的容器,可以使用 --no-deps 选项,例如

1
$ docker-compose run --no-deps web python manage.py shell # 将不会启动web容器所关联的其它容器

rm

格式为 docker-compose rm [options] [SERVICE...]

删除所有(停止状态的)服务容器。推荐先执行 docker-compose stop 命令来停止容器。

选项:

  • -f, --force 强制直接删除,包括非停止状态的容器。一般尽量不要使用该选项。
  • -v 删除容器所挂载的数据卷。

start

格式为 docker-compose start [SERVICE...]

启动已经存在的服务容器。

stop

格式为 docker-compose stop [options] [SERVICE...]

停止已经处于运行状态的容器,但不删除它。通过 docker-compose start 可以再次启动这些容器。

选项:

  • -t, --timeout TIMEOUT 停止容器时候的超时(默认为 10 秒)。

restart

格式为 docker-compose restart [options] [SERVICE...]

重启项目中的服务。

选项:

  • -t, --timeout TIMEOUT 指定重启前停止容器的超时(默认为 10 秒)。

kill

格式为 docker-compose kill [options] [SERVICE...]

通过发送 SIGKILL 信号来强制停止服务容器。

支持通过 -s 参数来指定发送的信号,例如通过如下指令发送 SIGINT 信号。

1
$ docker-compose kill -s SIGINT

pause

格式为 docker-compose pause [SERVICE...]

暂停一个服务容器。

unpause

格式为 docker-compose unpause [SERVICE...]

恢复处于暂停状态中的服务。

pull

格式为 docker-compose pull [options] [SERVICE...]

拉取服务依赖的镜像。

选项:

  • --ignore-pull-failures 忽略拉取镜像过程中的错误。

push

推送服务依赖的镜像到 Docker 镜像仓库。

ps

格式为 docker-compose ps [options] [SERVICE...]

列出项目中目前的所有容器。

选项:

  • -q 只打印容器的 ID 信息。

images

列出 Compose 文件中包含的镜像。

logs

格式为 docker-compose logs [options] [SERVICE...]

查看服务容器的输出。默认情况下,docker-compose 将对不同的服务输出使用不同的颜色来区分。可以通过 --no-color 来关闭颜色。

该命令在调试问题的时候十分有用。

port

格式为 docker-compose port [options] SERVICE PRIVATE_PORT

打印某个容器端口所映射的公共端口。

1
2
$ docker-compose port web 5000 # 打印web服务5000端口所映射的公共端口
0.0.0.0:5000

选项:

  • --protocol=proto 指定端口协议,tcp(默认值)或者 udp。
  • --index=index 如果同一服务存在多个容器,指定命令对象容器的序号(默认为 1)。

scale

格式为 docker-compose scale [options] [SERVICE=NUM...]

设置指定服务运行的容器个数。

通过 service=num 的参数来设置数量。例如:

1
$ docker-compose scale web=3 db=2

将启动 3 个容器运行 web 服务,2 个容器运行 db 服务。

一般的,当指定数目多于该服务当前实际运行容器,将新创建并启动容器;反之,将停止容器。

选项:

  • -t, --timeout TIMEOUT 停止容器时候的超时(默认为 10 秒)。

top

查看各个服务容器内运行的进程。

exec

进入指定的容器。

参考资料

Compose 模板文件

模板文件是使用 Compose 的核心,涉及到的指令关键字也比较多。但大家不用担心,这里面大部分指令跟 docker run 相关参数的含义都是类似的。

默认的模板文件名称为 docker-compose.yml,格式为 YAML 格式。

1
2
3
4
5
6
7
8
9
version: "3"

services:
webapp:
image: examples/web
ports:
- "80:80"
volumes:
- "/data"

注意每个服务都必须通过 image 指令指定镜像或 build 指令(需要 Dockerfile)等来自动构建生成镜像。

如果使用 build 指令,在 Dockerfile 中设置的选项(例如:CMD, EXPOSE, VOLUME, ENV 等) 将会自动被获取,无需在 docker-compose.yml 中重复设置。

下面分别介绍各个指令的用法。

build

指定 Dockerfile 所在文件夹的路径(可以是绝对路径,或者相对 docker-compose.yml 文件的路径)。 Compose 将会利用它自动构建这个镜像,然后使用这个镜像。

1
2
3
4
5
version: '3'
services:

webapp:
build: ./dir

使用 context 指令指定 Dockerfile 所在文件夹的路径;

使用 dockerfile 指令指定 Dockerfile 文件名;

使用 arg 指令指定构建镜像时的变量;

使用 cache_from 指定构建镜像的缓存;

1
2
3
4
5
6
7
8
9
10
11
12
version: '3'
services:

webapp:
build:
context: ./dir
dockerfile: Dockerfile-alternate
args:
buildno: 1
cache_from:
- alpine:latest
- corp/web_app:3.14

cap_add, cap_drop

参考: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities

指定容器的内核能力(capacity)分配。

例如,让容器拥有所有能力可以指定为:

1
2
cap_add:
- ALL

去掉 NET_ADMIN 能力可以指定为:

1
2
cap_drop:
- NET_ADMIN

command

覆盖容器启动后默认执行的命令。

1
command: echo "hello world"

configs

仅用于 Swarm mode,详细内容请查看 Swarm mode 一节。

cgroup_parent

指定父 cgroup 组,意味着将继承该组的资源限制。

例如,创建了一个 cgroup 组名称为 cgroups_1

1
cgroup_parent: cgroups_1

container_name

指定容器名称。默认将会使用 项目名称_服务名称_序号 这样的格式。

1
container_name: docker-web-container

注意: 指定容器名称后,该服务将无法进行扩展(scale),因为 Docker 不允许多个容器具有相同的名称。

deploy

仅用于 Swarm mode,详细内容请查看 Swarm mode 一节

devices

指定设备映射关系。

1
2
devices:
- "/dev/ttyUSB1:/dev/ttyUSB0"

depends_on

解决容器的依赖、启动先后的问题。以下例子中会先启动 redisdb 再启动 web

1
2
3
4
5
6
7
8
9
10
11
12
13
14
version: '3'

services:
web:
build: .
depends_on:
- db
- redis

redis:
image: redis

db:
image: postgres

注意:web 服务不会等待 redis db 「完全启动」之后才启动。

dns

自定义 DNS 服务器。可以是一个值,也可以是一个列表。

1
2
3
4
5
dns: 8.8.8.8

dns:
- 8.8.8.8
- 114.114.114.114

配置 DNS 搜索域。可以是一个值,也可以是一个列表。

1
2
3
4
5
dns_search: example.com

dns_search:
- domain1.example.com
- domain2.example.com

tmpfs

tmpfs -维基百科

挂载一个 tmpfs 文件系统到容器。

1
2
3
4
tmpfs: /run
tmpfs:
- /run
- /tmp

env_file

从文件中获取环境变量,可以为单独的文件路径或列表。

如果通过 docker-compose -f FILE 方式来指定 Compose 模板文件,则 env_file 中变量的路径会基于模板文件路径。

如果有变量名称与 environment 指令冲突,则按照惯例,以后者为准。

1
2
3
4
5
6
env_file: .env

env_file:
- ./common.env
- ./apps/web.env
- /opt/secrets.env

环境变量文件中每一行必须符合格式,支持 # 开头的注释行。

1
2
# common.env: Set development environment
PROG_ENV=development

environment

设置环境变量。可以使用数组或字典两种格式。

只给定名称的变量会自动获取运行 Compose 主机上对应变量的值,可以用来防止泄露不必要的数据

1
2
3
4
5
6
7
environment:
RACK_ENV: development
SESSION_SECRET:

environment:
- RACK_ENV=development
- SESSION_SECRET

如果变量名称或者值中用到 true|false,yes|no 等表达 布尔 含义的词汇,最好放到引号里,避免 YAML 自动解析某些内容为对应的布尔语义。这些特定词汇,包括

1
y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF

expose

暴露端口,但不映射到宿主机,只被连接的服务访问。

仅可以指定内部端口为参数

1
2
3
expose:
- "3000"
- "8000"

注意:不建议使用该指令。

链接到 docker-compose.yml 外部的容器,甚至并非 Compose 管理的外部容器。

1
2
3
4
external_links:
- redis_1
- project_db_1:mysql
- project_db_1:postgresql

extra_hosts

类似 Docker 中的 --add-host 参数,指定额外的 host 名称映射信息。

1
2
3
extra_hosts:
- "googledns:8.8.8.8"
- "dockerhub:52.1.157.61"

会在启动后的服务容器中 /etc/hosts 文件中添加如下两条条目。

1
2
8.8.8.8 googledns
52.1.157.61 dockerhub

healthcheck

通过命令检查容器是否健康运行。

1
2
3
4
5
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 1m30s
timeout: 10s
retries: 3

image

指定为镜像名称或镜像 ID。如果镜像在本地不存在,Compose 将会尝试拉取这个镜像。

1
2
3
image: ubuntu
image: orchardup/postgresql
image: a4bc65fd

labels

为容器添加 Docker 元数据(metadata)信息。例如可以为容器添加辅助说明信息。

1
2
3
4
labels:
com.startupteam.description: "webapp for a startup team"
com.startupteam.department: "devops department"
com.startupteam.release: "rc3 for v1.0"

注意:不推荐使用该指令。

logging

配置日志选项。

1
2
3
4
logging:
driver: syslog
options:
syslog-address: "tcp://192.168.0.42:123"

目前支持三种日志驱动类型。

1
2
3
driver: "json-file"
driver: "syslog"
driver: "none"

options 配置日志驱动的相关参数。

1
2
3
options:
max-size: "200k"
max-file: "10"

network_mode

设置网络模式。使用和 docker run--network 参数一样的值。

1
2
3
4
5
network_mode: "bridge"
network_mode: "host"
network_mode: "none"
network_mode: "service:[service name]"
network_mode: "container:[container name/id]"

networks

配置容器连接的网络。

1
2
3
4
5
6
7
8
9
10
11
version: "3"
services:

some-service:
networks:
- some-network
- other-network

networks:
some-network:
other-network:

pid

跟主机系统共享进程命名空间。打开该选项的容器之间,以及容器和宿主机系统之间可以通过进程 ID 来相互访问和操作。

1
pid: "host"

ports

暴露端口信息。

使用宿主端口:容器端口 (HOST:CONTAINER) 格式,或者仅仅指定容器的端口(宿主将会随机选择端口)都可以。

1
2
3
4
5
ports:
- "3000"
- "8000:8000"
- "49100:22"
- "127.0.0.1:8001:8001"

注意:当使用 HOST:CONTAINER 格式来映射端口时,如果你使用的容器端口小于 60 并且没放到引号里,可能会得到错误结果,因为 YAML 会自动解析 xx:yy 这种数字格式为 60 进制。为避免出现这种问题,建议数字串都采用引号包括起来的字符串格式。

secrets

存储敏感数据,例如 mysql 服务密码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: "3.1"
services:

mysql:
image: mysql
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
secrets:
- db_root_password
- my_other_secret

secrets:
my_secret:
file: ./my_secret.txt
my_other_secret:
external: true

security_opt

指定容器模板标签(label)机制的默认属性(用户、角色、类型、级别等)。例如配置标签的用户名和角色名。

1
2
3
security_opt:
- label:user:USER
- label:role:ROLE

stop_signal

设置另一个信号来停止容器。在默认情况下使用的是 SIGTERM 停止容器。

1
stop_signal: SIGUSR1

sysctls

配置容器内核参数。

1
2
3
4
5
6
7
sysctls:
net.core.somaxconn: 1024
net.ipv4.tcp_syncookies: 0

sysctls:
- net.core.somaxconn=1024
- net.ipv4.tcp_syncookies=0

ulimits

指定容器的 ulimits 限制值。

例如,指定最大进程数为 65535,指定文件句柄数为 20000(软限制,应用可以随时修改,不能超过硬限制) 和 40000(系统硬限制,只能 root 用户提高)。

1
2
3
4
5
ulimits:
nproc: 65535
nofile:
soft: 20000
hard: 40000

volumes

数据卷所挂载路径设置。可以设置为宿主机路径(HOST路径:CONTAINER路径)或者数据卷名称(VOLUME名称:CONTAINER路径),并且可以设置访问模式 (HOST路径:CONTAINER路径:ro)。

该指令中路径支持相对路径。

1
2
3
4
volumes:
- /var/lib/mysql
- cache/:/tmp/cache
- ~/configs:/etc/configs/:ro

如果路径为数据卷名称,必须在文件中配置数据卷。

1
2
3
4
5
6
7
8
9
10
version: "3"

services:
my_src:
image: mysql:8.0
volumes:
- mysql_data:/var/lib/mysql # VOLUME名称:CONTAINER路径

volumes:
mysql_data:

其它指令

此外,还有包括 domainname, entrypoint, hostname, ipc, mac_address, privileged, read_only, shm_size, restart, stdin_open, tty, user, working_dir 等指令,基本跟 docker run 中对应参数的功能一致。

指定服务容器启动后执行的入口文件。

1
entrypoint: /code/entrypoint.sh

指定容器中运行应用的用户名。

1
user: nginx

指定容器中工作目录。

1
working_dir: /code

指定容器中搜索域名、主机名、mac 地址等。

1
2
3
domainname: your_website.com
hostname: test
mac_address: 08-00-27-00-0C-0A

允许容器中运行一些特权命令。

1
privileged: true

指定容器退出后的重启策略为始终重启。该命令对保持服务始终运行十分有效,在生产环境中推荐配置为 always 或者 unless-stopped

Docker容器的重启策略及docker run的–restart选项详解

1
restart: always

以只读模式挂载容器的 root 文件系统,意味着不能对容器内容进行修改。

1
read_only: true

打开标准输入,可以接受外部输入。

1
stdin_open: true

模拟一个伪终端。

1
tty: true

读取变量

Compose 模板文件支持动态读取主机的系统环境变量和当前目录下的 .env 文件中的变量。

例如,下面的 Compose 文件将从运行它的环境中读取变量 ${MONGO_VERSION} 的值,并写入执行的指令中。

1
2
3
4
5
version: "3"
services:

db:
image: "mongo:${MONGO_VERSION}"

如果执行 MONGO_VERSION=3.2 docker-compose up 则会启动一个 mongo:3.2 镜像的容器;如果执行 MONGO_VERSION=2.8 docker-compose up 则会启动一个 mongo:2.8 镜像的容器。

若当前目录存在 .env 文件,执行 docker-compose 命令时将从该文件中读取变量。

在当前目录新建 .env 文件并写入以下内容。

1
2
# 支持 # 号注释
MONGO_VERSION=3.6

执行 docker-compose up 则会启动一个 mongo:3.6 镜像的容器。

参考资料

Docker Compose环境变量

Docker Compose 引用环境变量

Docker Compose实战

一条命令拉起所有中间件:

middleware.tar.gz

踩坑:

nacos集群启动依赖了mysql服务,但mysql第一次启动时慢,nacos启动快,所以nacos无法连接mysql,导致nacos无法访问。
解决办法:
1、执行两次 docker-compose up -d ,第二次mysql启动就快了。
2、参考 docker compose 服务启动顺序控制

Docker Machine

推荐连接:

https://docs.docker.com/machine/

https://www.runoob.com/docker/docker-machine.html

https://github.com/yeasy/docker_practice/blob/master/machine/README.md

配置 Docker 主机支持远程访问,以供管理主机使用 docker Machine 命令管理:

https://docs.docker.com/engine/install/linux-postinstall/#configure-where-the-docker-daemon-listens-for-connections

选择合适的 Machine 驱动程序创建 Docker 主机实例:

https://docs.docker.com/machine/drivers/

https://docs.docker.com/machine/drivers/generic/

Swarm mode

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

https://www.runoob.com/docker/docker-swarm.html

https://github.com/yeasy/docker_practice/blob/master/swarm_mode/README.md

Docker 1.12 Swarm mode 已经内嵌入 Docker 引擎,成为了 docker 子命令 docker swarm。请注意与旧的 Docker Swarm 区分开来。

Swarm mode 内置 kv 存储功能,提供了众多的新特性,比如:具有容错能力的去中心化设计、内置服务发现、负载均衡、路由网格、动态伸缩、滚动更新、安全传输等。使得 Docker 原生的 Swarm 集群具备与 Mesos、Kubernetes 竞争的实力。