Docker教程
百科:
官方:
https://docs.docker.com/engine/
https://docs.docker.com/reference/
教程:
“纯洁的微笑” Docker 入门教程: Docker 一 、 Docker 二 、 Docker 三
博文:
docker中宿主机与容器(container)互相拷贝传递文件的方法
请习惯使用 –help 及 tab 命令补全
官方文档
https://docs.docker.com/get-docker/
镜像
官方镜像
阿里云 docker-ce镜像
https://mirrors.aliyun.com/docker-ce/
在线安装
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 | sudo apt-get remove docker \ |
使用 APT 安装
由于 apt
源使用 HTTPS 以确保软件下载过程中不被篡改。因此,我们首先需要添加使用 HTTPS 传输的软件包以及 CA 证书。
1 | 更新apt软件包索引 |
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 | sudo yum remove docker \ |
使用 yum 安装
执行以下命令安装依赖包:
1 | 安装依赖包 |
离线安装
下载软件包
https://linux.cn/article-7937-1.html
Yumdownloader 是一款简单,但是却十分有用的命令行工具,它可以一次性下载任何 RPM 软件包及其所有依赖包。
1 | 安装Yumdownloader工具 |
安装
1 | cd /opt/dockerInstallPackage |
镜像加速器
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 | { |
注意:daemon.json 文件内容不能为空白,否则启动失败 unable to configure the Docker daemon with file /etc/docker/daemon.json: EOF
,除非删除文件或文件包含一对大括号 {}
。
然后重新启动服务:
1 | sudo systemctl daemon-reload |
检查加速器是否生效:
1 | docker info # 保证docker为启动状态 |
基本概念
Docker 包括三个基本概念
- 镜像(
Image
)https://github.com/yeasy/docker_practice/blob/master/basic_concept/image.md - 容器(
Container
)https://github.com/yeasy/docker_practice/blob/master/basic_concept/container.md - 仓库(
Repository
)https://github.com/yeasy/docker_practice/blob/master/basic_concept/repository.md
理解了这三个概念,就理解了 Docker 的整个生命周期。
使用镜像
查找镜像
可以从 Docker Hub 网站来搜索镜像,Docker Hub 网址为: https://hub.docker.com/
也可以使用 docker search 命令来搜索镜像
1 | docker search ubuntu |
NAME: 镜像仓库源的名称。
名为 ubuntu
的镜像,是由 Docker 公司创建、验证、支持、提供的基础镜像 (又叫根镜像)。往往使用单个单词作为名字。
名为 dorowu/ubuntu-desktop-lxde-vnc
的镜像,是由 Docker Hub 的注册用户创建并维护的,往往带有用户名称前缀。比如 ansible 用户。
DESCRIPTION: 镜像的描述
STARS: 类似 Github 里面的 star,表示点赞、喜欢的意思
OFFICIAL: 是否 docker 官方发布
AUTOMATED: 自动构建
拉取镜像
1 | docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] |
具体的选项可以通过 docker pull --help
命令看到,这里我们说一下镜像名称的格式。
- Docker 镜像仓库地址:地址的格式一般是
<域名/IP>[:端口号]
。默认地址是 Docker Hub(docker.io
)。 - 仓库名:如之前所说,这里的仓库名是两段式名称,即
<用户名>/<软件名>
。对于 Docker Hub,如果不给出用户名,则默认为library
,也就是官方镜像。
1 | $ docker pull ubuntu:18.04 # 没有给出Docker镜像仓库地址和仓库名,默认docker.io/library/ |
按照上述命令操作时,你看到的层 ID 以及 sha256
摘要和这里的不一样。因为官方镜像一直在维护更新,修复后再以原来的标签发布,确保任何使用这个标签的用户可以获得更安全、更稳定的镜像。
列出镜像
1 | docker image ls --help |
示例:
1 | $ docker images # 列出所有顶层镜像 |
--filter
配合 -q
将产生出指定范围的 ID 列表,然后送给另一个 docker
命令作为参数,从而对这组实体批量进行某种操作。这种做法非常常见,不仅仅是镜像,将来我们会在各个命令中看到这类搭配以完成很强大的功能。因此每次在文档看到过滤器后,可以多注意一下它们的用法。
1 | docker image ls -a --digests # 显示摘要 |
各个选项说明:
- **REPOSITORY:**镜像的仓库名
- **TAG:**镜像的标签
- **IMAGE ID:**镜像ID
- **CREATED:**镜像创建时间
- **SIZE:**镜像大小
同一仓库名可以有多个 TAG,代表这个仓库源的不同个版本,使用 REPOSITORY:TAG 来定义不同的镜像。
镜像 ID 是镜像的唯一标识,一个镜像可以对应多个 标签,ubuntu:18.04
和 ubuntu:bionic
拥有相同的 ID,因为它们对应的是同一个镜像。
镜像体积
1、本地标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。因为 Docker Hub 中显示的体积是压缩后的体积,而 docker image ls
显示的是镜像下载到本地后展开的大小,准确说,是展开后各层所占空间的总和。
2、docker image ls
列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。
1 | docker system df # 查看镜像、容器、数据卷所占用的空间 |
虚悬镜像
由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none>
的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image)
1 | docker image ls |
这个镜像原本是有镜像名和标签的,原来为 mongo:3.2
,随着官方镜像维护更新,重新 docker pull mongo:3.2
时,mongo:3.2
这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 <none>
。docker build
也同样可以导致这种现象。
1 | docker image ls -f dangling=true # 列出虚悬镜像(dangling image) |
既没有仓库名,也没有标签,均为 <none>
的镜像,不一定是虚悬镜像,也有可能是中间层镜像。
中间层镜像
为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像。
与虚悬镜像不同,这些无标签的镜像是中间层镜像,是其它镜像所依赖的镜像。不应该删除,否则会导致上层镜像因为依赖丢失而出错。也没必要删除,相同的层只会存一遍。
只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除。
运行容器
使用版本为18.04的ubuntu系统镜像来运行容器,命令如下:
1 | docker run -it --rm ubuntu:18.04 bash # 指定版本标签。当在本地主机上使用一个不存在的镜像时Docker就会自动下载这个镜像。 |
docker run
就是运行容器的命令,这里简要的说明一下上面用到的参数:
参数说明:
- -i: 交互式操作,让容器的标准输入保持打开。
- -t: 让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上。
- –rm: 容器退出或停止运行后随之将其删除。默认情况下,退出的容器并不会立即删除,除非手动
docker rm
。 - ubuntu:18.04: 这是指用 ubuntu 18.04 版本镜像为基础来启动容器。
- /bin/bash:放在镜像名后的是 命令,这里我们希望有个交互式 Shell,因此用的是
bash
。
退出终端或容器,ctrl+d
或 输入exit
。
删除镜像
1 | docker image rm --help |
示例:
1 | $ docker rmi ubuntu:15.10 # 根据标签删除 |
Untagged 和 Deleted
删除行为分为两类,一类是 Untagged
,另一类是 Deleted
。
镜像的唯一标识是其 ID (或摘要)。当一个镜像 ID 对应多个标签时,根据 ID 删除镜像不会成功,需要先取消镜像的标签,这就是看到的 Untagged
的信息。只要还有其他标签指向这个镜像,那么 Delete
行为就不会发生。所以并非所有的 docker image rm
都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。
当镜像的所有标签都被取消了,它也就失去了存在的意义,便会出发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。
除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。容器是以镜像为基础,再加一层容器存储层,组成多层存储结构运行。如果该镜像被容器所依赖,那么删除该镜像必然会导致故障。应该先删除容器,再删除镜像。
批量删除
使用 docker image ls -q
配合使用 docker image rm
1 | $ docker image rm $(docker image ls -q redis) # 删除所有仓库名为 redis 的镜像 |
构建镜像
镜像是容器的基础,每次执行 docker run
的时候都会指定哪个镜像作为容器运行的基础。当来自于 Docker Hub 的镜像无法满足需求时,就需要定制镜像。
两种方式:
- 修改已创建的容器(修改容器的存储层),并提交。
- 使用 Dockerfile 指令来创建一个新的镜像。
利用 commit 理解镜像构成
注意:如果您是初学者,您可以暂时跳过后面的内容,直接学习 容器 一节。
注意: docker commit
命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 docker commit
定制镜像,定制镜像应该使用 Dockerfile
来完成。如果想要定制镜像请查看下一小节。
镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。
1 | $ docker run --name webserver -d -p 80:80 nginx # 用nginx镜像启动一个容器,命名为webserver |
现在我们定制好了变化,我们希望能将其保存下来形成镜像。
当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。
Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。
docker commit
的语法格式为:
1 | docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]] |
将容器保存为镜像:
1 | docker commit --help |
各个参数说明:
- -m: 提交的描述信息
- -a: 指定镜像作者
- ff8700bb348f: 容器 ID,指定要提交的容器。也可以使用容器的名称webserver。
- zhaolq/nginx:v2: 指定要创建的目标镜像的 repository 和 tag。
查看:
1 | docker image ls nginx # 列出新定制的镜像 |
至此,我们完成了定制镜像,使用 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 | $ mkdir mynginx |
Ubuntu的Dockerfile
https://hub.docker.com/_/ubuntu
FROM 指定基础镜像
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。
FROM
指定 基础镜像,一个 Dockerfile
中 FROM
是必备指令,并且必须是第一条指令(有异议,因为ARG
指令可以放到FROM
前面)。
在 Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node
、openjdk
、python
、ruby
、golang
等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntu
、debian
、centos
、fedora
、alpine
等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
Docker 还存在一个特殊的镜像,名为 scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
1 | 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 | FROM debian:stretch |
Dockerfile 中每一个指令都会建立一层,RUN
也不例外。
每一个 RUN
的行为:新建立一层,在其上执行这些命令,执行结束后,commit
这一层的修改,构成新的镜像。
而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。
Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。
Dockerfile
正确的写法:
1 | FROM debian:stretch |
首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这应该是一层的事情。
这不是在写 Shell 脚本,而是在定义每一层该如何构建。
Dockerfile 支持 Shell 类的行尾添加 \
的命令换行方式,以及行首 #
进行注释的格式。
每一层构建的最后一定要清理掉无关文件,避免镜像臃肿 。这是很重要的一步,因为镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加 真正需要添加的东西,任何无关的东西都应该在每一层构建的最后清理掉 。
构建镜像
docker build 构建
格式:docker build [选项] <上下文路径/URL/->
在 Dockerfile
文件所在目录执行:
1 | $ docker build --help |
镜像构建上下文(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 | $ vim Dockerfile |
利用上述 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 |
如果发现标准输入的文件格式是 gzip
、bzip2
以及 xz
的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。
Dockerfile 指令详解
我们已经介绍了 FROM
,RUN
,还提及了 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 | COPY hom* /mydir/ |
<目标路径>
可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR
指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
此外,还需要注意一点,使用 COPY
指令,源文件的各种元数据都会保留,比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。
在使用该指令的时候还可以加上 --chown=<user>:<group>
选项来改变文件的所属用户及所属组。
1 | COPY --chown=55:mygroup files* /mydir/ |
如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。
ADD 更高级的复制文件
ADD
指令和 COPY
的格式和性质基本一致。但是在 COPY
基础上增加了一些功能。
比如 <源路径>
可以是一个 URL
,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径>
去。下载后的文件权限自动设置为 600
,如果这并不是想要的权限,那么还需要增加额外的一层 RUN
进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN
指令进行解压缩。所以不如直接使用 RUN
指令,然后使用 wget
或者 curl
工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。
如果 <源路径>
为一个 tar
压缩文件的话,压缩格式为 gzip
, bzip2
以及 xz
的情况下,ADD
指令会自动解压缩这个压缩文件到 <目标路径>
去。
在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu
中:
1 | FROM scratch |
但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD
命令了。
在 Docker 官方的 Dockerfile 最佳实践文档 中要求,尽可能的使用 COPY
,因为 COPY
的语义很明确,就是复制文件而已,而 ADD
则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD
的场合,就是所提及的需要自动解压缩的场合。
另外需要注意的是,ADD
指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。
因此在 COPY
和 ADD
指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY
指令,仅在需要自动解压缩的场合使用 ADD
。
在使用该指令的时候还可以加上 --chown=<user>:<group>
选项来改变文件的所属用户及所属组。
1 | ADD --chown=55:mygroup files* /mydir/ |
CMD 容器启动命令
CMD
指令的格式和 RUN
相似,也是两种格式:
shell
格式:CMD <命令>
exec
格式:CMD ["可执行文件", "参数1", "参数2"...]
- 参数列表格式:
CMD ["参数1", "参数2"...]
。在指定了ENTRYPOINT
指令后,用CMD
指定具体的参数。
CMD
指令如果写了多次,只有最后一个生效。
之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD
指令就是用于指定容器主进程的启动命令。
在运行时可以指定新的命令来替代镜像中设置的默认命令:
1 | docker run -it ubuntu # ubuntu镜像默认的CMD是/bin/bash,所以会直接进入bash |
在指令格式上,一般推荐使用 exec
格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 "
,而不要使用单引号。
如果使用 shell
格式的话,实际的命令会被包装为 sh -c
的参数的形式进行执行。比如:
1 | CMD echo $HOME |
在实际执行中,会将其变更为:
1 | CMD [ "sh", "-c", "echo $HOME" ] |
环境变量会被 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
来指定。
当指定 ENTRYPOINT
后,CMD
不再直接运行其命令,CMD
的内容将作为参数传给 ENTRYPOINT
指令。换句话说实际执行时,将变为:
1 | <ENTRYPOINT> "<CMD>" |
CMD
指令如果写了多次,只有最后一个生效。
那么有了 CMD
后,为什么还要有 ENTRYPOINT
呢?这种 <ENTRYPOINT> "<CMD>"
有什么好处么?让我们来看几个场景。
场景一:让镜像变成像命令一样使用
假设需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD
来实现:
1 | FROM ubuntu:18.04 |
假如使用 docker build -t myip:v1 .
来构建镜像的话,如果需要查询当前公网 IP,只需要执行:
1 | docker run --rm myip:v1 |
CMD
中可以看到实质的命令是 curl
,那么如果希望显示 HTTP 头信息,就需要加上 -i
参数。
1 | docker run --rm myip:v1 -i |
可以看到可执行文件找不到的报错。 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 | FROM ubuntu:18.04 |
再次使用 docker run --rm myip:v1 -i
就成功了。因为当存在 ENTRYPOINT
后,CMD
的内容将会作为参数传给 ENTRYPOINT
,而这里 -i
就是新的 CMD
,因此会作为参数传给 curl
,从而达到了我们预期的效果。
场景二:应用运行前的准备工作
启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。
比如 mysql
类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。
此外,可能希望避免使用 root
用户去启动服务,从而提高安全性,而在启动服务前还需要以 root
身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root
身份执行,方便调试等。
这些准备工作是和容器 CMD
无关的,无论 CMD
为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT
中去执行,而这个脚本会将接到的参数(也就是 <CMD>
)作为命令,在脚本最后执行。比如官方镜像 redis
中就是这么做的:
1 | FROM alpine:3.4 |
可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINT
为 docker-entrypoint.sh
脚本。进入redis容器可查看全部 docker-entrypoint.sh 脚本内容如下:
1 | !/bin/sh |
https://github.com/tianon/gosu
该脚本的内容就是根据 CMD
的内容来判断,如果是 redis-server
的话,则切换到 redis
用户身份启动服务器,否则依旧使用 root
身份执行。比如:
1 | docker run -it redis id |
ENV 设置环境变量
格式有两种:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,还是运行时的应用,都可以直接使用这里定义的环境变量。
1 | ENV VERSION=1.0 DEBUG=on \ |
ARG 构建参数
格式:ARG <参数名>[=<默认值>]
构建参数和 ENV
的效果一样,都是设置环境变量。所不同的是,ARG
所设置的构建环境的环境变量,在将来容器运行时不会存在这些环境变量。但是不要因此就使用 ARG
保存密码之类的信息,因为 docker history
还是可以看到所有值的。
Dockerfile
中的 ARG
指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build
中用 --build-arg <参数名>=<值>
来覆盖。
灵活的使用 ARG
指令,能够在不修改 Dockerfile 的情况下,构建出不同的镜像。
ARG 指令有生效范围,如果在 FROM
指令之前指定,那么只能用于 FROM
指令中。
1 | 只在 FROM 中生效 |
VOLUME 挂载为匿名卷
docker volumes 中 -v 和 -mount 区别
https://docs.docker.com/storage/volumes/
格式为:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。
为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile
中,可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载也不会向容器存储层写入大量数据。
1 | VOLUME /data # 容器内数据卷的路径 |
这里的 /data
目录就会在容器运行时自动挂载为匿名卷,任何向 /data
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。
由于匿名挂载的时候只指定了 容器内数据卷的路径
,那么到底挂载到 宿主机
的哪个路径,可以使用以下命令查看:
1 | docker ps -a |
Mounts 中可以看到 Destination 和 Source 分别就是 容器内数据卷路径 和 宿主机容器卷路径。
–运行容器时挂载–
匿名挂载(匿名卷):在进行数据卷挂载的时候不指定 宿主机数据卷的路径
,-v
命令之后直接跟上 容器内数据卷的路径
。会覆盖 VOLUME 定义的匿名卷。
具名挂载(命名卷):在进行数据卷挂载的时候既指定 宿主机数据卷的路径
,又指定 容器内数据卷的路径
。
1 | 匿名挂载(匿名卷),会覆盖 VOLUME 定义的匿名卷。 |
在这行命令中,就使用了 /home/mydata
这个命名卷挂载到了 /data
这个位置,替代了 Dockerfile
中定义的匿名卷挂载配置。
EXPOSE 暴露端口
格式为 EXPOSE <端口1> [<端口2>...]
。
EXPOSE
指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。
在 Dockerfile 中写入这样的声明有两个好处:
- 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;
- 在运行时使用随机端口映射时,也就是
docker run -P
时(P 大写),会自动随机映射EXPOSE
的端口。
EXPOSE
和在运行时使用 -p <宿主端口>:<容器端口>
的区别:
-p
,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问。而 EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
下面使用随机宿主端口映射到 EXPOSE
暴露的端口:
1 | docker images |
WORKDIR 指定工作目录
格式为 WORKDIR <工作目录路径>
。
使用 WORKDIR
指令可以来指定工作目录做为以后各层的当前目录,如该目录不存在,WORKDIR
会帮助建立目录。
1 | RUN cd /app |
以上 Dockerfile
进行构建镜像运行后,会发现找不到 /app/world.txt
文件。原因很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile
中,这两行 RUN
命令的执行环境根本不同,是两个完全不同的容器。
每一个 RUN
都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app
的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。
改变以后各层的工作目录的位置:
1 | WORKDIR /app |
如果 WORKDIR
指令使用的相对路径,那么所切换的路径与之前的 WORKDIR
有关:
1 | WORKDIR /a |
RUN pwd
的工作目录为 /a/b/c
。
USER 指定当前用户
格式:USER <用户名>[:<用户组>]
USER
指令和 WORKDIR
相似,都是改变环境状态并影响以后的层。WORKDIR
是改变工作目录,USER
则是改变之后层的执行 RUN
, CMD
以及 ENTRYPOINT
这类命令的身份。
注意,USER
只是帮助切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
1 | RUN groupadd -r redis && useradd -r -g redis redis |
如果以 root
执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su
或者 sudo
,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu
,阅读本文 ENTRYPOINT 入口点
- 场景二
以更好的了解gosu。
1 | # 建立 redis 用户,并使用 gosu 换另一个用户执行命令 |
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
来帮助判断,其 Dockerfile
的 HEALTHCHECK
写法:
1 | FROM nginx |
这里设置了使用 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 | 初始状态为 (health: starting) |
为了帮助排障,健康检查命令的输出(包括 stdout
以及 stderr
)都会被存储于健康状态里:
1 | docker inspect --format '{{json .State.Health}}' web | python -m json.tool |
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 | LABEL org.opencontainers.image.authors="zhaolq" |
具体可以参考 https://github.com/opencontainers/image-spec/blob/master/annotations.md
SHELL 指令
格式:SHELL ["executable", "parameters"]
1 | SHELL` 指令可以指定 `RUN` `ENTRYPOINT` `CMD` 指令的 shell,Linux 中默认为 `["/bin/sh", "-c"] |
两个 RUN
运行同一命令,第二个 RUN
运行的命令会打印出每条命令并当遇到错误时退出。
当 ENTRYPOINT
CMD
以 shell 格式指定时,SHELL
指令所指定的 shell 也会成为这两个指令的 shell
1 | SHELL ["/bin/sh", "-cex"] |
参考文档
Dockerfie
官方文档:https://docs.docker.com/engine/reference/builder/Dockerfile
最佳实践文档:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/Docker
官方镜像Dockerfile
:https://github.com/docker-library/docs
多阶段构建
…
构建多种系统架构支持的镜像
…
其它制作镜像的方式
除了标准的使用 Dockerfile
生成镜像的方法外,由于各种特殊需求和历史原因,还提供了一些其它方法用以生成镜像。
从 rootfs(跟文件系统) 压缩包导入
请阅读下文中 导出和导入容器
小节。
Docker 镜像的导入和导出
docker save
和 docker load
命令,用以将镜像保存为归档文件,然后传输到另一个服务器上,再加载进来。这是在没有 Docker Registry 时的做法,现在已经不推荐,镜像迁移应该使用 Docker Registry,无论是直接使用 Docker Hub 还是使用内网私有 Registry 都可以。
docker save
导出镜像存储文件(将镜像保存为归档文件):
1 | docker images ubuntu |
注意:如果同名则会覆盖(没有警告)
docker load
导入镜像存储文件到本地镜像库
1 | docker load -i ubuntu-21.04.tar |
如果我们结合这两个命令以及 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 | docker run --name helloworld --rm ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" |
如果使用 -d
参数运行容器,容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上(输出结果可以用 docker logs
查看)。
1 | docker run --name helloworld -d --rm ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" |
注: 容器是否会长久运行,是和 docker run
指定的命令有关,和 -d
参数无关。
使用 -d
参数启动后会返回一个唯一的 id,也可以通过 docker container ls
命令来查看容器信息。
1 | docker ps -a |
通过 docker container logs
命令获取容器的输出信息:
1 | docker container logs [container ID or NAMES] |
进入容器
有些时候需要进入正在后台运行的容器进行操作,可以使用 docker attach
或 docker exec
命令。
推荐使用 docker exec
,因为从这个 stdin 中 exit,不会导致容器停止。
attach
命令
1 | docker run -dit ubuntu:18.04 |
注意: 如果从这个 stdin 中 exit,会导致容器停止。
exec
命令
只用 -i
参数时,由于没有分配伪终端,界面没有 Linux 命令提示符,但命令执行结果仍然可以返回。Tab
键不会自动填充。
当 -i
-t
参数一起使用时,界面有 Linux 命令提示符。Tab
键会自动填充。
1 | docker run -dit ubuntu:18.04 |
导出和导入容器
导出容器快照
docker export
将容器的文件系统导出为 tar 存档到当前目录,无论容器是否运行。
1 | docker ps -a |
导入容器快照
docker import
导入容器快照到本地镜像库:
1 | docker import nginx.tar test/nginx:v1.0 # 从本地获取文件并导入 |
注:
用户既可以使用
docker load
来导入镜像存储文件到本地镜像库,也可以使用docker import
来导入容器快照到本地镜像库。两者区别在于:
1、归档文件不同。
docker load
用来导入由docker save
导出的镜像存储文件;docker import
用来导入由docker export
导出的容器快照。2、文件大小不同。容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态);镜像存储文件将保存完整记录,体积也要大。
3、从容器快照文件导入时可以重新指定标签等元数据信息。
相同点:
都是导入到本地镜像库。
即便原来的镜像有依赖,导出再导入后,都只有一个镜像。
删除容器
1 | docker container rm [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 | docker images |
自动构建
自动构建(Automated Builds
)功能对于需要经常升级镜像内程序来说,十分方便。
有时候,用户构建了镜像,安装了某个软件,当软件发布新版本则需要手动更新镜像。
而自动构建允许用户通过 Docker Hub 指定跟踪一个目标网站(支持 GitHub 或 BitBucket)上的项目,一旦项目发生新的提交 (commit
)或者创建了新的标签(tag
),Docker Hub 会自动构建镜像并推送到 Docker Hub 中。
1、创建 GitHub 仓库: docker-automated-builds-test
,包含 Dockerfile
文件:
1 | FROM ubuntu:18.04 |
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
中已经配置了 Webhooks
和 Deploy keys
。
5、查看构建结果:
配置完成之后,一旦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 | docker run -d \ |
推送镜像到本地仓库
例如私有仓库地址为 127.0.0.1:5000
,需先使用 docker tag
来标记一个镜像,然后就可以推送到仓库了。
1 | docker images -a |
可以看到 {"repositories":["ubuntu"]}
,表明镜像已经被成功推送了。
搜索本地仓库镜像
浏览器访问 http://127.0.0.1:5000/v2/_catalog
或使用命令 curl
:
1 | curl 127.0.0.1:5000/v2/_catalog # 查看仓库中的镜像 |
拉取本地仓库镜像
先删除已有镜像,再尝试从私有仓库中拉取这个镜像。
1 | docker rmi 127.0.0.1:5000/ubuntu:latest |
配置非 https 仓库地址
若想让本网段的其他主机也能把镜像推送到私有仓库,就要使用例如 172.27.13.27:5000
这样的内网地址做为私有仓库地址:
1 | docker tag nginx:latest 172.27.13.27:5000/nginx:latest |
会发现推送失败。这是因为 Docker 默认不允许非 HTTPS
方式推送镜像。可以通过 Docker 的配置选项来取消这个限制,或者查看下一节配置能够通过 HTTPS
访问的私有仓库。
对于使用 systemd
的系统,请在 /etc/docker/daemon.json
中写入如下内容(如果文件不存在请新建该文件)
1 | { |
注意:该文件必须符合
json
规范,否则 Docker 将不能启动。
然后重新启动服务:
1 | sudo systemctl daemon-reload |
检查是否生效:
1 | docker info # 保证docker为启动状态 |
私有仓库高级配置
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 | docker volume create --name nexus-data # 创建一个数据卷,在 /var/lib/docker/volumes 下 |
这里的 5001 端口映射出来供后面使用。
首次运行需等待 3-5 分钟。
查看滚动日志,输出以下内容表示启动成功,可以使用浏览器打开 http://YourIP:8081
访问 Nexus
:
1 | docker logs -f nexus3 |
获取初始密码 (默认帐号是 admin
),首次登录会提示更改初始密码:
1 | docker exec nexus3 cat /nexus-data/admin.password |
登录后,点击页面上方的齿轮按钮 按照下面的方法进行设置。
创建仓库
创建一个私有仓库的方法: Repository->Repositories
点击右边菜单 Create repository
选择 docker (hosted)
- Name: 仓库的名称
- HTTP: 仓库单独的访问端口(例如:5001) 记得添加端口映射
- Hosted -> Deployment pollcy: 请选择 Allow redeploy 否则无法上传 Docker 镜像。
其它的仓库创建方法请各位自己摸索,还可以创建一个 docker (proxy)
类型的仓库链接到 DockerHub 上。再创建一个 docker (group)
类型的仓库把刚才的 hosted
与 proxy
添加在一起。主机在访问的时候默认下载私有仓库中的镜像,如果没有将链接到 DockerHub 中下载并缓存到 Nexus 中。
注意:这里的 5001 端口需要对外映射,否则后面无法使用
docker login HostIP:5001
登录,最简单的方式就是删除容器,重新启动一个。由于已经挂载了/nexus-data
目录,所以一切设置都不会丢失。更复杂操作参考 docker容器添加对外映射端口 。
添加访问权限
菜单 Security->Realms
把 Docker Bearer Token Realm 移到右边的框中保存。
添加角色
菜单 Security->Roles
->Create role
在 Privlleges
选项搜索 docker 把相应的规则移动到右边的框中然后保存。
添加用户
菜单 Security->Users
->Create local user
在 Roles
选项中选中刚才创建的规则移动到右边的窗口保存。
NGINX 加密代理
证书的生成请参见 私有仓库高级配置
里面证书生成一节。
NGINX 示例配置如下
1 | upstream register |
访问镜像仓库
如果不启用 SSL 加密可以通过 前面章节 的方法添加非 https 仓库地址到 Docker 的配置文件中然后重启 Docker。
使用 SSL 加密以后程序需要访问就不能采用修改配置的方式了。具体方法如下:
1 | $ openssl s_client -showcerts -connect YourDomainName OR HostIP:443 </dev/null 2>/dev/null|openssl x509 -outform PEM >ca.crt |
使用 docker login YourDomainName OR HostIP
进行测试,用户名密码填写上面 Nexus 中设置的。
数据管理
docker volumes 中 -v 和 -mount 区别
https://docs.docker.com/storage/volumes/
这一章介绍如何在 Docker 内部以及容器之间管理数据,在容器中管理数据主要有两种方式:
- 数据卷(Volumes)
- 挂载主机目录 (Bind mounts)
数据卷
数据卷
是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:
数据卷
可以在容器之间共享和重用- 对
数据卷
的修改会立马生效 - 对
数据卷
的更新,不会影响镜像 数据卷
默认会一直存在,即使容器被删除
注意:
数据卷
的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会复制到数据卷中(仅数据卷为空时会复制)。
创建一个数据卷
1 | docker volume create my-vol |
启动一个挂载数据卷的容器
用 docker run
命令时,使用 --mount
标记来将 数据卷
挂载到容器里。一次 docker run
中可以挂载多个 数据卷
。
下面创建一个名为 web
的容器,并加载一个 数据卷
到容器的 /usr/share/nginx/html
目录。
1 | docker run -d -P \ |
查看数据卷的具体信息
1 | docker inspect web |
数据卷
的信息在 “Mounts” Key 下面
1 | "Mounts": [ |
删除数据卷
1 | docker volume rm my-vol |
数据卷
是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷
,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷
。如果需要在删除容器的同时移除数据卷,使用 docker rm -v
命令。
1 | docker rm -fv web # 本人测试无效 |
无主的数据卷可能会占据很多空间,要清理请使用以下命令
1 | docker volume prune |
挂载主机目录
挂载一个主机目录作为数据卷
使用 --mount
标记可以指定挂载一个本地主机的目录到容器中去。
1 | 加载主机的 /src/webapp 目录到容器的 /usr/share/nginx/html目录 |
这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。
本地目录的路径必须是绝对路径,使用 -v
参数时 Docker 会自动创建文件夹,使用 --mount
参数时如果本地目录不存在,Docker 会报错。
Docker 挂载主机目录的默认权限是 读写
。用户可以通过增加 readonly
指定为 只读
。
1 | docker run -d -P \ |
查看数据卷的具体信息
1 | docker inspect web |
挂载主机目录
的信息在 “Mounts” Key 下面
1 | "Mounts": [ |
挂载一个本地主机文件作为数据卷
--mount
标记也可以从主机挂载单个文件到容器中
1 | docker run --rm -it \ |
这样就可以记录容器输入过的命令了。
使用网络
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 | docker port fa 80 |
注意:
- 容器有自己的内部网络和 ip 地址(使用
docker inspect
查看,Docker 还可以有一个可变的网络配置。) -p
标记可以多次使用来绑定多个端口
例如
1 | docker run -d \ |
容器互联
如果有 Docker
使用经验,可能已经习惯了使用 --link
参数来使容器互联。
随着 Docker 网络的完善,强烈建议大家将容器加入自定义的 Docker 网络来连接多个容器,而不是使用 --link
参数。
新建网络
1 | 创建一个新的 Docker 网络 |
-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 | docker container ls |
下面通过 ping
来证明 busybox1
容器和 busybox2
容器建立了互联关系。
在 busybox1
容器输入以下命令
1 | / # ping busybox2 |
同理在 busybox2
容器执行 ping busybox1
,也会成功连接到。
这样,busybox1
容器和 busybox2
容器建立了互联关系。
Docker Compose
如果有多个容器之间需要互相连接,推荐使用 Docker Compose。
配置 DNS
如何自定义配置容器的主机名和 DNS 呢?秘诀就是 Docker 利用虚拟文件来挂载容器的 3 个相关配置文件。
在容器中使用 mount
命令可以看到挂载信息:
1 | mount |
这种机制可以让宿主主机 DNS 信息发生更新后,所有 Docker 容器的 DNS 配置通过 /etc/resolv.conf
文件立刻得到更新。
配置全部容器的 DNS ,也可以在 /etc/docker/daemon.json
文件中增加以下内容来设置。
1 | { |
这样每次启动的容器 DNS 自动配置为 114.114.114.114
和 114.114.115.115
。使用以下命令来证明其已经生效。
1 | docker run -it --rm ubuntu:18.04 cat etc/resolv.conf |
如果用户想要手动指定容器的配置,可以在使用 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 所有的网络定制配置,以及通过 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 | sysctl net.ipv4.ip_forward |
如果为 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 | sudo iptables -nL |
之后,启动容器(docker run
)时使用 --link=CONTAINER_NAME:ALIAS
选项。Docker 会在 iptable
中为 两个容器分别添加一条 ACCEPT
规则,允许相互访问开放的端口(取决于 Dockerfile
中的 EXPOSE
指令)。
当添加了 --link=CONTAINER_NAME:ALIAS
选项后,添加了 iptables
规则。
1 | sudo iptables -nL |
注意:--link=CONTAINER_NAME:ALIAS
中的 CONTAINER_NAME
目前必须是 Docker 分配的名字,或使用 --name
参数指定的名字。主机名则不会被识别。
映射容器端口到宿主主机的实现
默认情况下,容器可以主动访问到外部网络的连接,但是外部网络无法访问到容器。
容器访问外部实现
容器所有到外部网络的连接,源地址都会被 NAT 成本地系统的 IP 地址。这是使用 iptables
的源地址伪装操作实现的。
查看主机的 NAT 规则。
1 | sudo iptables -t nat -nL |
其中,上述规则将所有源地址在 172.17.0.0/16
网段,目标地址为其他网段(外部网络)的流量动态伪装为从系统网卡发出。
MASQUERADE 跟传统 SNAT 的好处是它能动态从网卡获取地址。
外部访问容器实现
容器允许外部访问,可以在 docker run
时候通过 -p
或 -P
参数来启用。
不管用那种办法,其实也是在本地的 iptable
的 nat 表中添加相应的规则。
使用 -P
时:
1 | iptables -t nat -nL |
使用 -p 80:80
时:
1 | iptables -t nat -nL |
注意:
- 这里的规则映射了
0.0.0.0
,意味着将接受主机来自所有接口的流量。用户可以通过-p IP:host_port:container_port
或-p IP::port
来指定允许访问容器的主机上的 IP、接口等,以制定更严格的规则。 - 如果希望永久绑定到某个固定的 IP 地址,可以在 Docker 配置文件
/etc/docker/daemon.json
中添加如下内容。
1 | { |
配置 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 | brctl show |
注:
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 | docker run -dit --name centos centos:centos8 |
自定义网桥
除了默认的 docker0
网桥,用户也可以指定网桥来连接各个容器。
在启动 Docker 服务(即 dockerd --help
)的时候,使用 -b BRIDGE
或--bridge=BRIDGE
来指定使用的网桥。
如果服务已经运行,那需要先停止服务,并删除旧的网桥。
1 | sudo systemctl stop docker |
然后创建一个网桥 bridge0
。
1 | sudo brctl addbr bridge0 |
查看确认网桥创建并启动。
1 | ip addr show bridge0 |
在 Docker 配置文件 /etc/docker/daemon.json
中添加如下内容,即可将 Docker 默认桥接到创建的网桥上。
1 | { |
然后重新启动 Docker 服务:
1 | sudo systemctl daemon-reload |
新建一个容器,可以看到它已经桥接到了 bridge0
上。
在容器中可以使用 ip addr
和 ip 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 | docker-compose --version |
Linux 系统请使用以下介绍的方法安装。
二进制包
在 Linux 上的也安装十分简单,从 官方 GitHub Release 处直接下载编译好的二进制文件即可。
例如,在 Linux 64 位系统上直接下载对应的二进制包:
1 | sudo curl -L https://github.com/docker/compose/releases/download/${version}/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose |
PIP 安装
注: x86_64
架构的 Linux 建议按照上边的方法下载二进制包进行安装,如果您计算机的架构是 ARM
(例如,树莓派),再使用 pip
安装。
这种方式是将 Compose 当作一个 Python 应用来从 pip 源中安装。
执行安装命令:
1 | sudo pip install -U docker-compose |
可以看到类似如下输出,说明安装成功。
1 | Collecting docker-compose |
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 | from flask import Flask |
Dockerfile
编写 Dockerfile
文件,内容为
1 | FROM python:3.6-alpine |
docker-compose.yml
编写 docker-compose.yml
文件,这个是 Compose 使用的主模板文件。
1 | version: '3' |
运行 compose 项目
1 | docker-compose up |
此时访问本地 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 | docker-compose port web 5000 # 打印web服务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 | version: "3" |
注意每个服务都必须通过 image
指令指定镜像或 build
指令(需要 Dockerfile)等来自动构建生成镜像。
如果使用 build
指令,在 Dockerfile
中设置的选项(例如:CMD
, EXPOSE
, VOLUME
, ENV
等) 将会自动被获取,无需在 docker-compose.yml
中重复设置。
下面分别介绍各个指令的用法。
build
指定 Dockerfile
所在文件夹的路径(可以是绝对路径,或者相对 docker-compose.yml 文件的路径)。 Compose
将会利用它自动构建这个镜像,然后使用这个镜像。
1 | version: '3' |
或
使用 context
指令指定 Dockerfile
所在文件夹的路径;
使用 dockerfile
指令指定 Dockerfile
文件名;
使用 arg
指令指定构建镜像时的变量;
使用 cache_from
指定构建镜像的缓存;
1 | version: '3' |
cap_add, cap_drop
参考: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
指定容器的内核能力(capacity)分配。
例如,让容器拥有所有能力可以指定为:
1 | cap_add: |
去掉 NET_ADMIN 能力可以指定为:
1 | cap_drop: |
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 | devices: |
depends_on
解决容器的依赖、启动先后的问题。以下例子中会先启动 redis
、db
再启动 web
1 | version: '3' |
注意:
web
服务不会等待redis
db
「完全启动」之后才启动。
dns
自定义 DNS
服务器。可以是一个值,也可以是一个列表。
1 | dns: 8.8.8.8 |
dns_search
配置 DNS
搜索域。可以是一个值,也可以是一个列表。
1 | dns_search: example.com |
tmpfs
挂载一个 tmpfs 文件系统到容器。
1 | tmpfs: /run |
env_file
从文件中获取环境变量,可以为单独的文件路径或列表。
如果通过 docker-compose -f FILE
方式来指定 Compose 模板文件,则 env_file
中变量的路径会基于模板文件路径。
如果有变量名称与 environment
指令冲突,则按照惯例,以后者为准。
1 | env_file: .env |
环境变量文件中每一行必须符合格式,支持 #
开头的注释行。
1 | # common.env: Set development environment |
environment
设置环境变量。可以使用数组或字典两种格式。
只给定名称的变量会自动获取运行 Compose 主机上对应变量的值,可以用来防止泄露不必要的数据。
1 | environment: |
如果变量名称或者值中用到 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 | expose: |
external_links
注意:不建议使用该指令。
链接到 docker-compose.yml
外部的容器,甚至并非 Compose
管理的外部容器。
1 | external_links: |
extra_hosts
类似 Docker 中的 --add-host
参数,指定额外的 host 名称映射信息。
1 | extra_hosts: |
会在启动后的服务容器中 /etc/hosts
文件中添加如下两条条目。
1 | 8.8.8.8 googledns |
healthcheck
通过命令检查容器是否健康运行。
1 | healthcheck: |
image
指定为镜像名称或镜像 ID。如果镜像在本地不存在,Compose
将会尝试拉取这个镜像。
1 | image: ubuntu |
labels
为容器添加 Docker 元数据(metadata)信息。例如可以为容器添加辅助说明信息。
1 | labels: |
links
注意:不推荐使用该指令。
logging
配置日志选项。
1 | logging: |
目前支持三种日志驱动类型。
1 | driver: "json-file" |
options
配置日志驱动的相关参数。
1 | options: |
network_mode
设置网络模式。使用和 docker run
的 --network
参数一样的值。
1 | network_mode: "bridge" |
networks
配置容器连接的网络。
1 | version: "3" |
pid
跟主机系统共享进程命名空间。打开该选项的容器之间,以及容器和宿主机系统之间可以通过进程 ID 来相互访问和操作。
1 | pid: "host" |
ports
暴露端口信息。
使用宿主端口:容器端口 (HOST:CONTAINER)
格式,或者仅仅指定容器的端口(宿主将会随机选择端口)都可以。
1 | ports: |
注意:当使用 HOST:CONTAINER
格式来映射端口时,如果你使用的容器端口小于 60 并且没放到引号里,可能会得到错误结果,因为 YAML
会自动解析 xx:yy
这种数字格式为 60 进制。为避免出现这种问题,建议数字串都采用引号包括起来的字符串格式。
secrets
存储敏感数据,例如 mysql
服务密码。
1 | version: "3.1" |
security_opt
指定容器模板标签(label)机制的默认属性(用户、角色、类型、级别等)。例如配置标签的用户名和角色名。
1 | security_opt: |
stop_signal
设置另一个信号来停止容器。在默认情况下使用的是 SIGTERM 停止容器。
1 | stop_signal: SIGUSR1 |
sysctls
配置容器内核参数。
1 | sysctls: |
ulimits
指定容器的 ulimits 限制值。
例如,指定最大进程数为 65535,指定文件句柄数为 20000(软限制,应用可以随时修改,不能超过硬限制) 和 40000(系统硬限制,只能 root 用户提高)。
1 | ulimits: |
volumes
数据卷所挂载路径设置。可以设置为宿主机路径(HOST路径:CONTAINER路径
)或者数据卷名称(VOLUME名称:CONTAINER路径
),并且可以设置访问模式 (HOST路径:CONTAINER路径:ro
)。
该指令中路径支持相对路径。
1 | volumes: |
如果路径为数据卷名称,必须在文件中配置数据卷。
1 | version: "3" |
其它指令
此外,还有包括 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 | domainname: your_website.com |
允许容器中运行一些特权命令。
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 | version: "3" |
如果执行 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 | # 支持 # 号注释 |
执行 docker-compose up
则会启动一个 mongo:3.6
镜像的容器。
参考资料
Docker Compose环境变量
Docker Compose实战
一条命令拉起所有中间件:
踩坑:
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
命令管理:
选择合适的 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 竞争的实力。