笔者一直想学前端来着,然而终于拖到了现在还是啥也不会。
2021 年的 CNSS Recruit 平台搭建工作由我负责后端和运维工作, esp 哥哥负责前端开发, 然而我的电脑上没有前端需要的开发环境, 单纯为了测试部署一整套环境还是挺麻烦的, 所以这回我打算用 Docker 容器搞一个环境用来 Build 前端的代码.
摸索了一个下午, 大概尝试了三种方案, 各有利弊, 记录以备后用:
- Docker 命令行 + entrypoint
- Docker-compose + dockerfile
Docker 命令行 + entrypoint.sh
前端是用 Vue 写的, 生成代码需要用 Node.js 打包, 原版 Node.js 的镜像怎样才能为我所用呢?
很显然, 我们在原版的 Node.js 镜像基础上, 修改它的 entrypoint.sh
就可以了.
首先需要有一个
entrypoint.sh
放在和项目文件同级的目录下, 里面写好想要执行的命令:npm config set registry https://registry.npm.taobao.org npm install --legacy-peer-deps npm run build
将项目文件挂载到容器内(这里是
/app
)docker run -v absolute/path:/app -w /app --rm -it --entrypoint /bin/sh node:alpine /app/entrypoint.sh
- 若挂载正确, 脚本无误的话就会看到容器内正在运行
entrypoint.sh
中的任务. - 运行完毕之后就会看到
./dist
中已经有了打包好的文件.
命令行参数的具体含义可以参考 Docker Docs, 这里额外做一点解释:
-v
是设置容器的挂载卷, 相当于把宿主机的某个目录 (:
前 ) 和容器内的某个目录 (:
后 ) 连接起来实现数据互通, 注意这里必须使用绝对路径.-w
是设置容器中的工作目录, 因为我们后面输入的所有指令都是在这个工作目录下执行的, 所以我们要把项目文件挂载到这里.-it
是返回一个交互式终端, 如果不打算实时看到容器里面的情况的话就不必添加这个参数.--rm
是当容器内的命令运行完毕之后自动销毁容器, 实现 "把一个容器当一条命令" 的效果.--entrypoint
是指定我们要运行的entrypoint.sh
, 无视镜像内定义的entrypoint.sh
和CMD
指令.按理来说, 我们应该运行的命令是
/bin/sh /app/entrypoint.sh
, 但是由于 docker 的机制, 我们--entrypoint
后面需要跟一个可执行文件, 而/app/entrypoint.sh
作为传递给可执行文件/bin/sh
的参数, 则应当放在命令的最后.所以
--entrypoint /app/entrypoint.sh
是根本不管用的.
这种方案的优点非常明显, 灵活, 简单, 快捷. 写好一次 entrypoint.sh
, 以后只要简单执行一下命令就可以了.
缺点也存在, 即部署时需要单独起一个 nginx 容器, 另外手动把编译好的网页拷贝到 nginx 的目录中, 自动化程度略低.
docker-compose 和 dockerfile
搭配
这是一种集成程度更高的部署方案, 大体思路如下:
- node 容器负责从项目源代码构建网页;
- nginx 容器接收连接;
- 通过 docker-compose 实现和其他服务的连接;
关于 1 和 2, 我们用到了 docker 的多阶段构建.
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm config set registry https://registry.npm.taobao.org && \
npm install --legacy-peer-deps
COPY ./ .
RUN npm run build
FROM nginx:alpine AS prod
COPY --from=builder /app/dist /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/nginx.conf
通过上面的这个 dockerfile
我们可以方便的定制出一个 nginx 容器, 其中已经包含了我们的网页和 nginx 相关配置文件.
然后我们通过 docker-compose 进一步和其他服务做好互通:
version: '3'
services:
db:
# some database
nginx:
build: ./src/recruit_cnss_2021_frontend
container_name: nginx
restart: always
environment:
TZ: Asia/Shanghai
ports:
- 80:80
- 443:443
volumes:
- ./nginx/logs:/var/log/nginx
# - ./nginx/www:/usr/share/nginx/html
# - ./nginx/config:/etc/nginx
depends_on:
- backend
networks:
recruit_net:
backend:
# some backend
networks:
recruit_net:
相较于前一种方案, 第二个的集成度很高, 可以说是开箱即用, 一个 docker-compose up --build
直接从源码构建并完成部署.
但是缺点也很明显, 如果只是想改一下 nginx.conf
, 或者是前端代码有更新, 都需要重新 build 镜像, 时间消耗比较大, 且不太容易搭配 CI/CD
工具.
一个小坑
这个问题困扰了我好几个小时,场景是这样的:
你在一个 dockerfile 里面 COPY/生成 了一些文件到容器内目录 A, 然后在启动容器阶段, 你将容器内的目录 A 挂载到了宿主机的目录 B 上, 那么在 B 中你能否看到 dockerfile 构建过程中释放到 A 中的文件呢?
答案是不能的.
以上面第二种方案为例, 如果把 compose 文件中的那几行注释取消掉, 服务是无法正常运行的. 原因和 docker 的原理相关, 如果要简单理解的话(因为我也不懂), 可以认为是:
构建在容器启动之前, 而容器启动时会把对应目录挂载上, 所以原本释放出的文件都无了, 取而代之的是宿主机对应目录的内容.
所以不要指望 dockerfile
里面写几条 RUN
然后把释放出的文件一挂载取回宿主机就删容器这种操作啦.
麻了我也太菜了, 基础不牢地动山摇, 这些小细节纠结太多花费太多时间, 本质上还是对底层原理了解太不到位!