为什么要学习Docker?
为了解决环境配置问题
怎样学习使用Docker?
安装Docker
Docker CE 的安装请参考官方文档。Windows版本
什么是Docker?
Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。
Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。
总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。
——阮一峰
就跟git一样,是为了方便我们程序员的powerful tools 之一。
Datawhale的组队学Docker
启航
通过本次docker的组队学习,我们希望你能学到以下几个方面的能力:
- 了解什么是docker
- docker镜像是怎么构建的
- 如何运行一个docker容器
- docker之间的网络通信是怎么样的
- docker中的数据如何做持久化存储
- 如何通过docker compose管理自己的项目
- 如何将自己的个人项目打造成容器化部署的形式
简介与安装
Docker 使用 Google
公司推出的 Go 语言 (opens new window)进行开发实现,基于 Linux
内核的 cgroup (opens new window),namespace (opens new window),以及 OverlayFS (opens new window)类的 Union FS (opens new window)等技术,对进程进行封装隔离,属于 操作系统层面的虚拟化技术 (opens new window)。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 LXC (opens new window),从 0.7
版本以后开始去除 LXC
,转而使用自行开发的 libcontainer (opens new window),从 1.11
版本开始,则进一步演进为使用 runC (opens new window)和 containerd (opens new window)。
比较了 Docker 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。
虚拟机在宿主机(host)中的OS上面是hypervisor(hypervisor),然后依次建立虚拟机,虚拟化的仓库,然后安装程序。但是对于Docker来说,在宿主机(host)中的OS上面是Docker Engine,然后直接在Doker Engine安装应用。
Docker三大基本概念
镜像(
Image
)容器(
Container
)仓库(
Repository
)
Docker镜像
我们都知道,操作系统分为 内核 和 用户空间。对于 Linux
而言,内核启动后,会挂载 root
文件系统为其提供用户空间支持。而 Docker 镜像(Image
),就相当于是一个 root
文件系统。比如官方镜像 ubuntu:18.04
就包含了完整的一套 Ubuntu 18.04 最小系统的 root
文件系统。
Docker 镜像 是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像 不包含 任何动态数据,其内容在构建之后也不会被改变。
分层存储
因为镜像包含操作系统完整的 root
文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS (opens new window)的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是像一个 ISO
那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。
镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。
分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。
关于镜像构建,将会在后续相关章节中做进一步的讲解。
Docker容器
镜像(Image
)和容器(Container
)的关系,就像是面向对象程序设计中的 类
和 实例
一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
容器的实质是进程,但与直接在宿主host执行的进程不同,容器进程运行于属于自己的独立的 命名空间 (opens new window)。
名称空间是Linux 内核的一个特征,它划分了内核资源,使一组过程看到一组资源,而另一组进程看到一组不同的资源。该功能的工作原理是为一组资源和流程设置相同的命名空间,但这些命名空间是指不同的资源。资源可能存在于多个空间中。此类资源示例包括流程 ID、主机名、用户 ID、文件名称以及与网络访问相关的一些名称以及处理间通信。
因此容器可以拥有自己的 root
文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。
前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 容器存储层。
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。
按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者 绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。
Docker Registry
镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。
一个 Docker Registry 中可以包含多个 仓库(Repository
);每个仓库可以包含多个 标签(Tag
);每个标签对应一个镜像。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签>
的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest
作为默认标签。
仓库名经常以 两段式路径 形式出现,比如 jwilder/nginx-proxy
,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry 的软件或服务。
Docker Registry 公开服务
由于某些原因,在国内访问这些服务可能会比较慢。国内的一些云服务商提供了针对 Docker Hub 的镜像服务(Registry Mirror
),这些镜像服务被称为 加速器。常见的有 阿里云加速器 (opens new window)、DaoCloud 加速器 (opens new window)等。使用加速器会直接从国内的地址下载 Docker Hub 的镜像,比直接从 Docker Hub 下载速度会提高很多。在 安装 Docker 一节中有详细的配置方法。
国内也有一些云服务商提供类似于 Docker Hub 的公开服务。比如 网易云镜像服务 (opens new window)、DaoCloud 镜像市场 (opens new window)、阿里云镜像库 (opens new window)等。
私有 Docker Registry
除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry。Docker 官方提供了 Docker Registry (opens new window)镜像,可以直接使用做为私有 Registry 服务。在 私有仓库 一节中,会有进一步的搭建私有 Registry 服务的讲解。
Docker镜像与容器
开始学习前,我C盘已经不够10个G了,不知道能不能撑住。所以……
虚拟机的默认存储位置是C:\Users\Administrator.docker\machine\machines ,后期docke镜像文件会不断增加,为了给系统盘减负,最好将磁盘移动到其他位置。
ps:不想开虚拟机所以就在Windows上折腾了,能hello-world很开心
Docker镜像
获取镜像
从 Docker 镜像仓库获取镜像的命令是 docker pull
。其命令格式为:
$ docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
具体的选项可以通过 docker pull --help
命令看到
比如:
$ docker pull ubuntu:18.04
列出镜像
要想列出已经下载下来的镜像,可以使用 docker image ls
命令。
删除本地镜像
如果要删除本地的镜像,可以使用 docker image rm
命令,其格式为:
$ docker image rm [选项] <镜像1> [<镜像2> ...]
Untagged 和 Deleted
如果观察上面这几个命令的运行输出信息的话,你会注意到删除行为分为两类,一类是 Untagged
,另一类是 Deleted
。我们之前介绍过,镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签。
因此当我们使用上面命令删除镜像的时候,实际上是在要求删除某个标签的镜像。所以首先需要做的是将满足我们要求的所有镜像标签都取消,这就是我们看到的 Untagged
的信息。因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么 Delete
行为就不会发生。所以并非所有的 docker image rm
都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。
当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。这就是为什么,有时候会奇怪,为什么明明没有别的标签指向这个镜像,但是它还是存在的原因,也是为什么有时候会发现所删除的层数和自己 docker pull
看到的层数不一样的原因。
除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。之前讲过,容器是以镜像为基础,再加一层容器存储层,组成这样的多层存储结构去运行的。因此该镜像如果被这个容器所依赖的,那么删除必然会导致故障。如果这些容器是不需要的,应该先将它们删除,然后再来删除镜像。
像其它可以承接多个实体的命令一样,可以使用 docker image ls -q
来配合使用 docker image rm
,这样可以成批的删除希望删除的镜像。我们在“镜像列表”章节介绍过很多过滤镜像列表的方式都可以拿过来使用。
比如,我们需要删除所有仓库名为 redis
的镜像:
$ docker image rm $(docker image ls -q redis)
或者删除所有在 mongo:3.2
之前的镜像:
$ docker image rm $(docker image ls -q -f before=mongo:3.2)
然而我并没有用Linux,而且也没看懂Linux命令
Dockerfile制作镜像
Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
还以之前定制 nginx
镜像为例,这次我们使用 Dockerfile 来定制。
在一个空白目录中,建立一个文本文件,并命名为 Dockerfile
:
$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile
我打开了vm打算装个乌班图重新开始。或者再在Windows上尝试一下
touch好像是Linux命令,我直接在mynginx创建一个文件然后记事本打开输入。这里你可以换成echo test> Dockerfile,就会生成Dockerfile 文件。
其内容为:
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
这个 Dockerfile 很简单,一共就两行。涉及到了两条指令,FROM
和 RUN
。
FROM 指定基础镜像
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx
镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM
就是指定 基础镜像,因此一个 Dockerfile
中 FROM
是必备的指令,并且必须是第一条指令。
在 Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node
、openjdk
、python
、ruby
、golang
等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntu
、debian
、centos
、fedora
、alpine
等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
FROM scratch
...
如果你以 scratch
为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch
会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
RUN 执行命令
RUN
指令是用来执行命令行命令的。由于命令行的强大能力,RUN
指令在定制镜像时是最常用的指令之一。其格式有两种:
- shell 格式:
RUN <命令>
,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的RUN
指令就是这种格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
- exec 格式:
RUN ["可执行文件", "参数1", "参数2"]
,这更像是函数调用中的格式。
既然 RUN
就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:
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
正确的写法应该是这样:
FROM debian:stretch
RUN set -x; 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 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN
一一对应不同的命令,而是仅仅使用一个 RUN
指令,并使用 &&
将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \
的命令换行方式,以及行首 #
进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。
此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt
缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。
Linux命令也能先看看,继续
构建镜像
好了,让我们再回到之前定制的 nginx 镜像的 Dockerfile 来。现在我们明白了这个 Dockerfile 的内容,那么让我们来构建这个镜像吧。
在 Dockerfile
文件所在目录执行:
$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
---> Running in 9cdc27646c7b
---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c
我在这遇到个小问题,ERROR [internal] load metadata for docker.io/library/nginx:latest,怀疑是挂了梯子的原因,关了代理之后就正常了。
从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 Step 2
中,如同我们之前所说的那样,RUN
指令启动了一个容器 9cdc27646c7b
,执行了所要求的命令,并最后提交了这一层 44aa4490ce2c
,随后删除了所用到的这个容器 9cdc27646c7b
。
这里我们使用了 docker build
命令进行镜像构建。其格式为:
docker build [选项] <上下文路径/URL/->
在这里我们指定了最终镜像的名称 -t nginx:v3
,构建成功后,我们可以像之前运行 nginx:v2
那样来运行这个镜像,其结果会和 nginx:v2
一样。
wait for 4 minutes
镜像构建上下文(Context)
如果注意,会看到 docker build
命令最后有一个 .
。.
表示当前目录,而 Dockerfile
就在当前目录,因此不少初学者以为这个路径是在指定 Dockerfile
所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定 上下文路径。那么什么是上下文呢?
首先我们要理解 docker build
的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker
命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker
功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。
当我们进行镜像构建的时候,并非所有定制都会通过 RUN
指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY
指令、ADD
指令等。而 docker build
命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?
这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build
命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
如果在 Dockerfile
中这么写:
COPY ./package.json /app/
这并不是要复制执行 docker build
命令所在的目录下的 package.json
,也不是复制 Dockerfile
所在目录下的 package.json
,而是复制 上下文(context) 目录下的 package.json
。
因此,COPY
这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY ../package.json /app
或者 COPY /opt/xxxx /app
无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。
现在就可以理解刚才的命令 docker build -t nginx:v3 .
中的这个 .
,实际上是在指定上下文的目录,docker build
命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。
如果观察 docker build
输出,我们其实已经看到了这个发送上下文的过程:
$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...
理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 COPY /opt/xxxx /app
不工作后,于是干脆将 Dockerfile
放到了硬盘根目录去构建,结果发现 docker build
执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build
打包整个硬盘,这显然是使用错误。
一般来说,应该会将 Dockerfile
置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore
一样的语法写一个 .dockerignore
,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
那么为什么会有人误以为 .
是指定 Dockerfile
所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile
的话,会将上下文目录下的名为 Dockerfile
的文件作为 Dockerfile。
这只是默认行为,实际上 Dockerfile
的文件名并不要求必须为 Dockerfile
,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php
参数指定某个文件作为 Dockerfile
。
当然,一般大家习惯性的会使用默认的文件名 Dockerfile
,以及会将其置于镜像构建上下文目录中。
更多关于Dockerfile的内容可以移步:Dockerfile详解,不过更多的内容还是大家在实践中逐渐熟悉,这样才能更了解里面的含义。
跨平台构建镜像
在日常的工作中,我们常常有需求将一个程序运行在不同架构CPU的设备上,尤其在嵌入式领域,我们常常接触的各种开发板、路由器往往都是使用ARM架构的芯片,而我们日常开发的设备都是在x86平台。我们在x86平台写的程序需要运行在使用ARM芯片的开发板上,这时候就需要跨CPU构建程序。
总的来说跨平台构建程序有以下几种方式。
- 直接在目标硬件编译 这是最直接的方法
- 使用交叉编译器 交叉编译器是专门为在给定的系统平台上运行而设计的编译器,作用是可以在一种CPU架构上编译出另一个CPU架构的可执行文件。最普遍的例子,开发人员开发安卓应用的时候几乎都在X86的平台上开发构建,但安卓应用很明显是ARM架构的,这其中就是交叉编译器在起作用
- 模拟目标硬件 模拟目标硬件最常见的开源模拟器是QEMU,QEMU是纯软件实现的虚拟化模拟器,几乎可以模拟任何硬件设备,我们最熟悉的就是能够模拟一台能够独立运行操作系统的虚拟机,虚拟机认为自己和硬件打交道
- 通过binfmt_misc模拟目标硬件的用户空间 QEMU 除了可以模拟完整的操作系统之外,还有另外一种模式叫用户态模式(User mod)。该模式下 QEMU 将通过 binfmt_misc 在 Linux 内核中注册一个二进制转换处理程序,并在程序运行时动态翻译二进制文件,根据需要将系统调用从目标 CPU 架构转换为当前系统的 CPU 架构。最终的效果看起来就像在本地运行目标 CPU 架构的二进制文件。 通过 QEMU 的用户态模式,我们可以创建轻量级的虚拟机(chroot 或容器),然后在虚拟机系统中编译程序,和本地编译一样简单轻松。
好像目前我没法实操
构建跨架构的Docker镜像
先跳过了
镜像存储位置
在操作系统中(Linux),默认情况下 Docker 容器的存放位置在 /var/lib/docker 目录下面,可以通过命令查看
docker info | grep "Docker Root Dir"
我们使用docker pull 下载的镜像,都会存在这个目录下,当下载的镜像过多,或容器运行过程中产生大量数据导致存储容量不足时,可以修改镜像储存的位置,有以下几种方式修改docker默认储存位置
使用软链接
- 首先停止docker 进程
- 然后进行链接
#stop
$ sudo systemctl stop docker
#move
$ mv /var/lib/docker /data/docker
#ln
$ ln -sf /data/docker /var/lib/docker
- 然后移动整个 /var/lib/docker 目录到空间比较大的目的路径。这时候启动 Docker 时发现存储目录依旧是 /var/lib/docker 目录,但是实际上是存储在数据盘 /data/docker 上了。
指定容器启动参数
- 在配置文件中指定容器启动的参数 –graph=/var/lib/docker 来指定镜像和容器存放路径。Docker 的配置文件可以设置大部分的后台进程参数,在各个操作系统中的存放位置不一致。在 Ubuntu 中的位置是 /etc/default/docker 文件,在 CentOS 中的位置是 /etc/sysconfig/docker 文件。
#Cent 7
# 更改储存位置
$ vi /usr/lib/systemd/system/docker.service
ExecStart=/usr/bin/dockerd --graph /new-path/docker
sudo systemctl restart docker
- 如果 Docker 的版本是 1.12 或以上的,可以修改或新建 daemon.json 文件。修改后会立即生效,不需重启 Docker 服务。
# 修改配置文件
$ vim /etc/docker/daemon.json
{
"registry-mirrors":
["http://7e61f7f9.m.daocloud.io"],
"graph": "/new-path/docker"
}
System 下创建配置文件
- 在 /etc/systemd/system/docker.service.d 目录下创建一个 Drop-In 文件 docker.conf,默认 docker.service.d 文件夹不存在,必须先创建它。创建 Drop-In 文件的原因,是我们希望 Docker服务使用 docker.conf 文件中提到的特定参数,将默认服务所使用的位于 /lib/systemd/system/docker.service 文件中的参数进行覆盖。
# 定义新的存储位置
$ sudo vi /etc/systemd/system/docker.service.d/docker.conf
[Service]
ExecStart=/usr/bin/dockerd --graph="/data/docker" --storage-driver=devicemapper
# 重启
$ sudo systemctl start docker
- /data/docker 就是新的存储位置,而 devicemapper 是当前 Docker 所使用的存储驱动。如果你的存储驱动有所不同,请输入之前第一步查看并记下的值。现在,你可以重新加载服务守护程序,并启动 Docker 服务了,这将改变新的镜像和容器的存储位置。为了确认一切顺利,运行 docker info 命令检查 Docker 的根目录。
因为docker pull 太慢所以待到图书馆关门,保安叔叔来赶人
Docker容器
容器是 Docker 又一核心概念。简单的说,容器是独立运行的一个或一组应用,以及它们的运行态环境。
本节将具体介绍如何来管理一个容器,包括创建、启动和停止等。
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(exited
)的容器重新启动。
因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。
新建并启动容器
所需要的命令主要为 docker run
。其中,-t
选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i
则让容器的标准输入保持打开。
docker run [-i -t -d -p -P -c] [--name]:在容器内运行一个应用程序
-t :在新容器内指定一个伪终端或终端
-i:允许你对容器内的标准输入进行交互
-d:以进程方式运行容器,让容器在后台运行
-p:设置端口
-P:将容器内部使用的网络端口映射到我们使用的主机,就是让我们访问我们使用的主机就等同于访问到容器内部
-c:command,后面接命令
--name container name:指定容器名字
当利用 docker run
来创建容器时,Docker 在后台运行的标准操作包括:
- 检查本地是否存在指定的镜像,不存在就从registry下载
- 利用镜像创建并启动一个容器
- 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
- 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
- 从地址池配置一个 ip 地址给容器
- 执行用户指定的应用程序
- 执行完毕后容器被终止
启动已终止的容器
可以利用 docker container start
命令,直接将一个已经终止(exited
)的容器启动运行。
容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 ps
或 top
来查看进程信息。
停止容器
docker stop可以停止运行的容器。理解:容器在docker host中实际上是一个进程,docker stop命令本质上是向该进程发送一个SIGTERM信号。如果想要快速停止容器,可使用docker kill命令,其作用是向容器进程发送SIGKILL信号。
重启容器
对于已经处于停止状态的容器,可以通过docker start重新启动。
$ docker start bdf593fda8be
bdf593fda8be
docker start会保留容器的第一次启动时的所有参数。docker restart可以重启容器,其作用就是依次执行docker stop和docker start。容器可能因某种错误而停止运行。对于服务类容器,通常希望它能够自动重启。启动容器时设置–restart就可以达到效果。–restart=always意味着无论容器因何种原因退出(包括正常退出),都立即重启;
$ docker run -it ubuntu:15.10 /bin/echo --restart=always -d "Hello world"
--restart=always -d Hello world
后台运行容器
更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d
参数来实现。下面举两个例子来说明一下。
如果不使用 -d
参数运行容器。
$ docker run ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
hello world
hello world
hello world
hello world
容器会把输出的结果 (STDOUT) 打印到宿主机上面
如果使用了 -d
参数运行容器。
$ docker run -d ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a
此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 docker logs
查看)。
注: 容器是否会长久运行,是和 docker run
指定的命令有关,和 -d
参数无关,只要命令不结束,容器也就不会退出。上述命令中,while语句不会让bash退出,因此该容器就不会退出。
使用 -d
参数启动后会返回一个唯一的 id,也可以通过 docker container ls
命令来查看容器信息。
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
77b2dc01fe0f ubuntu:18.04 /bin/sh -c 'while tr 2 minutes ago Up 1 minute agitated_wright
使用-d启动容器后,会回到host终端;此时如果想要获取容器的输出信息,可以通过 docker container logs
命令。
$ docker container logs [container ID or NAMES]
hello world
hello world
hello world
. . .
进入容器
在使用 -d
参数时,容器启动后会进入后台,启动完容器之后会停在host端;某些时候需要进入容器进行操作,包括使用 docker attach
命令或 docker exec
命令,推荐大家使用 docker exec
命令,原因会在下面说明。
attach
命令
下面示例如何使用 docker attach
命令。
$ docker run -dit ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
243c32535da7 ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds nostalgic_hypatia
$ docker attach 243c
root@243c32535da7:/#
注意: 如果从这个 stdin 中exit回到host端,会导致容器的停止。
exec
命令
docker exec
后边可以跟多个参数,这里主要说明 -i
-t
参数。
只用 -i
参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。
当 -i
-t
参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。
$ docker run -dit ubuntu
69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
69d137adef7a ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds zealous_swirles
$ docker exec -i 69d1 bash
ls
bin
boot
dev
...
$ docker exec -it 69d1 bash
root@69d137adef7a:/#
如果从这个 stdin 中 exit回到host端,但不会导致容器的停止。这就是为什么推荐大家使用 docker exec
的原因。
attach和exec的区别
attach和exec的区别: (1)attach直接进入容器启动命令的终端,不会启动新的进程; (2)exec则是在容器中打开新的终端,并且可以启动新的进程; (3)如果想直接在终端中查看命令的输出,用attach,其他情况使用exec;
暂停容器
有时我们只是希望让容器暂停工作一段时间,比如要对容器的文件系统打个快照,或者docker host需要使用CPU,可以执行:docker pause CONTAINER [CONTAINER…]
删除容器
可以使用 docker container rm
来删除一个处于终止状态的容器。例如
$ docker container rm trusting_newton
trusting_newton
如果要删除一个运行中的容器,可以添加 -f
参数。Docker 会发送 SIGKILL
信号给容器。
清理所有处于终止状态的容器
用 docker container ls -a
命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。
$ docker container prune
####批量删除所有已经退出的容器
$ docker rm -v $(docker ps -aq -f status=exited)
导出容器
如果要导出本地某个容器,可以使用 docker export
命令。
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7691a814370e ubuntu:18.04 "/bin/bash" 36 hours ago Exited (0) 21 hours ago test
$ docker export 7691a814370e > ubuntu.tar
这样将导出容器快照到本地文件。
导入容器
可以使用 docker import
从容器快照文件中再导入为镜像,例如
$ cat ubuntu.tar | docker import - test/ubuntu:v1.0
cmd试一下用type替代,不行,直接导入,成功
此外,也可以通过指定 URL 或者某个目录来导入,例如
$ docker import http://example.com/exampleimage.tgz example/imagerepo
注:用户既可以使用 docker load
来导入镜像存储文件到本地镜像库,也可以使用 docker import
来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。
Docker 数据管理
数据卷
数据卷是一个可供一个或多个容器使用的特殊目录,它绕过 UFS (UNIX File System) ,可以提供很多有用的特性:
注意:数据卷的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会复制到数据卷中(仅数据卷为空时会复制)。
创建数据卷
$ docker volume create datawhale
查看所有的数据卷
$ docker volume ls
在主机里使用以下命令可以查看指定数据卷的信息
$ docker volume inspect datawhale
启动一个挂载数据卷的容器
在用 docker run
命令的时候,使用 --mount
标记来将数据卷挂载到容器里。在一次 docker run
中可以挂载多个 数据卷
。
下面创建一个名为 web
的容器,并加载一个数据卷到容器的 /usr/share/nginx/html
目录。
$ docker run -d -P \
--name web \
--mount source=datawhale,target=/usr/share/nginx/html \
nginx:alpine
–-mount参数说明:
source :数据卷
target :是容器内文件系统挂载点
注意,可以不需要提前创建好数据卷,直接在运行容器的时候mount 这时如果不存在指定的数据卷,docker会自动创建,自动生成。
查看数据卷的具体信息
在主机里使用以下命令可以查看 web
容器的信息
$ docker inspect web
删除数据卷
$ docker volume rm datawhale #datawhale为卷名
数据卷是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷
,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷
。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v
这个命令。
无主的数据卷可能会占据很多空间,要清理请使用以下命令
$ docker volume prune
挂载主机目录
挂载一个主机目录作为数据卷
使用 --mount
标记可以指定挂载一个本地主机的目录到容器中去。
$ docker run -d -P \
--name web \
--mount type=bind,source=/src/webapp,target=/usr/share/nginx/html \
nginx:alpine
上面的命令加载主机的 /src/webapp
目录到容器的 /usr/share/nginx/html
目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,以前使用 -v
参数时如果本地目录不存在 Docker 会自动为你创建一个文件夹,现在使用 --mount
参数时如果本地目录不存在,Docker 会报错。
Docker 挂载主机目录的默认权限是 读写
,用户也可以通过增加 readonly
指定为 只读
。
注意: 如果挂载的目录不存在,创建容器时,docker 不会自动创建,此时会报错
$ docker run -d -P \
--name web \
--mount type=bind,source=/src/webapp,target=/usr/share/nginx/html,readonly \
nginx:alpine
源路径不存在,source是本地路径,然后前面启动了一个叫web的容器了,不能再启动
加了 readonly
之后,就挂载为 只读
了。如果你在容器内 /usr/share/nginx/html
目录新建文件,会显示如下错误
/usr/share/nginx/html # touch new.txt
touch: new.txt: Read-only file system
查看数据卷的具体信息
在主机里使用以下命令可以查看 web
容器的信息
$ docker inspect web
挂载一个本地主机文件作为数据卷
--mount
标记也可以从主机挂载单个文件到容器中
$ docker run --rm -it \
--mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \
ubuntu:18.04 \
bash
Docker 网络
Docker 基础网络介绍
外部访问容器
容器中可以运行一些网络应用,要让外部也可以访问这些应用,可以通过-P
或-p
参数来指定端口映射。
当使用-P
标记时,Docker
会随机映射一个端口到内部容器开放的网络端口。 使用docker container ls
可以看到,本地主机的 32768 被映射到了容器的 80 端口。此时访问本机的 32768 端口即可访问容器内 NGINX 默认页面。
同样的,可以通过docker logs
命令来查看访问记录。
$ docker logs [container name]
D:\mynginx>docker logs 64a2
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
-p
则可以指定要映射的端口,并且,在一个指定端口上只可以绑定一个容器。支持的格式有ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort
.
映射所有接口地址
使用hostPort:containerPort
格式本地的 80 端口映射到容器的 80 端口,可以执行
$ docker run -d -p 80:80 nginx:alpine
此时默认会绑定本地所有接口上的所有地址。
映射到指定地址的指定端口
可以使用ip:hostPort:containerPort
格式指定映射使用一个特定地址,比如localhost
地址127.0.0.1
$ docker run -d -p 127.0.0.1:80:80 nginx:alpine
映射到指定地址的任意端口
使用ip::containerPort
绑定localhost
的任意端口到容器的80端口,本地主机会自动分配一个端口。
$ docker run -d -p 127.0.0.1::80 nginx:alpine
还可以使用udp
标记来指定udp
端口
$ docker run -d -p 127.0.0.1:80:80/udp nginx:alpine
查看映射端口配置
使用docker port
来查看当前映射的端口配置,也可以查看到绑定的地址
$ docker port fa 80
0.0.0.0:32768
注意: 容器有自己的内部网络和 ip 地址(使用docker inspect
查看,Docker
还可以有一个可变的网络配置。) -p
标记可以多次使用来绑定多个端口
例如
$ docker run -d \
-p 80:80 \
-p 443:443 \
nginx:alpine
容器互联
新建网络
下面先创建一个新的 Docker
网络。
$ docker network create -d bridge my-net
-d
参数指定Docker
网络类型,有bridge overlay
,其中overlay
网络类型用于Swarm mode
,在本小节中你可以忽略它。
连接容器
运行一个容器并连接到新建的my-net
网络
$ docker run -it --rm --name busybox1 --network my-net busybox sh
打开新的终端,再运行一个容器并加入到 my-net
网络
$ docker run -it --rm --name busybox2 --network my-net busybox sh
-rm是退出后删除了容器,所以我要开两个终端
再打开一个新的终端查看容器信息
$ 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
容器建立了互联关系。
Docker Compose
如果你有多个容器之间需要互相连接,推荐使用Docker
Compose。
配置DNS
如何自定义配置容器的主机名和 DNS 呢?秘诀就是Docker
利用虚拟文件来挂载容器的 3个相关配置文件。
在容器中使用 mount
命令可以看到挂载信息:
$ mount
/dev/disk/by-uuid/1fec...ebdf on /etc/hostname type ext4 ...
/dev/disk/by-uuid/1fec...ebdf on /etc/hosts type ext4 ...
tmpfs on /etc/resolv.conf type tmpfs ...
这种机制可以让宿主主机 DNS 信息发生更新后,所有Docker
容器的 DNS 配置通过 /etc/resolv.conf
文件立刻得到更新。
配置全部容器的 DNS ,也可以在 /etc/docker/daemon.json
文件中增加以下内容来设置。
{
"dns" : [
"114.114.114.114",
"8.8.8.8"
]
}
这样每次启动的容器 DNS 自动配置为 114.114.114.114 和8.8.8.8。使用以下命令来证明其已经生效。
$ docker run -it --rm ubuntu:18.04 cat etc/resolv.conf
nameserver 114.114.114.114
nameserver 8.8.8.8
cat是Linux连接文件并打印到标准输出设备上的命令,win上可以好像用type命令
如果用户想要手动指定容器的配置,可以在使用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
。
关于DNS还有一些不明白
注意:如果在容器启动时没有指定最后两个参数,Docker
会默认用主机上的/etc/resolv.conf
来配置容器。
Docker的网络模式
可以通过docker network ls
查看网络
常见网络的含义:
网络模式 | 简介 |
---|---|
Bridge | 为每一个容器分配、设置 IP 等,并将容器连接到一个 docker0 虚拟网桥,默认为该模式。 |
Host | 容器将不会虚拟出自己的网卡,配置自己的 IP 等,而是使用宿主机的 IP 和端口。 |
None | 容器有独立的 Network namespace,但并没有对其进行任何网络设置,如分配 veth pair 和网桥连接,IP 等。 |
Container | 新创建的容器不会创建自己的网卡和配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。 |
上学期机组课上虚拟机间通网老师让我们看过各种桥接模式,当时没看懂,之后要好好看看计算机网络了,现在先把看不懂的名词记下来
Bridge 模式
虚拟网桥是什么?网卡?
二层网络?子网分配ip?虚拟网卡两端放在容器和主机?
守护进程?
对于每个容器的 IP 地址和 Gateway 信息,可以通过 docker inspect 容器名称|ID
进行查看
可以通过 docker network inspect bridge
查看所有 bridge
网络模式下的容器,在 Containers
节点中可以看到容器名称
关于 bridge
网络模式的使用,只需要在创建容器时通过参数 --net bridge
或者 --network bridge
指定即可,当然这也是创建容器默认使用的网络模式,也就是说这个参数是可以省略的。
iptables
做了DNAT
规则,实现端口转发功能?
以使用iptables -t nat -vnL查看。演示:
$ docker run -tid --net=bridge --name docker_bri1 \
ubuntu-base:v3
docker run -tid --net=bridge --name docker_bri2 \
ubuntu-base:v3
$ brctl show
$ docker exec -ti docker_bri1 /bin/bash
$ ifconfig –a
$ route –n
还有一些细节不明白,命令完全不明白在干嘛,暂时用不上所以先放放
Host 模式
通过参数 --net host
或者 --network host
指定
可以直接使用宿主机的 IP 地址与外界进行通信,若宿主机的 eth0 是一个公有 IP,那么容器也拥有这个公有 IP。同时容器内服务的端口也可以使用宿主机的端口,无需额外进行 NAT 转换;
是不是说我把container扔到服务器上去就可以直接用服务器ip的意思?
不会获得一个独立的Network Namespace
,而是和宿主机共用一个Network Namespace
None 模式
通过参数 --net none
或者 --network none
指定;
只有 lo 接口 local 的简写,代表 127.0.0.1
localhost 本地环回接口?loopback 网络设备?
需要自己为 Docker
容器添加网卡、配置 IP 等
“少即是多”
Container 模式
在创建容器时通过参数 --net container:已运行的容器名称|ID
或者 --network container:已运行的容器名称|ID
指定;
共享一个网络栈?
这样两个容器之间可以使用 localhost 高效快速通信。
Container 网络模式即新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。
Docker高级网络配置
快速配置指南
其中有些命令选项只有在 Docker 服务启动的时候才能配置,而且不能马上生效。
-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服务启动的时候指定则会成为默认值,后面执行 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
映射容器所有端口到宿主主机
容器访问控制
iptables
是 Linux 上默认的防火墙软件,在大部分发行版中都自带。机组课上用过。
容器要想访问外部网络,需要本地系统的转发支持。在Linux 系统中,检查转发是否打开。
$sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
如果为 0,说明没有开启转发,则需要手动打开。
$sysctl -w net.ipv4.ip_forward=1
如果在启动 Docker 服务的时候设定 --ip-forward=true
, Docker 就会自动设定系统的 ip_forward
参数为 1。
容器之间相互访问,需要两方面的支持。
- 容器的网络拓扑是否已经互联。默认情况下,所有容器都会被连接到
docker0
网桥上。 - 本地系统的防火墙软件
-- iptables
是否允许通过。
当启动 Docker 服务(即 dockerd)的时候,默认会添加一条转发策略到本地主机 iptables 的 FORWARD 链上。策略为通过(ACCEPT
)还是禁止(DROP
)取决于配置--icc=true
(缺省值)还是 --icc=false
。当然,如果手动指定 --iptables=false
则不会添加 iptables
规则。
可见,默认情况下,不同容器之间是允许网络互通的。如果为了安全考虑,可以在 /etc/docker/daemon.json
文件中配置 {"icc": false}
来禁止它。
在通过 -icc=false
关闭网络访问后,还可以通过 --link=CONTAINER_NAME:ALIAS
选项来访问容器的开放端口。
例如,在启动 Docker 服务时,可以同时使用 icc=false --iptables=true
参数来关闭允许相互的网络访问,并让 Docker 可以修改系统中的 iptables
规则。
启动容器(docker run
)时使用 --link=CONTAINER_NAME:ALIAS
选项。Docker 会在 iptable
中为 两个容器分别添加一条 ACCEPT
规则,允许相互访问开放的端口(取决于 Dockerfile
中的 EXPOSE
指令)。
端口映射实现
默认情况下,容器可以主动访问到外部网络的连接,但是外部网络无法访问到容器。
容器所有到外部网络的连接,源地址都会被 NAT 成本地系统的 IP 地址。这是使用 iptables
的源地址伪装操作实现的。
查看主机的 NAT 规则。
$ sudo iptables -t nat -nL
将所有源地址在 172.17.0.0/16
网段,目标地址为其他网段(外部网络)的流量动态伪装为从系统网卡发出
端口映射实现
容器允许外部访问,可以在 docker run
时候通过 -p
或 -P
参数来启用。
开放端口
其实也是在本地的 iptable
的 nat 表中添加相应的规则。
- 永久绑定到某个固定的 IP 地址,可以在 Docker 配置文件
/etc/docker/daemon.json
中添加如下内容。
{
"ip": "0.0.0.0"
}
配置docker0网桥
它在内核层连通了其他的物理或虚拟网卡,这就将所有容器和本地主机都放到同一个物理网络。
MTU(接口允许接收的最大传输单元),通常是 1500 Bytes,或宿主主机网络路由上支持的默认值。这些值都可以在服务启动的时候进行配置。
--bip=CIDR
IP 地址加掩码格式,例如 192.168.1.5/24--mtu=BYTES
覆盖默认的 Docker mtu 配置
由于目前 Docker 网桥是 Linux 网桥,用户可以使用 brctl show
来查看网桥和端口连接信息。
$ sudo brctl show
注:brctl
命令在 Debian、Ubuntu 中可以使用 sudo apt-get install bridge-utils
来安装。
每次创建一个新容器的时候,Docker 从可用的地址段中选择一个空闲的 IP 地址分配给容器的 eth0 端口。使用本地主机上 docker0
接口的 IP 作为所有容器的默认网关。
自定义网桥
在启动 Docker 服务的时候,使用 -b BRIDGE
或--bridge=BRIDGE
来指定使用的网桥。
如果服务已经运行,那需要先停止服务,并删除旧的网桥。
$ sudo systemctl stop docker
$ sudo ip link set dev docker0 down
$ sudo brctl delbr docker0
都没法在Windows上实现一遍了
然后创建一个网桥 bridge0
。
$ sudo brctl addbr bridge0
$ sudo ip addr add 192.168.5.1/24 dev bridge0
$ sudo ip link set dev bridge0 up
查看确认网桥创建并启动。
在 Docker 配置文件 /etc/docker/daemon.json
中添加如下内容,即可将 Docker 默认桥接到创建的网桥上。
{
"bridge": "bridge0",
}
启动 Docker 服务。
新建一个容器,可以看到它已经桥接到了 bridge0
上。
可以继续用 brctl show
命令查看桥接的信息。另外,在容器中可以使用 ip addr
和 ip route
命令来查看 IP 地址配置和路由信息。
工具和示例
pipework,shell 脚本,可以帮助用户在比较复杂的场景中完成容器的连接。
playground,一个提供完整的 Docker 容器网络拓扑管理的 Python库,包括路由、NAT 防火墙;以及一些提供 HTTP
SMTP
POP
IMAP
Telnet
SSH
FTP
的服务器。
编辑网络配置文件
Docker 1.2.0 开始支持在运行中的容器里编辑 /etc/hosts
, /etc/hostname
和 /etc/resolv.conf
文件。
但是这些修改是临时的,只在运行的容器中保留,容器终止或重启后并不会被保存下来,也不会被 docker commit
提交。
实例:创建一个点到点连接
首先启动 2 个容器:
$ docker run -i -t --rm --net=none base /bin/bash
root@1f1f4c1f931a:/#
$ docker run -i -t --rm --net=none base /bin/bash
root@12e343489d2f:/#
在两个终端启动吗?
找到进程号,然后创建网络命名空间的跟踪文件。
$ docker inspect -f '{{.State.Pid}}' 1f1f4c1f931a
2989
$ docker inspect -f '{{.State.Pid}}' 12e343489d2f
3004
$ sudo mkdir -p /var/run/netns
$ sudo ln -s /proc/2989/ns/net /var/run/netns/2989
$ sudo ln -s /proc/3004/ns/net /var/run/netns/3004
绿色的是端口号?ln用于硬连接 ln -s软连接?什么是创建接口?
创建一对 peer
接口,然后配置路由
$ sudo ip link add A type veth peer name B
$ sudo ip link set A netns 2989
$ sudo ip netns exec 2989 ip addr add 10.1.1.1/32 dev A
$ sudo ip netns exec 2989 ip link set A up
$ sudo ip netns exec 2989 ip route add 10.1.1.2/32 dev A
$ sudo ip link set B netns 3004
$ sudo ip netns exec 3004 ip addr add 10.1.1.2/32 dev B
$ sudo ip netns exec 3004 ip link set B up
$ sudo ip netns exec 3004 ip route add 10.1.1.1/32 dev B
相互 ping 通,并成功建立连接。点到点链路不需要子网和子网掩码。
此外,也可以不指定 --net=none
来创建点到点链路。这样容器还可以通过原先的网络来通信。
利用类似的办法,可以创建一个只跟主机通信的容器。但是一般情况下,更推荐使用 --icc=false
来关闭容器之间的通信。
后面完全是因为基本功没到位看不懂了
Docker Compose
先手资料
awesome-compose这个项目也非常推荐。
推荐大家多看看一些项目的docker-compose.yml文件是怎么写的,慢慢模仿着去写很多就越来越熟练清晰了。
什么是docker compose
它允许用户通过一个单独的 docker-compose.yml
模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)
如何使用docker compose
在Compose
中有两个重要的概念:
- 服务 (
service
):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。 - 项目 (
project
):由一组关联的应用容器组成的一个完整业务单元,在docker-compose.yml
文件中定义。
web应用
基本使用
启动服务
docker-compose up
命令后会自动接一个默认值-f docker-compose.yml
,也就是默认是使用docker-compose.yml文件的。我们也可以给文件起名为docke-test.yml
,这样在使用时指定文件名,但是为了符合规范,还是统一为docker-compose.yml
。
docker-compose up -f docer-test.yml
-d参数让启动时的输出不会打印到终端
查看服务状态
docker-compose ps
要是想要查看所有service的状态可以使用-a参数
停止或删除服务
docker-compose stop
docker-compose down
其中stop是直接停止services,而down则会停止并删除创建的service,volume和network。
进入服务
docker-compose exec mysql bash
查看服务输出日志
docker-compose logs
Compose模板文件
文件格式为 YAML 格式。
version: "3"
services:
webapp:
image: examples/web
ports:
- "80:80"
volumes:
- "/data"
每个服务都必须通过 image
指令指定镜像或 build
指令(需要 Dockerfile)等来自动构建生成镜像。
build
指定 Dockerfile
所在文件夹的路径(可以是绝对路径,或者相对 docker-compose.yml 文件的路径)。 Compose
将会利用它自动构建这个镜像,然后使用这个镜像。
context
指令指定 Dockerfile
所在文件夹的路径
使用 dockerfile
指令指定 Dockerfile
文件名。
使用 arg
指令指定构建镜像时的变量。
使用 cache_from
指定构建镜像的缓存。
depends_on
解决容器的依赖、启动先后的问题。
environment
设置环境变量。你可以使用数组或字典两种格式。
只给定名称的变量会自动获取运行 Compose 主机上对应变量的值,可以用来防止泄露不必要的数据。
如果变量名称或者值中用到 true|false,yes|no
等表达 布尔 (opens new window)含义的词汇,最好放到引号里,避免 YAML 自动解析某些内容为对应的布尔语义。这些特定词汇,包括
y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF
expose
暴露端口,但不映射到宿主机,只被连接的服务访问。仅可以指定内部端口为参数
ports
暴露端口信息。使用宿主端口:容器端口 (HOST:CONTAINER)
格式,或者仅仅指定容器的端口(宿主将会随机选择端口)都可以。
secrets存储敏感数据,例如 mysql
服务密码。
image指定为镜像名称或镜像 ID。
labels为容器添加 Docker 元数据(metadata)信息。例如可以为容器添加辅助说明信息。
network_mode设置网络模式。使用和 docker run
的 --network
参数一样的值。
networks配置容器连接的网络。
volumes
数据卷所挂载路径设置。可以设置为宿主机路径(HOST:CONTAINER
)或者数据卷名称(VOLUME:CONTAINER
),并且可以设置访问模式 (HOST:CONTAINER:ro
)。该指令中路径支持相对路径。
如果路径为数据卷名称,必须在文件中配置数据卷。
Compose命令
docker-compose
命令的基本的使用格式是
docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]
f, --file FILE
指定使用的 Compose 模板文件,默认为docker-compose.yml
,可以多次指定。-p, --project-name NAME
指定项目名称,默认将使用所在目录名称作为项目名。--verbose
输出更多调试信息。-v, --version
打印版本并退出。
命令 | 作用 |
---|---|
build | 格式为 docker-compose build [options] [SERVICE...] 。构建(重新构建)项目中的服务容器。 |
config | 验证 Compose 文件格式是否正确 |
down | 此命令将会停止 up 命令所启动的容器,并移除网络 |
exec | 进入指定的容器。 |
help | 帮助 |
image | 列出 Compose 文件中包含的镜像 |
kill | 格式为 docker-compose kill [options] [SERVICE...] 。通过发送 SIGKILL 信号来强制停止服务容器。支持通过 -s 参数来指定发送的信号 |
logs | 查看服务容器的输出。默认情况下,docker-compose 将对不同的服务输出使用不同的颜色来区分。可以通过 --no-color 来关闭颜色。该命令在调试问题的时候十分有用。 |
pause | 暂停一个服务容器。 |
port | 打印某个容器端口所映射的公共端口。 |
ps | 列出项目中目前的所有容器。 |
pull | 拉取服务依赖的镜像。–ignore-pull-failures忽略拉取镜像过程中的错误。 |
push | 推送服务 |
restart | 重启项目中的服务。 |
rm | 删除所有(停止状态的)服务容器。推荐先执行 docker-compose stop 命令来停止容器。-f, --force 强制直接删除 -v删除容器所挂载的数据卷 |
start | 启动 |
stop | 停止已经处于运行状态的容器,但不删除它 |
top | 查看各个服务容器内运行的进程 |
unpause | 恢复处于暂停状态中的服务 |
up | 尝试自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一系列操作。-d ,将会在后台启动并运行所有的容器 |
up选项:
-d
在后台运行服务容器。--no-color
不使用颜色来区分不同的服务的控制台输出。--no-deps
不启动服务所链接的容器。--force-recreate
强制重新创建容器,不能与--no-recreate
同时使用。--no-recreate
如果容器已经存在了,则不重新创建,不能与--force-recreate
同时使用。--no-build
不自动构建缺失的服务镜像。-t, --timeout TIMEOUT
停止容器时候的超时(默认为 10 秒)。
扩缩容
docker-compose up --scale web=3 -d
理想情况下这三个web会同时对外提供服务,以减轻访问单个容器的压力。但是我们在上面也看到了因为大家都是绑定的5000端口,这样端口就冲突了,导致新创建的两个web服务都是Exit的状态,对于这个问题我们可以通过HAProxy来解决。
HAProxy?等我学完网络再来看能不能PR好了
综合实践
WordPress
看github上的教程没搞懂这次打卡具体要做什么,如果自己写dockfile估计时间不够了,所以参照阮一峰的docker实战尝试在服务器上做点东西出来好了。
在服务器上装docker,见参考资料
首先还要更新一下yum,遇到点小问题,csdn解决。
yum更新失败:rpmdb: BDB0113 Thread/process 2673/140126198814528 failed: BDB1507 Thread died…
服务器一边还在进行一些其他任务,所以在本地同时开工。
由于时间关系这次就不自建WordPress容器了,直接采取最简单的方法,使用官方提供的容器。
首先,新建并启动 MySQL 容器。
$ docker container run \ -d \ --rm \ --name wordpressdb \ --env MYSQL_ROOT_PASSWORD=123456 \ --env MYSQL_DATABASE=wordpress \ mysql:5.7
然后,基于官方的 WordPress image,新建并启动 WordPress 容器。
$ docker container run \ -d \ --rm \ --name wordpress \ --env WORDPRESS_DB_PASSWORD=123456 \ --link wordpressdb:mysql \ wordpress
上面命令中,各个参数的含义前面都解释过了,其中环境变量WORDPRESS_DB_PASSWORD
是 MySQL 容器的根密码。
上面命令指定wordpress
容器在后台运行,导致前台看不见输出,使用下面的命令查出wordpress
容器的 IP 地址。
$ docker container inspect wordpress
上面命令运行以后,会输出很多内容,找到IPAddress
字段即可。我的机器返回的 IP 地址是172.17.0.3
。
浏览器访问172.17.0.3
,就会看到 WordPress 的安装提示。
然而我打不开172.17.0.3
,不知道是不是打开方式不对。
到了上一步,官方 WordPress 容器的安装就已经成功了。但是,这种方法有两个很不方便的地方。
- 每次新建容器,返回的 IP 地址不能保证相同,导致要更换 IP 地址访问 WordPress。
- WordPress 安装在容器里面,本地无法修改文件。
解决这两个问题很容易,只要新建容器的时候,加两个命令行参数就可以了。
先把刚才启动的 WordPress 容器终止(容器文件会自动删除)。
$ docker container stop wordpress
然后,使用下面的命令新建并启动 WordPress 容器。
$ docker container run \ -d \ -p 127.0.0.2:8080:80 \ --rm \ --name wordpress \ --env WORDPRESS_DB_PASSWORD=123456 \ --link wordpressdb:mysql \ --volume "$PWD/wordpress":/var/www/html \ wordpress
上面的命令跟前面相比,命令行参数只多出了两个。
-p 127.0.0.2:8080:80
:将容器的 80 端口映射到127.0.0.2
的8080
端口。--volume "$PWD/wordpress":/var/www/html
:将容器的/var/www/html
目录映射到当前目录的wordpress
子目录。
浏览器访问127.0.0.2:8080:80
就能看到 WordPress 的安装提示了。而且,你在wordpress
子目录下的每次修改,都会反映到容器里面。
最后,终止这两个容器(容器文件会自动删除)。
$ docker container stop wordpress wordpressdb
第一次实际实用docker
教你用AI Studio+wechaty+阿里云白嫖一个智能微信机器人
资料
关于安装问题
关于服务器装docker