• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Nest 通关秘籍

    • 1.开篇词
    • 2.给你 5 个学习 Nest 的理由,你会心动么
    • 3.Nest 基础概念扫盲
    • 4.快速掌握 Nest CLI
    • 5.五种HTTP数据传输方式
    • 6.IoC 解决了什么痛点问题?
    • 7.如何调试 Nest 项目
    • 8.使用多种 Provider,灵活注入对象
    • 9.全局模块和生命周期
    • 10.AOP 架构有什么好处?
    • 11.一网打尽 Nest 全部装饰器
    • 12.Nest 如何自定义装饰器
    • 13.Metadata 和 Reflector
    • 14.ExecutionContext:切换不同上下文
    • 15.Module 和 Provider 的循环依赖怎么处理?
    • 16.如何创建动态模块
    • 17.Nest 和 Express 的关系,如何切到 fastify
    • 18.Nest 的 Middleware
    • 19.RxJS 和 Interceptor
    • 20.内置 Pipe 和自定义 Pipe
    • 21.如何使用 ValidationPipe 验证 post 请求参数
    • 22.如何自定义 Exception Filter
    • 23.图解串一串 Nest 核心概念
    • 24.接口如何实现多版本共存
    • 25.Express 如何使用 multer 实现文件上传
    • 26.Nest 如何使用 multer 实现文件上传
    • 27.图书管理系统:需求分析和原型图
    • 28.图书管理系统:用户模块后端开发
    • 29.图书管理系统:图书模块后端开发
    • 30.图书管理系统:用户模块前端开发
    • 31.图书管理系统:图书模块前端开发--图书搜索
    • 32.图书管理系统:图书模块前端开发--图书增删改
    • 33.图书管理系统:项目总结
    • 34.大文件分片上传
    • 35.最完美的 OSS 上传方案
    • 36.Nest 里如何打印日志?
    • 37.为什么 Node 里要用 Winston 打印日志?
    • 38.Nest 集成日志框架 Winston
    • 39.通过 Desktop 学 Docker 也太简单了
    • 40.你的第一个 Dockerfile
    • 41.Nest 项目如何编写 Dockerfile
    • 42.提升 Dockerfile 水平的 5 个技巧
    • 43.Docker 是怎么实现的?
    • 44.为什么 Node 应用要用 PM2 来跑?
    • 45.快速入门 MySQL
    • 46.SQL 查询语句的所有语法和函数
    • 47.一对一、join 查询、级联方式
    • 48.一对多、多对多关系的表设计
    • 49.子查询和 EXISTS
    • 50.SQL 综合练习
    • 51.MySQL 的事务和隔离级别
    • 52.MySQL 的视图、存储过程和函数
    • 53.使用 Node 操作 MySQL 的两种方式
    • 54.快速掌握 TypeORM
    • 55.TypeORM 一对一的映射和关联 CRUD
    • 56.TypeORM 一对多的映射和关联 CRUD
    • 57.TypeORM 多对多的映射和关联 CRUD
    • 58.在 Nest 里集成 TypeORM
    • 59.TypeORM 如何保存任意层级的关系?
    • 60.为什么生产环境要用 TypeORM 的 migration 迁移功能?
    • 61.Nest 项目里如何使用 TypeORM 迁移
    • 62.如何动态读取不同环境的配置?
    • 63.快速入门 Redis
    • 64.在 Nest 里操作 Redis
    • 65.为什么不用 cache-manager 操作 Redis?
    • 66.两种登录状态保存方式:JWT、Session
    • 67.Nest 里实现 Session 和 JWT
    • 68.MySQL + TypeORM + JWT 实现登录注册
    • 69.基于 ACL 实现权限控制
    • 70.基于 RBAC 实现权限控制
    • 71.基于 access_token 和 refresh_token 实现登录状态无感刷新
    • 72.单 token 无限续期,实现登录状态无感刷新
    • 73.使用 passport 做身份认证
    • 74.passport 实现 GitHub 三方账号登录
    • 75.passport 实现 Google 三方账号登录
    • 76.为什么要使用 Docker Compose ?
    • 77.Docker 容器通信的最简单方式:桥接网络
    • 78.Docker 支持重启策略,是否还需要 PM2
    • 79.快速掌握 Nginx 的 2 大核心用法
    • 80.基于 Nginx 实现灰度系统
    • 81.基于 Redis 实现分布式 session
    • 82.Redis + 高德地图,实现附近的充电宝
    • 83.用 Swagger 自动生成 api 文档
    • 84.如何灵活创建 DTO
    • 85.class-validator 的内置装饰器,如何自定义装饰器
    • 86.序列化 Entity,你不需要 VO 对象
    • 87.手写序列化 Entity 的拦截器
    • 88.使用 compodoc 生成文档
    • 89.Node 如何发邮件?
    • 90.实现基于邮箱验证码的登录
    • 91.定时任务 + Redis 实现阅读量计数
    • 92.Nest 的 3 种定时任务
    • 93.Nest 里如何实现事件通信?
    • 94.HttpModule + pinyin 实现天气预报查询服务
    • 95.如何记录请求日志
    • 96.短链服务?自己写一个
    • 97.Nest 实现 Server Sent Event 数据推送
    • 98.用 minio 自己搭一个 OSS 服务
    • 99.前端如何直传文件到 Minio
    • 100.基于 sharp 实现 gif 压缩工具
    • 101.大文件如何实现流式下载?
    • 102.Puppeteer 实现爬虫,爬取 BOSS 直聘全部前端岗位
    • 103.实现扫二维码登录
    • 104.Nest 的 REPL 模式
    • 105.实现 Excel 导入导出
    • 106.如何用代码动态生成 PPT
    • 107.如何拿到服务器 CPU、内存、磁盘状态
    • 108.Nest 如何实现国际化?
    • 109.会议室预订系统:需求分析和原型图
    • 110.会议室预订系统:技术方案和数据库设计
    • 111.会议室预订系统:用户管理模块-用户注册
    • 112.会议室预订系统:用户管理模块-配置抽离、登录认证鉴权
    • 113.会议室预订系统:用户管理模块-interceptor、修改信息接口
    • 114.会议室预订系统:用户管理模块-用户列表和分页查询
    • 115.会议室预订系统:用户管理模块-swagger 接口文档
    • 116.会议室预订系统:用户管理模块-用户端登录注册页面
    • 117.会议室预订系统:用户管理模块-用户端信息修改页面
    • 118.会议室预订系统:用户管理模块-头像上传
    • 119.会议室预订系统:用户管理模块-管理端用户列表页面
    • 120.会议室预订系统:用户管理模块-管理端信息修改页面
    • 121.会议室预订系统:会议室管理模块-后端开发
    • 122.会议室预订系统:会议室管理模块-管理端前端开发
    • 123.会议室预订系统:会议室管理模块-用户端前端开发
    • 124.会议室预订系统:预定管理模块-后端开发
    • 125.会议室预订系统:预定管理模块-管理端前端开发
    • 126.会议室预订系统:预定管理模块-用户端前端开发
    • 127.会议室预订系统:统计管理模块-后端开发
    • 128.会议室预订系统:统计管理模块-前端开发
    • 129.会议室预订系统:后端项目部署到阿里云
    • 130.会议室预订系统:前端项目部署到阿里云
    • 131.会议室预定系统:用 migration 初始化表和数据
    • 132.会议室预定系统:文件上传 OSS
    • 133.会议室预定系统:Google 账号登录后端开发
    • 134.会议室预定系统:Google 账号登录前端开发
    • 135.会议室预定系统:后端代码优化
    • 136.会议室预定系统:集成日志框架 winston
    • 137.会议室预定系统:前端代码优化
    • 138.会议室预定系统:全部功能测试
    • 139.会议室预定系统:项目总结
    • 140.Nest 如何创建微服务?
    • 141.Nest 的 Monorepo 和 Library
    • 142.用 Etcd 实现微服务配置中心和注册中心
    • 143.Nest 集成 Etcd 做注册中心、配置中心
    • 144.用 Nacos 实现微服务配置中心和注册中心
    • 145.基于 gRPC 实现跨语言的微服务通信
    • 146.快速入门 ORM 框架 Prisma
    • 147.Prisma 的全部命令
    • 148.Prisma 的全部 schema 语法
    • 149.Primsa Client 单表 CRUD 的全部 api
    • 150.Prisma Client 多表 CRUD 的全部 api
    • 151.在 Nest 里集成 Prisma
    • 152.为什么前端监控系统要用 RabbitMQ?
    • 153.基于 Redis 实现关注关系
    • 154.基于 Redis 实现各种排行榜(周榜、月榜、年榜)
    • 155.考试系统:需求分析
    • 156.考试系统:技术方案和数据库设计
    • 157.考试系统:微服务、Lib 拆分
    • 158.考试系统;用户注册
    • 159.考试系统:用户登录、修改密码
    • 160.考试系统:考试微服务
    • 161.考试系统:登录、注册页面
    • 162.考试系统:修改密码、试卷列表页面
    • 163.考试系统:新增试卷、回收站
    • 164.考试系统:试卷编辑器
    • 165.考试系统:试卷回显、预览、保存
    • 166.考试系统:答卷微服务
    • 167.考试系统:答题页面
    • 168.考试系统:自动判卷
    • 169.考试系统:分析微服务、排行榜页面
    • 170.考试系统:整体测试
    • 171.考试系统:项目总结
    • 172.用 Node.js 手写 WebSocket 协议
    • 173.Nest 开发 WebSocket 服务
    • 174.基于 Socket.io 的 room 实现群聊
    • 175.聊天室:需求分析和原型图
    • 176.聊天室:技术选型和数据库设计
    • 177.聊天室:用户注册
    • 178.聊天室:用户登录
    • 179.聊天室:修改密码、修改信息
    • 180.聊天室:好友列表、发送好友申请
    • 181.聊天室:创建聊天室、加入群聊
    • 182.聊天室:登录、注册页面开发
    • 183.聊天室:修改密码、信息页面开发
    • 184.聊天室:头像上传
    • 185.聊天室:好友∕群聊列表页面
    • 186.聊天室:添加好友弹窗、通知页面
    • 187.聊天室:聊天功能后端开发
    • 188.聊天室:聊天功能前端开发
    • 189.聊天室:一对一聊天
    • 190.聊天室:创建群聊、进入群聊
    • 191.聊天室:发送表情、图片、文件
    • 192.聊天室:收藏
    • 193.聊天室:全部功能测试
    • 194.聊天室:项目总结
    • 195.MongoDB 快速入门
    • 196.使用 mongoose 操作 MongoDB 数据库
    • 197.GraphQL 快速入门
    • 198.Nest 开发 GraphQL 服务:实现 CRUD
    • 199.GraphQL + Primsa + React 实现 TodoList
    • 200.如何调试 Nest 源码?

Docker 是一种容器技术,它可以在操作系统上创建多个相互隔离的容器。容器内独立安装软件、运行服务。

但是,这个容器和宿主机还是有关联的,比如可以把宿主机的端口映射到容器内的端口、宿主机某个目录挂载到容器内的目录。

比如映射了 3000 端口,那容器内 3000 端口的服务,就可以在宿主机的 3000 端口访问了。

比如挂载了 /aaa 到容器的 /bbb/ccc,那容器内读写 /bbb/ccc 目录的时候,改的就是宿主机的 /aaa 目录,反过来,改宿主机 /aaa 目录,容器内的 /bbb/ccc 也会改,这俩同一个。

这分别叫做端口映射、数据卷(volume)挂载。

这个容器是通过镜像起来的,通过 docker run image-name。

比如:

docker run -p 3000:3000 -v /aaa:/bbb/ccc --name xxx-container xxx-image

通过 xxx-image 镜像跑起来一个叫做 xxx-container 的容器。

-p 指定端口映射,映射宿主机的 3000 到容器的 3000 端口。

-v 指定数据卷挂载,挂载宿主机的 /aaa 到容器的 /bbb/ccc 目录。

这个镜像是通过 Dockerfile 经过 build 产生的。

也就是这样的流程:

一般在项目里维护 Dockerfile ,然后执行 docker build 构建出镜像、push 到镜像仓库,部署的时候 pull 下来用 docker run 跑起来。

基本 CI/CD 也是这样的流程:

CI 的时候 git clone 项目,根据 dockerfile 构建出镜像,打上 tag,push 到仓库。

CD 的时候把打 tag 的镜像下下来,docker run 跑起来。

这个 Dockerfile 是在项目里维护的,虽然 CI/CD 流程不用自己搞,但是 Dockefile 还是要开发者自己写的。

比如我创建一个 nest 项目:

npx nest new dockerfile-test -p npm

然后执行 npm run build,之后把它跑起来:

npm run build
node ./dist/main.js

这时候访问 http://localhost:3000 可以看到 hello world,说明服务跑成功了:

那如何通过 Docker 部署这个服务呢?

我们来写下 Dockerfile:

FROM node:18

WORKDIR /app

COPY package.json .

COPY *.lock .

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install

COPY . .

RUN npm run build

EXPOSE 3000

CMD [ "node", "./dist/main.js" ]

FROM node:18 是继承 node:18 基础镜像。

WORKDIR /app 是指定当前目录为 /app

COPY 复制宿主机的 package.json 和 lock 文件到容器的当前目录,也就是 /app 下

RUN 是执行命令,这里执行了 npm install。

然后再复制其余的文件到容器内。

EXPOSE 指定容器需要暴露的端口是 3000。

CMD 指定容器跑起来时执行的命令是 node ./dist/main.js。

然后通过 docker build 把它构建成镜像:

docker build -t dockerfile-test:first .

-t 是指定名字和标签,这里镜像名为 dockerfile-test 标签为 first。

然后在 docker desktop 的 images 里就可以看到这个镜像了:

就是现在镜像稍微大了点,有 1.45 G。

我们先跑起来看看:

docker run -d -p 2333:3000 --name first-container dockerfile-test:first

-d 是后台运行。

-p 指定端口映射,映射宿主机的 2333 端口到容器的 3000 端口。

--name 指定容器名

然后就可以看到容器部分有了这个容器了:

浏览器访问 http://localhost:2333 就可以访问容器内跑的这个服务:

这就是 Dockerfile 构建成镜像,然后通过容器跑起来的流程。

但是刚才也发现了,现在镜像太大了,有 1.45G 呢,怎么优化一下呢?

这就涉及到了第一个技巧:

使用 alpine 镜像,而不是默认的 linux 镜像

docker 容器内跑的是 linux 系统,各种镜像的 dockerfile 都会继承 linux 镜像作为基础镜像。

比如我们刚刚创建的那个镜像,点开详情可以看到它的镜像继承关系:

最终还是继承了 debian 的 Linux 镜像,这是一个 linux 发行版。

但其实这个 linux 镜像可以换成更小的版本,也就是 alpine。

它裁剪了很多不必要的 linux 功能,使得镜像体积大幅减小了。

alpine 是高山植物,就是很少的资源就能存活的意思。

我们改下 dockerfile,使用 alpine 的镜像:

node:18-alpine3.14 是使用 18 版本的 node 镜像,它底层使用 alpine 3.14 的基础镜像。

然后 docker build

docker build -t dockerfile-test:second .

这次的 tag 为 second。

然后在 docker desktop 里看下:

好家伙,足足小了 900M。

我们点开看看:

可以看到它的底层 linux 镜像是 alpine3.14。

体积小了这么多,功能还正常么?

我们跑跑看:

docker run -d -p 2334:3000 --name second-container dockerfile-test:second

docker desktop 可以看到这个跑起来的容器:

浏览器访问下,依然是正常的:

alpine 只是去掉了很多 linux 里用不到的功能,使得镜像体积更小。

这就是第一个技巧。

然后再来看第二个:

使用多阶段构建

看下这个 dockerfile,大家发现有啥问题没:

有的同学可能会说:为什么先复制 package.json 进去,安装依赖之后再复制其他文件,直接全部复制进去不就行了?

不是的,这两种写法的效果不同。

docker 是分层存储的,dockerfile 里的每一行指令是一层,会做缓存。

每次 docker build 的时候,只会从变化的层开始重新构建,没变的层会直接复用。

也就说现在这种写法,如果 package.json 没变,那么就不会执行 npm install,直接复用之前的。

那如果一开始就把所有文件复制进去呢?

那不管 package.json 变没变,任何一个文件变了,都会重新 npm install,这样没法充分利用缓存,性能不好。

我们试试看就知道了:

现在重新跑 docker build,不管跑多少次,速度都很快,因为文件没变,直接用了镜像缓存:

docker build -t dockerfile-test:second .

现在我们改下 README.md:

然后重新跑 build:

现在花了 25s,其实是没有重新 npm install 的。

然后改下 package.json:

再跑 docker build

时间明显多了很多,过程中你可以看到在 npm install 那层停留了很长时间。

这就是为什么要这样写:

这里没问题,大家还能发现有没有什么别的问题么?

问题就是源码和很多构建的依赖是不需要的,但是现在都保存在了镜像里。

实际上我们只需要构建出来的 ./dist 目录下的文件还有运行时的依赖。

那怎么办呢?

这时可以用多阶段构建:

FROM node:18-alpine3.14 as build-stage

WORKDIR /app

COPY package.json .

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install

COPY . .

RUN npm run build

# production stage
FROM node:18-alpine3.14 as production-stage

COPY --from=build-stage /app/dist /app
COPY --from=build-stage /app/package.json /app/package.json

WORKDIR /app

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install --production

EXPOSE 3000

CMD ["node", "/app/main.js"]

FROM 后面添加一个 as 来指定当前构建阶段的名字。

通过 COPY --from=xxx 可以从上个阶段复制文件过来。

然后 npm install 的时候添加 --production,这样只会安装 dependencies 的依赖。

docker build 之后,只会留下最后一个阶段的镜像。

也就是说,最终构建出来的镜像里是没有源码的,有的只是 dist 的文件和运行时依赖。

这样镜像就会小很多。

我们来试试看:

docker build -t dockerfile-test:third -f 222.Dockerfile .

标签为 third。

-f 是指定 Dockerfile 的名字。

然后 desktop 里看下构建出来的镜像:

镜像体积比没有用多阶段构建的时候小了 250 M。

然后跑起来试试看:

这次映射 2335 端口到容器内的 3000 端口。

依然能正常访问:

这就是第二个技巧,多阶段构建。

使用 ARG 增加构建灵活性

我们写一个 test.js

console.log(process.env.aaa);
console.log(process.env.bbb);

打印了环境变量 aaa、bbb

跑一下:

export aaa=1 bbb=2
node ./test.js

可以看到打印了这俩环境变量:

然后我们写个 dockerfile,文件名是 333.Dockerfile:

FROM node:18-alpine3.14

ARG aaa
ARG bbb

WORKDIR /app

COPY ./test.js .

ENV aaa=${aaa} \
    bbb=${bbb}

CMD ["node", "/app/test.js"]

使用 ARG 声明构建参数,使用 ${xxx} 来取

然后用 ENV 声明环境变量。

dockerfile 内换行使用 \

之后构建的时候传入构建参数:

docker build --build-arg aaa=3 --build-arg bbb=4 -t arg-test -f 333.Dockerfile .

通过 --build-arg xxx=yyy 传入 ARG 参数的值。

点击查看镜像详情,可以看到 ARG 已经被替换为具体的值了:

然后跑起来:

docker run  --name fourth-container arg-test

这次就不用 -d 后台运行了,直接看下日志:

可以看到容器内拿到的环境变量就是 ENV 设置的。

也就是说 ARG 是构建时的参数,ENV 时运行时的变量。

灵活使用 ARG,可以增加 dockerfile 的灵活性。

这就是第三个技巧。

CMD 结合 ENTRYPOINT

前面我们指定容器跑起来之后运行什么命令,用的是 CMD:

其实还可以写成 ENTRYPOINT:

这两种写法有什么区别么?

我们来试试:

写个 444.Dockerfile

FROM node:18-alpine3.14

CMD ["echo", "光光", "到此一游"]

然后 build:

docker build -t cmd-test -f 444.Dockerfile .

然后 run 一下:

docker run cmd-test

没有指定 --name 时,会生成一个随机容器名。

就是这种:

这不是重点。

重点是用 CMD 的时候,启动命令是可以重写的:

docker run cmd-test echo "东东"

可以替换成任何命令。

而用 ENTRYPOINT 就不会:

FROM node:18-alpine3.14

ENTRYPOINT ["echo", "光光", "到此一游"]

docker build:

docker build -t cmd-test -f 444.Dockerfile .

docker run:

docker run cmd-test echo "东东"

可以看到,现在 dockerfile 里 ENTRYPOINT 的命令依然执行了。

docker run 传入的参数作为了 echo 的额外参数。

这就是 ENTRYPOINT 和 CMD 的区别。

一般还是 CMD 用的多点,可以灵活修改启动命令。

其实 ENTRYPOINT 和 CMD 是可以结合使用的。

比如这样:

FROM node:18-alpine3.14

ENTRYPOINT ["echo", "光光"]

CMD ["到此一游"]

docker build:

docker build -t cmd-test -f 444.Dockerfile .

docker run:

docker run cmd-test
docker run cmd-test 66666

当没传参数的时候,执行的是 ENTRYPOINT + CMD 组合的命令,而传入参数的时候,只有 CMD 部分会被覆盖。

这就起到了默认值的作用。

所以,用 ENTRYPOINT + CMD 的方式更加灵活。

这是第四个技巧。

COPY vs ADD

其实不只是 ENTRYPOINT 和 CMD 相似,dockerfile 里还有一对指令也比较相似,就是 ADD 和 COPY。

这俩都可以把宿主机的文件复制到容器内。

但有一点区别,就是对于 tar.gz 这种压缩文件的处理上:

我们创建一个 aaa 目录,下面添加两个文件:

使用 tar 命令打包:

tar -zcvf aaa.tar.gz ./aaa

然后写个 555.Dockerfile

FROM node:18-alpine3.14

ADD ./aaa.tar.gz /aaa

COPY ./aaa.tar.gz /bbb

docker build 生成镜像:

docker build -t add-test -f 555.Dockerfile .

docker run 跑起来:

docker run -d --name sixth-container add-test

可以看到,ADD 把 tar.gz 给解压然后复制到容器内了。

而 COPY 没有解压,它把文件整个复制过去了:

也就是说,ADD、COPY 都可以用于把目录下的文件复制到容器内的目录下。

但是 ADD 还可以解压 tar.gz 文件。

一般情况下,还是用 COPY 居多。

案例代码在小册仓库

总结

Docker 是流行的容器技术,它可以在操作系统上创建多个隔离的容器,在容器内跑各种服务。

它的流程是 Dockerfile 经过 docker build 生成 docker 镜像,然后 docker run 来跑容器。

docker run 的时候可以通过 -p 指定宿主机和容器的端口映射,通过 -v 挂载数据卷到容器内的某个目录。

CI/CD 基本也是这套流程,但是 Dockerfile 是要开发者自己维护的。

Dockerfile 有挺多技巧:

  • 使用 alpine 的镜像,而不是默认的 linux 镜像,可以极大减小镜像体积,比如 node:18-alpine3.14 这种
  • 使用多阶段构建,比如一个阶段来执行 build,一个阶段把文件复制过去,跑起服务来,最后只保留最后一个阶段的镜像。这样使镜像内只保留运行需要的文件以及 dependencies。
  • 使用 ARG 增加构建灵活性,ARG 可以在 docker build 时通过 --build-arg xxx=yyy 传入,在 dockerfile 中生效,可以使构建过程更灵活。如果是想定义运行时可以访问的变量,可以通过 ENV 定义环境变量,值使用 ARG 传入。
  • CMD 和 ENTRYPOINT 都可以指定容器跑起来之后运行的命令,CMD 可以被覆盖,而 ENTRYPOINT 不可以,两者结合使用可以实现参数默认值的功能。
  • ADD 和 COPY 都可以复制文件到容器内,但是 ADD 处理 tar.gz 的时候,还会做一下解压。

灵活使用这些技巧,可以让你的 Dockerfile 更加灵活、性能更好。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
41.Nest 项目如何编写 Dockerfile
Next
43.Docker 是怎么实现的?