Docker部署monorepo项目
最近在使用pnpm和monorepo搭建一个前后端项目,为了方便部署,决定统一走Docker环境,本文主要记录整个过程中遇到的一些问题。
整个项目位于github上面。
docker部署node开发环境
这一步可以参考nodejs官网上面的示例:Dockerizing a Node.js web app
下面整理一些相关的问题
减少镜像体积
参考
查了一下发现Node官方的Docker镜像有三种版本
node:<version>
,基于Debian,常规版本,体积最大,功能最全node:<version>-shim
,基于Debian,移除了很多公共软件包,只有最小的node运行环境node:<version>-alpine
,基于alpine,体积最小,某些C环境的软件可能不支持,但大部分兼容还是可以的
由于整个项目基于Node V16
开发,打包时发现默认FROM node:16.13.0
打出来的镜像有900多M,改成使用FROM node:16.13.0-alpine
打出来的包只有100来M了
那node_modules里面的依赖文件该怎么处理呢?是打在镜像里面,还是在容器初始化的时候安装?
看起来出于镜像大小考虑,我们应该避免把node_modules放在镜像里面;换言之,镜像只保存package.json
,依赖在容器初始化的时候安装,Dockerfile就类似于下面的结构
FROM node:16.13.0-alpine
WORKDIR /usr/src/app
COPY . .
EXPOSE 8080
CMD npm i && node index.js
但实际上,如果每个容器都要安装依赖,这在需要频繁修改镜像和启动容器的情况下,安装依赖就会耗费非常庞大的时间。
Docker在构建镜像时,会使用缓存layer,根据Building Efficient Dockerfiles - Node.js这篇文章的介绍,将node_modules放在镜像里面构建更为合理
FROM node:16.13.0-alpine
WORKDIR /usr/src/app
# 先安装依赖
COPY package*.json ./
RUN npm install
# 在package.json不发生改变的情况下,前面RUN得到的结果都会被缓存,将会节省大量的构建和启动时间。
COPY . .
EXPOSE 8080
CMD node index.js
由于Docker分层构建,以及复用公共层缓存的设计,如果对于镜像的大小不是特别在意,感觉并不需要过分关注如何优化镜像大小这类问题。
开发环境
在开发环境下,不可避免地需要频繁改动源码,如果每改动一下源码都要重新构建镜像启动容器,那还不如本地开发呢?
因此需要使用volume
来挂载源代码,把docker容器想象成一台远程虚拟机,volume就像是使用remote ftp一样在本地更新文件,然后定期同步到远程机器上去,只不过这里是宿主机与docker容器的文件目录映射。
因此在开发环境下,源码应该是放在volume下频繁更新的,在运行容器的时候指定对应的volume
docker run -p 8080:8080 -v ~/docker_demo/server:/usr/src/app docker_test_server
如果node index.js
里面依赖了node_modules里面的模块的话,上面的命令运行并不会成功,参考stack overflow上面的这个问题。
原因是我们构建镜像时就安装了node_modules目录到/usr/src/app
,然后在启动时容器将~/docker_demo/server
这个目录声明为vloumn挂载到容器的/usr/src/app
,相当于覆盖了容器中的/usr/src/app
目录,因此在运行时,源码文件就找不到node_modules了。
因此,要解决这个问题,需要先将容器里面的/usr/src/app/node_modules
映射到宿主机~/docker_demo/server/node_modules
下,再将宿主机~/docker_demo/server
整个目录映射到容器/usr/src/app
下,可以通过Dockerfile中的VOLUME
关键字实现
COPY package*.json ./
RUN npm install
VOLUME [ "/usr/src/app/node_modules/" ]
COPY . .
这样,更新源码之后,重启镜像就可以看见最新效果了。如果是使用了代码热更新之类的工具,比如webpack、vite等前端项目,或者nodemon --watch
、supervisor
等工具,甚至连容器都不需要再重启了,可以复刻与本地基本一致的开发体验。
前面提到,node_modules里面的代码是放在镜像里面,通过虚拟卷的方式映射到本地目录,存在的问题
- 在本地编辑器里面打开node_modules里面看不到具体的文件(不知道是不是我操作的问题)
- 即使能看到文件,容器(Linux)与宿主机(Mac、Windows)可能不是同一个系统,文件可能也不一样
这可能影响eslint、代码补全等功能,因此宿主机本地也可以安装一份node_modules用于开发,然后将其放在.dockerignore
里面,这样本地的node_modules文件就不会直接复制到镜像里面了
.dockerignore
文件的语法格式与.gitignore
基本一致
node_modules
npm-debug.log
多容器之间通信
参考
既然都用上了Docker了,那整个开发环境都可以用Docker来实现。
现在我们用docker启动了多个容器,一个是node server服务,一个是monogo 数据库服务,一个前端vite服务,这三个服务之间肯定是需要通信的。
容器之间的通信通过network实现,参考:https://blog.csdn.net/weixin_39729840/article/details/109906703。因为在同一宿主机内的容器都接入了同一个网桥,因此这些容器之间就能够通过容器IP直接通信。
首先需要创建一个network
docker network create xxx_network
获得network并启动容器之后,可以看到每个容器分配到的ip
# 启动容器时指定network
docker run --network xxx_network xxx_image
# 查看network
docker network ls
docker network inspect xxx_network
可以看到network下具体的结果
然后通过ip和端口号访问目标容器即可
由于每个容器分配的虚拟IP貌似是会变化的,为了保证代码里面访问地址的可靠性,可以使用container name作为host域名来访问
// await mongoose.connect("mongodb://172.30.0.2/test"); // 通过ip链接其他容器里面的数据库
await mongoose.connect("mongodb://database/test"); // 通过容器名称访问
docker-compose
参考
容器逐渐变多的话,按个手动管理肯定是非常麻烦的,docker-compose可以让我们使用一份声明来件来管理镜像创建、容器启动、容器network等
目前用的最多的一个技巧是build
字段:除了直接通过image
字段指定镜像名称之外,也可以通过build
字段指定要镜像打包上下文,这在monorepo项目里面非常有用,下面就会提到了
pnpm workspace管理monorepo
相较于npm
和yarn
,pnpm
的有点在于下载速度非常快,同时解决了一些潜在的安全问题,如影子依赖等
目前越来越多的前端库使用pnpm monorepo的形式构建,比如vite
等。
使用pnpm构建monorepo也非常简单,按照下面这三步来就可以
第一步:在项目根目录创建文件pnpm-workspace.yaml
packages:
- 'packages/**'
第二步:安装依赖
# 安装全局依赖
pnpm i react -w
# 安装某个模块的局部依赖
cd packages/mod1
pnpm i axios -S
第三步:--filter
将mod1添加为mod2的依赖
pnpm i mod1 -r --filter mod2
接下来就是愉快的编码环节了。
docker部署monorepo开发环境
参考
monorepo部署在Docker项目中,麻烦的还是老朋友node_modules。
不同的monorepo构建工具实现可能有差异,但总的结构大概都是一个项目根目录下的node_modules和各个package目录下面的node_modules,然后由根目录下的*-lock
文件来锁定整个项目的依赖版本。
pnpm提供了fetch
命令从pnpm-lock.yaml
文件直接下载文件到.pnpm-store
中,然后pnpm install --offline
可以只从本地store目录中安装模块。
基于这个功能,我们可以在构建镜像时将lock文件的包打包到镜像中,然后复制整个monorepo项目,最后再执行install --offline
,这样可以尽量复用已下载的模块,避免了install安装远程模块的耗时,相当于只有packages里面的模块会随volume更新
假设我们现在的monorepo项目结构是这样的
├── README.md
├── docker-compose.yml
├── node_modules
│ ├── @babel
│ ├── xxx
├── package.json
├── packages
│ ├── admin
│ ├── example-vue
│ ├── sdk
│ └── server
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.json
对于admin这个管理后台项目而言,其Dockerfile可以按照如下形式书写
FROM node:16.13.0-alpine
WORKDIR /projects
COPY ./pnpm-lock.yaml ./
RUN npm install -g pnpm \
&& pnpm fetch
# 将整个项目复制下来
COPY . .
RUN pnpm install -r --offline
VOLUME ["/projects/node_modules/", "/projects/packages/admin/node_modules/", "/projects/.pnpm-store/"]
# 启动admin项目的开发环境
CMD cd packages/admin && npm run dev
在启动容器时,再将当前目录挂载到容器的/projects
目录下面,就可以体验monorepo的Docker开发环境了。
由于存在多个项目,因此打包镜像就体现了docker-compose
的优势
version: '3.7'
services:
database:
image: mongo
restart: always
volumes:
- ~/data/db:/data/db
networks:
- app-network
server:
build:
context: ./
dockerfile: ./packages/server/Dockerfile # 每个package对应的Dockerfile
volumes:
- .:/projects
ports:
- 1546:1546
tty: true
networks:
- app-network
admin:
build:
context: ./
dockerfile: ./packages/admin/Dockerfile
volumes:
- .:/projects
ports:
- 3000:3000
tty: true
networks:
- app-network
networks:
app-network:
driver: bridge
在目录下面启动一下docker-compose up -d
即可,非常方便
小结
本文主要记录了使用Docker构建monorepo开发环境的过程,主要包括
- Docker搭建Node开发和部署环境
- pnpm workspace实现monorepo
- Docker部署monorepo,主要借助了
pnpm fetch
和pnpm install --offline
这两个特点
由于之前对于Docker的了解基本上只停留在概念和可视化界面上,经过这次的操作,更加体会到了Docker的一些优点:这才叫打包嘛~
关于Docker,还有很多要学习的地方,比如Dockerfile优化构建、私有Docker镜像,k8s等等,接下来有空的话再继续学习吧。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。