Docker
开源的应用容器引擎,如果你是从事后端的开发者,相信对这门技术应该是了解或是熟悉,而对于很多前端开发者,也许只是停留在听过的阶段上,甚至不知道是啥?或是会认为这是后端的技术,我不需要知道,比如说我,还真的不知道是什么,但是如果想成为一名资深前端,这部分空缺是需要填补上的。咸鱼也要有梦想嘛,也许哪天可以跃龙门呢!
本文会通过构建带有web前端代码和mongoDB
数据库的全栈node.js
应用程序出发,来进一步了解Docker
以及它的用途。
什么是Docker
Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux、Window 机器上,也可以实现虚拟化。听不懂,能否通俗解释Docker前世今生?
2010年,几个搞IT的年轻人,在美国旧金山成立了一家名叫
dotCloud
(搞容器技术)的公司。结果坚持不下去,于是开源,结果火了,火了得重新起个牛逼的名字呀,于是Docker
就出现了。Docker
出现前,如何模拟一个相互隔离
的系统环境?答案就是虚拟机
,大家应该都不陌生,很多开发者电脑里面都会装VMWare
,通过它,我们可以变出好几台子电脑
,一个装window11
、一个装CentOS
,安装上我喜欢的QQ
、微信
等软件,多个子电脑互相隔离、互不影响
,美滋滋!但是动不动就几个G、几十个G,磁盘吃不消呀,而且还启动慢。前面说到了
Docker
出现前,虚拟机
在做环境隔离上是业界的网红,但是弊端是重
,而Docker
容器技术,其实也是一种虚拟化技术
,而且轻、快、一体化,只需要MB级甚至KB级,不像虚拟机
,需要模拟一个操作系统出来,Docker
只需要虚拟一个小规模的环境(类似“沙箱”)。文章图片
不行,我要看数据比对,我才信,安排....
文章图片
Docker核心概念 上一小节,我们了解了Docker是一种容器虚拟化技术,更轻、更快、更容易一体化,接下来我们快速了解一下它的核心概念后,再去进入我们今天的主题,在编写代码才更好理解。
Docker
的三大核心概念:- 镜像(Image)
- 容器(Container)
- 仓库(Repository)
文章图片
以上关系图能反映出三者之间的关系,此处需要注意的是,我们说
Docker
是一种容器技术,但是Docker本身并不是容器,它是创建容器的工具,是应用容器引擎。镜像
,也就是Docker镜像,是一个特殊的文件系统。它除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(例如环境变量),同时镜像不包含任何动态数据。我们可以有很多
镜像
,我们想存起来,然后可以到任何地方去使用它创建容器
环境,那么就需要仓库
来存储,也就是Docker仓库
。有怎么一个
仓库
存在,那么所有人都可以往里面存镜像
么?不是的,如果存放了个有问题的镜像
,那创建容器时候不就挂了么?所以需要有个负责对Docker镜像
进行管理的角色,就是Docker Registry服务(类似仓库管理员)
了。官方也提供了公共Registry服务
,就是Docker Hub
(有点像我们的npm市场),里面存放着很多高质量的官方镜像。同时我们还可以通过 Dockfile文件来定制我们的镜像,后续有介绍通过上面的介绍,相信大家对
Docker
应该也有了个大概的认识了,这里提供一些常用的Docker
命令,# 容器
$ docker run// 创建并启动容器
$ docker start // 启动容器
$ docker ps // 查看容器
$ docker stop // 终止容器
$ docker restart // 查看容器
$ docker attach // 进入容器
$ docker exec // 查看容器
$ docker export // 导出容器
$ docker import // 导入容器快照
$ docker rm // 删除容器
$ docker log // 查看日志# 镜像
$ docker search // 检索镜像
$ docker pull // 获取镜像
$ docker images // 列出镜像
$ docker image ls // 列出镜像
$ docker rmi // 删除镜像
$ docker image rm // 删除镜像
$ docker save // 导出镜像
$ docker load // 导入镜像# Dockfile定制镜像以及常用指令$ docker build // 构建镜像
$ docker run // 运行镜像COPY // 复制文件
ADD // 高级复制
CMD // 容器启动指令
ENV // 环境变量
EXPOSE // 暴露接口# 服务
$ docker -v // 查看docker的简要信息
$ docker -version // 查看docker版本的简详细信息
$ systemctl start docker // 启动docker
$ systemctl stop docker // 关闭docker
$ systemctl enable docker // 设置开机启动
$ service docker restart // 重启docker服务
$ service docker stop // 关闭docker服务
创建Hello-World容器 首先需要下载
Docker
下载地址,我下载的是Window Docker Desktop
,接下来查看版本信息是否下载成功,我下载的是
20.10.11
版本文章图片
然后拉取
Docker Hub
的官方hello-world
镜像,文章图片
创建并执行容器,
文章图片
这样我们第一个容器就创建出来了,可以用用上面提到的命令行
docker image ls / docker image prune
来查看或是删除没有用(停止运行容器时,它不会被删除,会帮助下次下载安装速度加快)的镜像,更多命令大家可以去试试!创建Node程序 接下来我们创建个Node程序,在后面教程介绍需要使用到,具体代码详情就不去做介绍了,可以查看以下
server.js和package.json
:const express = require("express");
const app = express();
const port = 8080;
app.get("/", async (req, res) => {
res.setHeader("Content-Type", "text/html");
res.status(200);
res.send("你好呀!前端晚间课");
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
{
"name": "docker-example",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "nodemon server.js"
},
"author": "前端晚间课",
"license": "ISC",
"dependencies": {
"express": "^4.17.2"
},
"devDependencies": {
"nodemon": "^2.0.15"
}
}
运行
npm run start
,成功跑起来了...文章图片
Node版本不同的困扰 针对上面的
server.js
文件,我们添加下面的代码: // ...
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("good");
}, 300);
reject("bad");
});
myPromise.then(() => {
console.log("this will never run");
});
然后分别在
Node < 15
和Node >= 15
运行,结果会得到两个不同的结果,Node < 15
(node:764) UnhandledPromiseRejectionWarning: something happened
(Use `node --trace-warnings ...` to show where the warning was created)
(node:764) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:764) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Node >= 15
node:internal/process/promises:218
triggerUncaughtException(err, true /* fromPromise */);
^[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "recipe
could not be generated".] {
code: 'ERR_UNHANDLED_REJECTION'
}
会发现两个版本的执行结果会不一致,高版本(Node >= 15)会直接导致程序崩溃(
ERR_UNHANDLED_REJECTION
),这是未处理的被拒绝 Promise的错误。现在假设出于某种原因,此应用程序必须在
Node v14
或更早版本上运行才能工作(忽略用try...catch
)。团队中的每个开发人员都必须准备好在该环境中开发和运行,但我们公司还有一个新的应用程序,要在 Node v17
上运行!那这时应该如何去解决这个问题?答案:Docker
创建Dockerfile 上一小节,我们介绍了
两个不同版本的Node
引起的未处理的被拒绝 Promise
的问题,我们介绍如何使用Docker
去解决这个问题,其实也很简单,就是需要一个Node < 15
的运行环境来保证我们的程序不会崩溃,我们在
Docker Hub
可以搜索到关于的Node
的镜像,而且还有很多版本信息可选。文章图片
文章图片
当然我们没必要直接使用
docker pull node
来拉取node镜像
,前面我们有提过Dockerfile
可以用来自定义定制镜像,它自动判断当前机器是否存在Node镜像
,没有的话再自动去Docker Hub
拉取,看一下我们的Dockerfile
文件:# 首先先选择你需要的镜像,运行在alpine的node版本是当下最流行的
FROM node:14-alpine3.12# 工作目录
# 这是您你将在容器内的位置
WORKDIR /usr/src/app# 通配符用于确保 package.json 和 package-lock.json 都被复制
# COPY 源目录 容器的工作目录
COPY package*.json ./# 安装应用依赖
RUN npm install# 如果你正在构建用于生产的代码
# RUN npm ci --only=production# 捆绑应用程序源
COPY . .# 配置这个端口可以从容器外部访问
# 浏览器向 Node 应用程序发送 HTTP 请求所必需的
EXPOSE 8080# CMD 在docker run 时运行
# 就是执行shell npm run start
CMD [ "npm", "run", "start"]
ok,我们的
Dockerfile
文件已经创建成功,对Dockerfile指令
在上面代码中注释也有做简单的介绍,更多详细指令用法可以到谷歌搜索,但你可能会好奇上面的配置文件,为什么COPY
需要执行两次,最后的COPY . .
不是复制了整个目录,为啥上面还需要复制package*.json
?Docker的层和缓存
COPY
两次是有必要的,因为Docker
拥有layers
(层的特性),每执行一条指令都会基于上一次指令创建的图层基础上再创建一层图层,创建的图层会被缓存,只要发生改变时才会再次重新创建,我们再回过头来看Dockerfile
文件。COPY package*.json ./
我们创建了一个基于该文件内容的图层,然后再运行npm install
,这意味着除非我们更改 package.json
,否则下次我们构建 Docker
时将使用npm install
已经运行的缓存层,我们不必每次运行时都安装所有依赖项docker build。这将为我们节省大量时间。COPY . .
会查看我们项目目录中的每个文件,因此该层将在任何文件更改时重建(除了package*.json
)。这正是我们想要的。构建应用容器 我们再添加个
.dockerignore
文件,类似我们的.gitignore
,因为我们不想复制这些文件呀。node_modules
npm-debug.log
一切准备就绪,我们开始构建属于自己的镜像,
# 以当前项目目录为源目录,给镜像名个名叫做qianduanwanjianke
$ docker build . -t qianduanwanjianke
再查看我们创建的镜像存不存在?
文章图片
创建镜像后,我们现在准备从镜像构建一个容器来运行我们的应用程序:
# --name 我们给容器名了个名,叫做qianduanwanjianke-container
# -p标志将端口从我们的主机(我们的计算机)环境3001端口映射到容器环境的8080端口,当然也可以是8080:8080。
docker run -p 3001:8080 --name qianduanwanjianke-container qianduanwanjianke
文章图片
大功告成,我们访问一下
http://localhost:3001/
看看是否成功,文章图片
结语 到这里,我们通过一个
Node 应用程序
切入来介绍Docker
,创建了我们的第一个自定义 Docker 镜像和容器,并在其中运行我们的应用程序!这是面向 Javascript 开发人员的 Docker 简介,由Node
程序切入,文章内容对于熟悉Docker
的后端开发者来说可能是很相当容易的,对于我们前端开发者来说应该是很好的入门教程。由于篇幅已经很长了,我决定把Docker Volume
("连接"起容器内部的程序副本与项目目录中的副本,更新同步) 以及引入数据库,分析数据库托管服务器不同,如何创建分离、Docker Compose
等内容放置到下篇内容再做介绍。过程中踩的坑 1、拉取镜像时报错,报错信息:
error during connect: This error may indicate that the docker daemon is not running...
解决办法:
# 在Powershell 提升访问权限解决此问题
cd "C:\Program Files\Docker\Docker"
./DockerCli.exe -SwitchDaemon
2、创建应用容器的时候,执行
docker build . -t my-node-app
, 报错信息:no matching manifest for windows/amd64 10.0.18363 in the manifest list entries
?【「Part 1」面向 Javascript 开发人员的 Docker 简介(基于 Node.js)】解决办法:
打开
Docker Devlop
软件, settiong -> Docker Engine
,将experimental设置为true,重启Docker