小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前言,如何给零基础的前端同学讲清楚docker属实是不小的挑战,看似简单的内容,却不知道从何入手,或者说,我自己学到的三分皮毛,怎么好意思给大家share?与后端同学经过几轮深入讨论后,决定从最基础的使用开始,告诉大家docker是什么,怎么用。
本文目的
如果你的时间和精力很充裕,最直接有效的方式是看官方文档跟着Get started
一步一步操作,一天的时间足矣学会。
希望能够帮助您了解docker。
本文将介绍:
- 如何安装docker(作为合理的闭环,该环节需要存在)
- 初识docker,简单的Getting startted。
- 跑一个简单的应用
- 构建自己的镜像
- 分享最佳实践
- 配置项优化
安装
你需要把docker桌面端(大于500MB的安装包)安装到本地。
当然,如果你需要更多类型的安装包,你需要去这里看看
概念
- Docker 码头工人???集装箱???
想象一下,要把15头大象从西双版纳运到昆明分几步。如果有直升机的话,可以一次运送一头,或者使用15架直升机;或者把15头大象放一起,放在一个超级大的集装箱中,找一架超级大的飞机一次性运往昆明;当然大象更愿意自己走过去,实际它们也是那么做的。那么问题来了,如果再从昆明运送回西双版纳,是不是集装箱这种方式更方便?回去的时候,可以选择坐火车,同时还可以把大象爱吃的菠萝放在另一个集装箱,也不用担心被大象吃掉,这就是Docker。
- 规整摆放
- 标准化
- 不互相影响
- Containers / Apps 容器 / 应用
- Images 镜像
- Volumes 挂载
初识
安装成功后,是这个界面
我们点击start
或者Skip tutorial
跳过教程,我们这里点击skip,直接往后看
命令行执行
docker run -d -p 80:80 docker/getting-started
- -d 后台运行,启动成功后会打印容器id
- -p 指定内外映射端口号
- docker/getter-started 执行的镜像名称
命令会在本地下载并启动docker/getting-started镜像
~ $ docker run -d -p 80:80 docker/getting-started
Unable to find image 'docker/getting-started:latest' locally
latest: Pulling from docker/getting-started
540db60ca938: Pull complete
0ae30075c5da: Pull complete
9da81141e74e: Pull complete
b2e41dd2ded0: Pull complete
7f40e809fb2d: Pull complete
758848c48411: Pull complete
23ded5c3e3fe: Pull complete
38a847d4d941: Pull complete
Digest: sha256:10555bb0c50e13fc4dd965ddb5f00e948ffa53c13ff15dcdc85b7ab65e1f240b
Status: Downloaded newer image for docker/getting-started:latest
8bc8dcde4b87fb57620d0df43f0da3b14688a2c27f15709692ed59497f2127c4
切换到Images,可以看到刚刚pull的镜像
切换到Containers,可以看到已经在运行的容器,可以使用浏览器访问教程
那么,这整个过程中发生了什么?
- 启动了一个nginx服务
- 静态化一些html页面
我们通过Inspect
选项或者在容器界面直接点击镜像名称,可以查看到镜像详细构建过程
可以通过CLI
入口进入容器,查看下物理文件
再理解概念
- Container,一个完全独立的本机进程,和其他所有进程完全隔离。
- Image,类似于我们安装操作系统时所需要的那个iso光盘镜像,通过运行这个镜像来完成各种应用的部署
- Layer,层,每个docker镜像就像一张千层饼。
简单应用
我们接下来,暂时抛开docker,先看个简单的基于express
的demo
git clone git@github.com:fuchunhui/docker-start.git
cd docker-start
npm install && node server.js
代码的结构如下:
server.js
是主入口
import express from 'express';
import path from 'path';
import word from './word.js';
const app = express();
app.use(express.json());
app.use(express.text());
app.use(express.raw());
app.use(express.urlencoded({ extended: false }));
const __dirname = path.resolve();
app.use(express.static(path.join(__dirname, 'public')));
app.get('/get', (req, res) => {
res.send('get request success.');
});
app.get('/test', (req, res) => {
res.send(word.random().join('</br>'));
});
app.post('/faces', (req, res) => {
const num = req.body?.num || 10;
res.send(word.random(num).join('\n\n'));
});
app.listen(8080);
app监听三个接口
/get
服务端返回get request success.
提示/test
随机返回颜文字/faces
随机返回指定数量的颜文字,并换行显示
npm run dev
后,浏览器访问get
npm run post
测试post请求
2.word.js
是基于百度输入法,构建的颜文字字库,提供一个random(num)
函数,随机返回指定个数的颜文字
3.test.js
本地测试word.js
功能。
4.public/index.html
提供一个静态页面,提供一个按钮获取数据
,点击后,调用server.js
的test
请求,显示一个颜文字。
到这里,我们的示例演示介绍完成,那么,请思考一下,如果我们本机环境没有Node怎么办?除了在本机安装Node外,我们还可以借用Docker的能力,构建我们自己的镜像,把示例Demo跑起来。
继续下面内容之前,我们先按住control + c
,停止本地node服务
构建镜像
我们需要一个叫做Dockerfile
的文件,Docker会自动读取它的文本内容构建镜像,Dockerfile
是一个命令行的集合。
让我们开始吧,先构建一个Node服务。
准备node环境
touch Dockerfile
然后在Docker Hub查找合适的Node镜像,docker hub和我们熟知的npmjs.com一样,是所有镜像的源,我们通过搜索查找Node官方镜像
接下来,编辑Dockerfile
文件
FROM node:16
CMD ["node", "--version"]
构建node16版本的镜像,默认输出当前版本信息。然后执行build命令
docker build -t node:16 .
在.(当前目录下)执行docker build,并生成name为node,tag为16的镜像。
docker-start (main) $ docker build -t node:16 .
[+] Building 440.6s (5/5) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 43B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/node:16 70.2s
=> [1/1] FROM docker.io/library/node:16@sha256:7a888d7030be38976daabcd0881ff6564fb05885046fef9d08eb6002fa651 370.1s
=> => resolve docker.io/library/node:16@sha256:7a888d7030be38976daabcd0881ff6564fb05885046fef9d08eb6002fa6516f 0.0s
=> => sha256:e1b0cde43c0e73ca4ef26ae54aed841b7e5152f6dd510ad5e5e30cb771265f02 7.60kB / 7.60kB 0.0s
=> => sha256:8f04e8168e3873638397ca4beb7d8484b150eca0d10fe1b033a125202ba57692 50.44MB / 50.44MB 91.6s
=> => sha256:c1c8f1c77d6674046d7deb41be1ca07f25cb43fd67f87e879ee79cc6586087f0 10.00MB / 10.00MB 6.0s
=> => sha256:7a888d7030be38976daabcd0881ff6564fb05885046fef9d08eb6002fa6516fb 1.21kB / 1.21kB 0.0s
=> => sha256:2465d69d013cb7ea85444fac62310badce86e89c54716999eae31577ec2bc6f3 2.21kB / 2.21kB 0.0s
=> => sha256:82e5f66f5d0e1c97622f33d44fb04efb42bd3562bdc3482537d121040c789f9a 7.83MB / 7.83MB 17.9s
=> => sha256:5095cab277710f0c2883844158323ad986c763ffc37353ddff874dd85585d9b6 51.84MB / 51.84MB 25.8s
=> => sha256:ea7fe362a971515971cf53613a30cc824f94d544272a5e061eb6365923ccbc11 192.39MB / 192.39MB 357.4s
=> => sha256:9000ed6ad54103b2ede6fec607e8b3f7cb6a2610158eb76c6091eecd65961179 4.20kB / 4.20kB 27.1s
=> => sha256:bda6f8304dc4da996112a502fe92f5636bd29849f06aa75527febd6910342f74 34.19MB / 34.19MB 43.4s
=> => sha256:74bed10e609e5fb224fd52136389aa57e9b8c35c675be45211e4cd29a8e5675c 2.26MB / 2.26MB 45.8s
=> => sha256:5bacd780a65fbe617d4d47adf55e74f97d8d0143a021ea55557c75e5c3ae4978 282B / 282B 46.3s
=> => extracting sha256:8f04e8168e3873638397ca4beb7d8484b150eca0d10fe1b033a125202ba57692 2.8s
=> => extracting sha256:82e5f66f5d0e1c97622f33d44fb04efb42bd3562bdc3482537d121040c789f9a 0.4s
=> => extracting sha256:c1c8f1c77d6674046d7deb41be1ca07f25cb43fd67f87e879ee79cc6586087f0 0.4s
=> => extracting sha256:5095cab277710f0c2883844158323ad986c763ffc37353ddff874dd85585d9b6 3.1s
=> => extracting sha256:ea7fe362a971515971cf53613a30cc824f94d544272a5e061eb6365923ccbc11 9.0s
=> => extracting sha256:9000ed6ad54103b2ede6fec607e8b3f7cb6a2610158eb76c6091eecd65961179 0.1s
=> => extracting sha256:bda6f8304dc4da996112a502fe92f5636bd29849f06aa75527febd6910342f74 2.2s
=> => extracting sha256:74bed10e609e5fb224fd52136389aa57e9b8c35c675be45211e4cd29a8e5675c 0.2s
=> => extracting sha256:5bacd780a65fbe617d4d47adf55e74f97d8d0143a021ea55557c75e5c3ae4978 0.0s
=> exporting to image 0.1s
=> => exporting layers 0.0s
=> => writing image sha256:88211c2be135416f38e0814a33a644ca4224f13ce946b831f4cfcd44f52e2a82 0.0s
=> => naming to docker.io/library/node:16
我们打开桌面端,在Images目录,可以看到刚生成Node镜像
点击RUN
默认的镜像,会按照约定的方式,执行node --version
命令,表示镜像构建成功,接下来我们把docker-start
内容部署到docker上。
部署可执行程序
继续编辑Dockerfile
文件
FROM node:16
WORKDIR /docker-start
COPY . .
RUN npm install --production
CMD [ "node", "server.js" ]
设定docker-start为工作目录,然后把.目录的所有内容,拷贝到docker-start目录下,npm install后启动server服务。
我们通过命令行的方式构建新的镜像,并运行镜像。
docker-start
为镜像名,v1
为tag,在.
(当前目录下)执行build
操作
docker-start (main) $ docker build -t docker-start:v1 .
[+] Building 8.2s (9/9) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 152B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/node:16 0.0s
=> [1/4] FROM docker.io/library/node:16 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 25.34kB 0.0s
=> CACHED [2/4] WORKDIR /docker-start 0.0s
=> [3/4] COPY . . 0.1s
=> [4/4] RUN npm install --production 7.8s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:c9822dcaf7fdda7107445d13acbc184f269e52909244fefa577c6f2f6299b70 0.0s
=> => naming to docker.io/library/docker-start:v1
因为我们刚刚构建过node16的镜像,所以再次基于node16构建镜像,速度很快,利用了上一次镜像的缓存。
docker run -dp 8080:8080 docker-start:v1
通过run命令,启动服务。-d
后台运行,'-p'指定内外映射端口号,因为我们在server.js
中app.listen(8080)
,为方便使用,直接1对1映射。
再次打开浏览器,访问http://localhost:8080/
试试吧,刚刚的东西又出来啦。
- 浏览器访问
http://localhost:8080/get
,检测是否显示get request success.
- 浏览器访问
http://localhost:8080
,点击获取数据
按钮,检测是否出现颜文字 - 命令行执行
npm run post
,检测是否输出指定数量的颜文字,当然也可以直接curl
或者使用其他的post请求检测
构建优化
我们的命令行构建过程,COPY . .
把本地所有的内容都拷贝到目标目录下,当然,这就必然包含了node_modules
的内容,这不是我们想看到的,所以同.gitignore
一样,我们创建.dockerignore
文件,忽略node_modules
,并重新执行构建。
touch .dockerignore
echo 'node_modules'>.dockerignore
为了检测是否生效,我们先注释掉这两行代码
FROM node:16
WORKDIR /docker-start
COPY . .
# RUN npm install --production
# CMD [ "node", "server.js" ]
重新执行构建,并使用tag:v2
标记新的镜像
docker build -t docker-start:v2 .
启动docker-start:v2
,看一下里面内容,是否包含node_modules
内容
docker run -it --name=start2 docker-start:v2 /bin/bash
镜像发布
基本同npm的发布流程一样,先要注册账号,然后本地登录成功后,使用push命令发布。
以docker-start:v3
为例说明,发布到个人账号目录下。
先移除Dockerfile
文件中的两行注释,然后修改server.js
,增加listen后的回调
app.listen(8080, () => {
console.log('server start.');
});
构建v3版本
docker build -t docker-start:v3 .
docker tag docker-start:v3 fuchunhui/docker-start
查看下镜像,并重命名为USER_NAME/image:tag
,如果不写tag,默认使用latest
在hub.docker.com创建名称为docker-start
的repo
本地登录,并push镜像
docker login
docker push fuchunhui/docker-start
上传的速度,取决于镜像的大小和网络状况,我们小等一会儿,待完成后,我们刷新hub查看
Playground
我们还可以通过palyground来运行我们的镜像
登录成功后,我们pull下刚刚发布的镜像,并启动。
docker run -dp 8080:8080 fuchunhui/docker-start
执行完成后,然后点击右上角的OPEN PORT
输入8080
,然后回车确定。
这可是白嫖4个小时的Server啊,Close后,还能继续白嫖,这简直太爽了吧。
可以通过logs命令查看刚刚的callback是否执行
docker container ls // 获取container id
docker logs containerId
其他内容
鉴于篇幅原因,对其他内容不再过多介绍,如下内容也非常精彩,是使用docker的开始,推荐大家按照官方文档step by step的动手实操。
-
compose 顾名思义,组合式,使用yaml文件,可以同时执行多个任务,类似于github的Action或者ci.yml
-
volumes 挂载,通常用于共享DB,无论我们是v1,v2,还是v3都可以使用同一个数据库,并保证数据的可持久化。官网给的例子是todolist,每次新版本构建后,都可以看到上一次写好的todo。
-
network 网络,由于每个容器之间都是互相隔离的,如果两个容器想要通信,就需要使用network,先创建network,并在该network下面,运行两个容器,就可以互相通信了,比如实现A容器访问B容器的数据。
最佳实践
个人总结的几点最佳实践,供参考。
- 每个镜像,尽可能的干净,只提供单一的服务。
- 构建镜像,按独立镜像来推进,一个一个的build,能够保证服务的可追溯。(想想美味的千层饼)
- 对于需要安装的特定资源,可以考虑在不影响其他功能的前提下,在最上层,加载进来,比如npm的
canvas
包(因为它特别难以安装),可以在构建镜像的时候,保证node_modules的可用,解决后续npm install
的麻烦。 - 单个镜像内,控制层的个数,层数越少,镜像的体积越小,应该想尽办法获取体积更小的镜像,从而保证更快的传输速度和部署效率。
- 选择合适的基础镜像,例如针对不同环境的node镜像,两者在功能集上有本质的区别
镜像node:16是基于debian:buster构建
镜像node:16-stretch是基于debian:stretch构建
- 个人使用docker,多使用docker-compose
建议配置项
- 我们打开桌面端的配置中心,你会发现,你岌岌可危的硬盘空间,被Docker占用了60G,这里我建议设置成20G,足够使用。注意,每次修改配置项,保存后,本地构建的所有镜像,容器,挂载等等全部会清除,相当于一次初始化设置啦,所以请慎重操作,做好备份。
- 我们打开Mac自带的活动监视器,搜一下docker后台偷偷跑的进程
是不是有点吓人,不管你是否运行桌面端,这些内容都会在后台运行着(当然,使用体验确实不错),所以在不使用docker的时候,要把它们通通kill,减少CPU的负载。
总结
开阔自己的眼界,让思路不那么狭隘和局限。作为是软件工程师,最终是解决问题,解决问题有很多种方式,我们应该选择最合适的那个,而不是我们最擅长的那一套。
QA
分享后,收集大家的问题,汇总在这里