一 docker 是什么?
docker 是一种容器化技术, 用于支持创建和使用 linux 容器。那要了解 docker,首先要明白什么是 linux 容器。
容器的出现主要是为了解决应用在不同机器间部署的问题,如下列情况:
假设你在开发一个前端应用,你在本地使用的是 node 14 版本进行开发,但是你如果要在其他人的机器上打包或者开发相关服务,但是其他人的机器上没有 node 相关的配置,这样就需要重新在这台机器上安装一套完全相同的运行环境,才能保证在新的机器和本地运行的结果一致。
虚拟机技术
在使用容器之前我们经常使用虚拟机来解决这个问题。虚拟机是一种在操作系统里运行另一种操作系统的方式,它通过虚拟化出一套硬件后,在其上运行一个完整的操作系统,在该系统上再运行所需的应用程序,这样也能起到封装环境的作用。
但是虚拟机因为其自身的技术特点导致了虚拟机有如下缺点:
- 占用资源多
- 冗余步骤多
- 启动慢
传统容器技术
而容器技术和虚拟机技术不一样,它是与系统其他部分隔离开的一个或一组进程。
容器没有虚拟化出硬件系统,也不虚拟一个完整的操作系统,它在宿主内核的基础上对进程进行了隔离。也因为这样,容器里的应用其实是底层系统的一个进程,而不是和虚拟机一样是虚拟机内部的进程,所以容器内的应用启动要比虚拟机里快,而且容器只占用自己需要的资源,相比于虚拟机需要完整的操作系统,容器的提及也更小。
综上,容器化技术一定程度上解决了虚拟机存在的这几个主要问题。
虚拟机和传统容器技术的区别
可以用一句话概括两者的区别
虚拟化可以在单个硬件系统上运行多个操作系统,容器则共享同一个操作系统内核,在同一个操作系统的支持下,隔离运行多个应用进程。
那以上说了传统虚拟机技术和容器技术的区别,那我们再来看看传统的 linux 容器和 docker 之间又有什么区别?
传统 liunx 容器和 docker
docker 最初就是基于 lxc 技术构建,从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 版本开始,则进一步演进为使用 runC 和 containerd。
它和传统 linux 容器最大不同在于,docker 在容器的基础上进行了进一步封装,通过简化用于构建容器、传输镜像以及控制镜像版本等的流程。极大提升了用户的使用体验。
传统的 Linux 容器使用 init 系统来管理多种进程。这意味着,所有应用都作为一个整体运行。与此相反,Docker 技术力争让应用各自独立运行其进程,并提供相应工具,帮助实现这一功能。这种精细化运作模式自有其优势。
得益于上面的运行模式,docker 在其他容器特点的基础上还带来以下优点:
- 快速部署:容器仅包含了应用运行所需要的最小运行时,从而能做到快速部署。
- 便于分享:docker 提供 docker hub 远程仓库,可以把你的容器通过远程仓库分享给他人
- 层和镜像的版本控制: 每个 docker 镜像文件包含多个层,层组合在一起,构成单个镜像。每次修改镜像都会新增一个层,通过层你可以对 docker 镜像进行更细力度的控制。
- 回滚:得益于镜像的版本控制,实现回滚是很方便的。
同时,也为 docker 带来了一些缺点:
- 容器编排:Docker 本身非常适合用于管理单个容器。但随着您开始使用越来越多的容器和容器化应用,并把它们划分成数百个部分,很可能会导致管理和编排变得非常困难。
- 守护进程的安全性:为使用和运行 Docker 容器,您很可能需要使用 Docker 守护进程,来为容器提供持续运行时环境。Docker 守护进程需要根权限,所以我们需要特别留意谁可以访问该进程,以及进程驻留在哪个位置。
docker 核心概念
如果大家访问过 docker 的官网,就会看到 docker 官网 对 docker 的下列描述:
Build safer, share wider, run faster:
那上面这三个关键字,build、share、run 就能表示 docker 所做的所有工作,其中 build 表示构建镜像、share 表示分享镜像,run 表示运行镜像。下面就来详细介绍一下这里牵扯的几个概念,同时也是 docker 中最重要的几个内容——镜像,容器以及仓库。
镜像
镜像是 docker 里最基础也是最核心的概念,要理解镜像是什么,可以从 docker 的 logo 中窥知一二。
镜像类似于集装箱。只不过现实的集装箱里装的是货物,而 docker 镜像里装的是软件的文件以及所需的运行环境资源等。
那实际上 Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。
由于镜像包含了不少东西,为了避免其体积过于庞大,所以其被设计为分层存储的结构。所以镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。这样的设计方式同时能让镜像的复用和定制变得相对容易。
容器
容器是应用程序层的抽象,可以将代码和依赖项打包在一起。容器不虚拟化整个物理机,仅虚拟化主机操作系统。
镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
仓库
镜像仓库,一个集中存储镜像的位置。类似于 github。你可以将自己本地构建的镜像推送到 Docker hub 中给其他人使用,同时也能在 Docker hub 中找到其他人构建的镜像,基于这个镜像改造或者运行容器。
讲完了 docker 中最重要的三个概念之后, 我们再回到官网中介绍 docker 的那句话就会发现 docker 其实和它的单词背后的意思类似,是一个”码头工人”,把有关货物装配在一个”集装箱里”,统一送到集中的”超级码头”上。或者根据他人需要,从”超级码头”找到指定的”集装箱”,送到有需要的人手里。
docker 入门 demo
下面我们就以将一个 nodejs 应用装入到 docker 为例,为大家演示 docker 操作的整体过程,同时基于这个过程说明一下 docker 的整体架构情况。
安装过程我们这里就不详细讲了,想了解的同学可以点击这里查看。假设大家的机器上现在都已经安装好了 docker,接下来我们首先要创建应用。
创建 Node.js 应用
首先,创建一个新文件夹以便于容纳需要的所有文件,并且在此其中创建一个 package.json 文件,描述你应用程序以及需要的依赖:
{ "name": "docker_web_app", "version": "1.0.0", "description": "Node.js on Docker", "author": "xxx", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.16.1" }}配合着你的 package.json 请运行 npm install。如果你使用的 npm 是版本 5 或者之后的版本,这会自动生成一个 package-lock.json 文件,它将一起被拷贝进入你的 Docker 镜像中。
然后,创建一个 server.js 文件,使用 Express.js 框架定义一个 Web 应用:
'use strict';
const express = require('express');
// Constantsconst PORT = 8080;const HOST = '0.0.0.0';
// Appconst app = express();app.get('/', (req, res) => { res.send('Hello World');});
app.listen(PORT, HOST);console.log(`Running on http://${HOST}:${PORT}`);以上我们就创建了一个最简单的 nodejs 应用,在接下来的章节中,我们要学习如何在 Docker 容器中运行起这个应用。
构建镜像
我们要做的第一步就是构建我们所要的镜像,那 docker 社区通常在实际环境中都是使用 Dockerfile 脚本来构建镜像。(为什么慎用 docker commit 来构建镜像)
Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
我们首先创建一个空文件,命名为 Dockerfile
touch Dockerfile在该文件中先输入以下内容
FROM node:12那我们前面说过,镜像是分层的,可以在已有的镜像之上定制新镜像。这里的 FROM 指令就是指定基础镜像,并在其之上进行定制。这里就是使用官方的 node 12 版本作为基础镜像,你可以从 Docker 站点 获取更多相关镜像。
(⚠️: 一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。)
接下来要在镜像中创建一个路径来存放程序代码,这同时也是我们应用程序接下来的工作目录。
WORKDIR /usr/f2e/app接下来我们使用 npm 来安装项目所需依赖
COPY package*.json ./
RUN npm install这里只拷贝了 package.json 文件,而不是整个目录是为了利用缓存 docker 层的优势,详见此
。(熟悉 gitlab ci/cd 的同学可以等同于 node modules 的缓存机制)。
在 docker 镜像中使用 copy 命令绑定应用程序。
COPY . .(对于 COPY 命令的路径感兴趣的同学可点击这里 查看。)
你的应用程序绑定的端口为 8080,所以你可以使用 EXPOSE 命令使它与 docker 的镜像做映射:
EXPOSE 8080(关于 EXPOSE的 更多用法)
最后,使用定义运行时的 CMD 定义命令来运行应用程序。这里我们使用 node server.js 来启动你的服务器:
CMD [ "node", "server.js" ]最后你的 Dockerfile 文件如下
# 指定基础镜像FROM node:12
# 指定工作区WORKDIR /usr/f2e/app
# 拷贝 package.jsonCOPY package*.json ./
# 安装依赖RUN npm install
# 拷贝本地应用到 docker 中COPY . .
# 映射端口EXPOSE 8080
# 执行命令CMD [ "node", "server.js" ]最后需要注意的是,如果你有不想拷贝进入 Docker 镜像中的文件,和 git 类似,可以在 Dockerfile 的同一个文件夹创建一个 .dockerignore 文件,里面写入不想被 docker 拷贝的内容。
如你不想本地模块以及调试日志被拷贝进入到你的 Docker 镜像,你可以在 .dockerignore 中写入以下内容:
node_modulesnpm-debug.log在完成 Dockerfile 文件的书写之后,我们还需要最后一步就完成了整个镜像的构建了。进入到 Dockerfile 所在的那个目录中,运行以下命令构建 Docker 镜像。
docker build . -t chrismiaomiao/node-web-app(参数 -t 用来标记镜像)
运行完之后,通过执行 docker images 命令就能看到你刚刚构建好的镜像了。
$ docker images
# ExampleREPOSITORY TAG ID CREATEDnode 12 1934b0b038d1 5 days agochrismiaomiao/node-web-app latest d64d3505b0d2 1 minute ago运行容器
在构建完镜像之后,运行容器就比较简单了,运行 docker run 命令就能把刚刚构建的镜像作为容器运行起来。
docker run -p 49160:8080 -d chrismiaomiao/node-web-app(使用 -d 模式运行镜像将以分离模式运行 Docker 容器,使得容器在后台自助运行。开关符 -p 在容器中把一个公共端口导向到私有的端口,这里即在容器中 Docker 把端口号 8080 映射到你机器上的 49160 )
运行完之后,执行 docker ps 把运行的容器信息打印出来:
docker ps
# ExampleCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESf1d7a1e222d0 chrismiaomiao/node-web-app "docker-entrypoint.s…" 4 seconds ago Up 3 seconds 0.0.0.0:49160->8080/tcp hopeful_raman接下来我们使用 curl 测试一下容器中运行的应用程序:
curl -i localhost:49160
HTTP/1.1 200 OKX-Powered-By: ExpressContent-Type: text/html; charset=utf-8Content-Length: 11ETag: W/"b-Ck1VqNd45QIvq3AZd8XYQLvEhtA"Date: Tue, 08 Mar 2022 01:25:49 GMTConnection: keep-aliveKeep-Alive: timeout=5
Hello World%到此,我们就成功的在 docker 容器中运行起了一个 node 应用了。如果你想把刚刚在本地构建的镜像推送到远程仓库中给更多人使用可以使用 docker push 命令来操作,这里不再过多赘述,当然 docker 命令还有很多,那对此有兴趣的同学们可以点击这里查看详细。
docker 的整体架构
最后,我们通过上面的 demo 来了解一下 docker 的整体架构。
docker 架构主要分成三个部分
- Docker 守护程序: 守护程序(
dockerd)是一个始终在后台运行并等待来自客户端的命令的进程。守护程序能够管理各种 Docker 对象。 - Docker 客户端: 客户端(
docker)是一个命令行界面程序,主要负责传输用户发出的命令。 - REST API: REST API 充当守护程序和客户端之间的桥梁。使用客户端发出的任何命令都将通过 API 传递,最终到达守护程序。
docker 官方文档是这样描述 docker 架构的:
” Docker 使用客户端-服务器体系结构。Docker client 与 Docker daemon 对话,daemon 繁重地构建、运行和分发 Docker 容器”。
作为用户,通常将使用客户端执行命令。然后,客户端使用 REST API 来访问长期运行的守护程序并完成工作。
就像我们上面 demo 中执行 docker run chrismiaomiao/node-web-app 这个命令。它看似是一条命令,但是其背后会执行下列流程
- Docker 客户端访问守护程序,告诉它获取
chrismiaomiao/node-web-app镜像并从中运行一个容器。 - Docker 守护程序在本地仓库中查找镜像,发现存在,则根据该新镜像创建一个新容器,如果本地仓库不存在,则 Docker 守护程序会访问默认的公共仓库(如 docker hub)拉取该镜像。
- 最后,Docker 守护程序运行使用
chrismiaomiao/node-web-app镜像创建的容器,该镜像启动了一个 node 服务。
参考资料
- https://www.redhat.com/zh/topics/containers/what-is-container-orchestration
- https://www.redhat.com/zh/topics/containers/what-is-docker
- https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/7.0_release_notes/sect-red_hat_enterprise_linux-7.0_release_notes-linux_containers_with_docker_format-advantages_of_using_docker
- http://dockone.io/article/6051
- https://blog.csdn.net/henni_719/article/details/102803598