Docker入门
docker是一个开源的应用容器引擎,基于Go语言开发并遵循apache2.0协议开源.docker可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的linux服务器,也可以实现虚拟化。
Docker特点
Docker容器特点有:
- 性能好.和物理机几乎一样.
- 部署、交付简单.
- 跨平台可迁移.
- 容器启动速度快.
容器和虚拟机
虚拟机和容器看起来很像,运行的应用并不直接运行在物理机上而是运行在它们提供的环境上.但在技术上,虚拟机和容器差别很大.
虚拟机通过在操作系统上建立了一个中间虚拟软件层 Hypervisor ,并利用物理机器的资源虚拟出多个虚拟硬件环境来共享宿主机的资源,其中的应用运行在虚拟机内核上.虚拟机有自己的操作系统,通过操作系统去使用物理机资源.因为应用并不是直接使用物理机资源,导致虚拟机运行的应用对物理资源的使用率不高的.
Docker 容器不使用硬件虚拟化,它的守护进程是宿主机上的一个进程,换句话说,应用直接运行在宿主机内核上。因为容器中运行的程序和计算机的操作系统之间没有额外的中间层,没有资源被冗余软件的运行或虚拟硬件的模拟而浪费掉。
安装
Mac下安装Docker是很简单的.有两种方式:
Homebrew
1 | $ brew cask install docker |
手动安装
从官网下载安装包,图形界面操作安装
Docker核心组成
镜像
Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
image 是二进制文件。实际开发中,一个 image 文件往往通过继承另一个 image 文件,加上一些个性化设置而生成。举例来说,你可以在 Ubuntu 的 image 基础上,往里面加入 Apache 服务器,形成你的 image。
Docker 的镜像就是它的文件系统,一个镜像可以放在另外一个镜像的上层,那么位于下层的就是它的父镜像。所以,Docker 会存在很多镜像层,每个镜像层都是只读的,并且不会改变。当我们创建一个新的容器时,Docker 会构建出一个镜像栈,并在栈的最顶层添加一个读写层,如图所示。
容器
镜像(Image
)和容器(Container
)的关系,就像是面向对象程序设计中的 类
和 实例
一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root
文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。
仓库
镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。一个 Docker Registry 中可以包含多个仓库;每个仓库可以包含多个标签;每个标签对应一个镜像,其中标签可以理解为镜像的版本号。
仓库是用于存放镜像。我们可以从中心仓库下载镜像,也可以从自建仓库下载。同时,我们可以把制作好的镜像从本地推送到远程仓库。
常用命令
查看Docker版本信息
1 | $ docker version |
获取镜像
从 Docker 镜像仓库获取镜像的命令是 docker pull
。其命令格式为:
1 | docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] |
具体的选项可以通过 docker pull --help
命令看到,这里我们说一下镜像名称的格式。
- Docker 镜像仓库地址:地址的格式一般是
<域名/IP>[:端口号]
。默认地址是 Docker Hub。 - 仓库名:如之前所说,这里的仓库名是两段式名称,即
<用户名>/<软件名>
。对于 Docker Hub,如果不给出用户名,则默认为library
,也就是官方镜像。
列出本地镜像
列出已经下载下来的镜像,可以使用 docker image ls
命令。
1 | $ docker image ls |
列表包含了 仓库名
、标签
、镜像 ID
、创建时间
以及 所占用的空间
。
镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个 标签。因此,在上面的例子中,我们可以看到 ubuntu:18.04
和 ubuntu:latest
拥有相同的 ID,因为它们对应的是同一个镜像。
虚悬镜像
1 | <none> <none> 00285df0df87 5 days ago 342 MB |
既没有仓库名,也没有标签,均为 <none>
的镜像被称为虚悬镜像.
这个镜像原本是有镜像名和标签的,原来为 mongo:3.2
,随着官方镜像维护,发布了新版本后,重新 docker pull mongo:3.2
时,mongo:3.2
镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 <none>
。
查看虚悬镜像:
1 | $ docker image ls -f dangling=true |
虚悬镜像是已经没有价值的,可以随意删掉:
1 | $ docker image prune |
删除本地镜像
如果要删除本地的镜像,可以使用 docker image rm
命令,其格式为:
1 | $ docker image rm [选项] <镜像1> [<镜像2> ...] |
<镜像>
可以是 镜像短 ID
、镜像长 ID
、镜像名
或者 镜像摘要
。
用 docker image ls 命令来配合
像其它可以承接多个实体的命令一样,可以使用 docker image ls -q
来配合使用 docker image rm
,这样可以成批的删除希望删除的镜像。
删除所有仓库名为 redis
的镜像:
1 | $ docker image rm $(docker image ls -q redis) |
删除所有在 mongo:3.2
之前的镜像:
1 | $ docker image rm $(docker image ls -q -f before=mongo:3.2) |
启动容器
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped
)的容器重新启动。
因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。
新建并启动
命令主要为 docker run
。
下面的命令输出一个 “Hello World”,之后终止容器。
1 | $ docker run ubuntu:18.04 /bin/echo 'Hello world' |
下面的命令则启动一个 bash 终端,允许用户进行交互。
1 | $ docker run -t -i ubuntu:18.04 /bin/bash |
其中,-t
选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i
则让容器的标准输入保持打开。
后台运行(-d后台运行)(–name添加一个名字)
1 | $ docker run -it -d --name test1 alpine |
启动已终止容器
可以利用 docker container start
命令,直接将一个已经终止的容器启动运行。
查看容器信息
通过 docker container ls
命令来查看容器信息。
1 | $ docker container ls |
要获取容器的输出信息,可以通过 docker container logs
命令。
1 | $ docker container logs [container ID or NAMES] |
终止容器
可以使用 docker container stop
来终止一个运行中的容器。
终止状态的容器可以用 docker container ls -a
命令看到。例如
1 | docker container ls -a |
处于终止状态的容器,可以通过 docker container start
命令来重新启动。
此外,docker container restart
命令会将一个运行态的容器终止,然后再重新启动它。
进入容器
在使用 -d
参数时,容器启动后会进入后台。
某些时候需要进入容器进行操作,包括使用 docker attach
命令或 docker exec
命令,推荐大家使用 docker exec
命令
attach 命令
下面示例如何使用 docker attach
命令。
1 | $ docker run -dit ubuntu |
注意: 如果从这个 stdin 中 exit,会导致容器的停止。
exec
命令
docker exec
后边可以跟多个参数,这里主要说明 -i
-t
参数。
只用 -i
参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。
当 -i
-t
参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。
1 | $ docker run -dit ubuntu |
如果从这个 stdin 中 exit,不会导致容器的停止。
更多参数说明请使用 docker exec --help
查看。
删除容器
可以使用 docker container rm [name\container id]
来删除一个处于终止状态的容器。例如
1 | $ docker container rm trusting_newton |
用 docker container ls -a
命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。
1 | $ docker container prune |
Docker 定制
docker commit
镜像是容器的基础,每次执行 docker run
的时候都会指定哪个镜像作为容器运行的基础。
如果我们进入容器,修改容器内文件的内容,也就是改动了容器的存储层。我们可以通过 docker diff
命令看到具体的改动。
Docker 提供了一个 docker commit
命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。
docker commit
的语法格式为:
1 | docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]] |
我们可以用下面的命令将容器保存为镜像:
1 | $ docker commit \ |
其中 --author
是指定修改的作者,而 --message
则是记录本次修改的内容。这点和 git
版本控制相似,不过这里这些信息可以省略留空。
我们还可以用 docker history
具体查看镜像内的历史记录,如果比较 nginx:latest
的历史记录,我们会发现新增了我们刚刚提交的这一层。
1 | $ docker history nginx:v2 |
使用 docker commit
命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。
首先,如果仔细观察之前的 docker diff webserver
的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html
文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像极为臃肿。
dockerfile
Dockerfile 是一个文本文件,用来配置image.其内包含了一条条的 **指令(Instruction)**,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
定制 nginx
镜像为例,这次我们使用 Dockerfile 来定制。
在一个空白目录中,建立一个文本文件,并命名为 Dockerfile
:
1 | $ mkdir mynginx |
其内容为:
1 | FROM nginx |
FROM
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。 FROM
就是指定 基础镜像,因此一个 Dockerfile
中 FROM
是必备的指令,并且必须是第一条指令。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch
。
1 | FROM scratch |
如果你以 scratch
为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
RUN 执行命令
RUN
指令是用来执行命令行命令的。格式有两种:
- shell 格式:
RUN <命令>
,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的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
9FROM 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 | FROM debian:stretch |
首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN
一一对应不同的命令,而是仅仅使用一个 RUN
指令,并使用 &&
将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
COPY 复制文件
格式:
COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
和 RUN
指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。
COPY
指令将从构建上下文目录中 <源路径>
的文件/目录复制到新的一层的镜像内的 <目标路径>
位置。比如:
1 | COPY package.json /usr/src/app/ |
ADD 更高级的复制文件
ADD
指令和 COPY
的格式和性质基本一致。但是在 COPY
基础上增加了一些功能。
比如 <源路径>
可以是一个 URL
,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径>
去。下载后的文件权限自动设置为 600
,如果这并不是想要的权限,那么还需要增加额外的一层 RUN
进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN
指令进行解压缩。
EXPOSE 暴露端口
格式为 EXPOSE <端口1> [<端口2>...]
。
EXPOSE
指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P
时,会自动随机映射 EXPOSE
的端口。
要将 EXPOSE
和在运行时使用 -p <宿主端口>:<容器端口>
区分开来。-p
,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
WORKDIR 指定工作目录
格式为 WORKDIR <工作目录路径>
。
使用 WORKDIR
指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR
会帮你建立目录。
USER 指定当前用户
格式:USER <用户名>[:<用户组>]
USER
指令和 WORKDIR
相似,都是改变环境状态并影响以后的层。WORKDIR
是改变工作目录,USER
则是改变之后层的执行 RUN
, CMD
以及 ENTRYPOINT
这类命令的身份。
ENV 设置环境变量
格式有两种:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN
,还是运行时的应用,都可以直接使用这里定义的环境变量。
定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 node
镜像 Dockerfile
中,就有类似这样的代码:
1 | ENV NODE_VERSION 7.2.0 |
例子🌰
下面我以 koa-demos 项目为例,介绍怎么写 Dockerfile 文件,实现让用户在 Docker 容器里面运行 Koa 框架。
作为准备工作,请先下载源码。
1 | $ git clone https://github.com/ruanyf/koa-demos.git |
编写 Dockerfile 文件
首先,在项目的根目录下,新建一个文本文件.dockerignore
,写入下面的内容。
1 | .git |
上面代码表示,这三个路径要排除,不要打包进入 image 文件。如果你没有路径要排除,这个文件可以不新建。
然后,在项目的根目录下,新建一个文本文件 Dockerfile,写入下面的内容。
1 | FROM node:8.4 |
上面代码一共五行,含义如下。
1 | * FROM node:8.4:该 image 文件继承官方的 node image,冒号表示标签,这里标签是8.4,即8.4版本的 node。 |
创建 image 文件
有了 Dockerfile 文件以后,就可以使用docker image build
命令创建 image 文件了。
1 | $ docker image build -t koa-demo . |
上面代码中,-t
参数用来指定 image 文件的名字,后面还可以用冒号指定标签。如果不指定,默认的标签就是latest
。最后的那个点表示 Dockerfile 文件所在的路径,上例是当前路径,所以是一个点。
发布镜像
在验证镜像有效后,可以将镜像发布到远程仓库以供大家使用.
发布镜像分为四步:
登录
首先,去 hub.docker.com 或 cloud.docker.com 注册一个账户。然后,用下面的命令登录。
1 | $ docker login |
标注用户名和版本
接着,为本地的 image 标注用户名和版本。
1 | $ docker image tag [imageName] [username]/[repository]:[tag] |
构建image 文件
1 | $ docker image build -t [username]/[repository]:[tag] . |
发布 image 文件
1 | $ docker image push [username]/[repository]:[tag] |
Docker 架构
Docker 采用了 C/S
架构,包括客户端和服务端。Docker 守护进程 (Daemon
)作为服务端接受来自客户端的请求,并处理这些请求(创建、运行、分发容器)。
客户端和服务端既可以运行在一个机器上,也可通过 socket
或者 RESTful API
来进行通信。
Docker Client
docker client 是docker架构中用户用来和docker daemon建立通信的客户端,用户使用的可执行文件为docker,通过docker命令行工具可以发起众多管理container的请求。
docker client可以通过一下三宗方式和docker daemon建立通信:tcp://host:port;unix:path_to_socket;fd://socketfd。,docker client可以通过设置命令行flag参数的形式设置安全传输层协议(TLS)的有关参数,保证传输的安全性。
docker client发送容器管理请求后,由docker daemon接受并处理请求,当docker client 接收到返回的请求相应并简单处理后,docker client 一次完整的生命周期就结束了,当需要继续发送容器管理请求时,用户必须再次通过docker可以执行文件创建docker client。
Docker daemon
docker daemon 是docker架构中一个常驻在后台的系统进程,功能是:接收处理docker client发送的请求。该守护进程在后台启动一个server,server负载接受docker client发送的请求;接受请求后,server通过路由与分发调度,找到相应的handler来执行请求。
docker daemon启动所使用的可执行文件也为docker,与docker client启动所使用的可执行文件docker相同,在docker命令执行时,通过传入的参数来判别docker daemon与docker client。
docker daemon的架构可以分为:docker server、engine、job。daemon
作者: Fynn
链接: https://fynn90.github.io/2020/07/25/Docker%E5%85%A5%E9%97%A8/
本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可