工程经验 - 开源项目的 docker 部署优化实践
一、前言
前两天在调研开源工具,看到个不错的项目,国人开发,感觉做的不错,但是没有 docker 部署支持。于是 fork 下来,花了点时间给这个项目做了下相关的支持
做这个过程调研学习了 docker 相关的知识:
- 构建上下文
- 镜像体积优化
本文代码:jalr4ever/buitar
二、项目分析
首先这是一个前端项目,不依赖于服务端,作为一名后端来说,没做过相关的开发编译,那还是抄一下人家是怎么玩儿的编译吧
从 README 来看,其是使用 pnpm 工具作为包管理、编译
pnpm install
# 构建
pnpm build
# 本地开发(localhost:8282)
pnpm dev
并且有做 github action 的 workflow
name: Deploy Jekyll with GitHub Pages dependencies preinstalled
on:
push:
branches: ["main"]
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Install and Build
run: |
npm install pnpm -g
pnpm install --no-frozen-lockfile
pnpm build
cd packages/buitar
pnpm build:ghpages
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: ./packages/buitar/dist
destination: ./_site
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: ./packages/buitar/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
此时,我们可以直接获取到其编译的基础镜像是 ubuntu,语句为:
npm install pnpm -g
pnpm install --no-frozen-lockfile
pnpm build
cd packages/buitar
pnpm build:ghpages
最后一个阶段是发布到 github-pages 这个我们不需要
三、Dockerfile - 基于 pnpm
从基础镜像 & pnpm 的内容来看,其是一个基于 pnpm 作为编译、代理的项目,那我们将这一套直接在 dockerfile 体现出来就好
在项目根目录建立:dockerfile/Dockerfile.pnpm
FROM ubuntu:latest
RUN apt-get update && \
apt-get install -y curl git && \
curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
apt-get install -y nodejs
RUN npm install -g pnpm
WORKDIR /app
COPY . .
RUN pnpm install --no-frozen-lockfile
RUN pnpm build
# Only 8282
EXPOSE 8282
# By pnpm
CMD ["pnpm", "dev"]
执行 docker 构建命令
docker build -t jalr4ever/buitar:latest -f dockerfile/Dockerfile.pnpm .
镜像成功构建,执行 docker run 测试建立容器
docker run --rm -p 8282:8282 jalr4ever/buitar
发现直接报错,说浏览器无法打开的错误,排查一番原来是 CMD 入口命令中,会出发打开浏览器。最简单的解决方案就是禁止相关的默认错误,对于这个项目,我们需要去将每个 vite.config.ts 文件关于 open 的值改为 false
重新构建镜像、建立容器,可以发现成功启动
四、Dockerfile - 基于 Nginx
我们正常作为非开发,而是生产使用不会使用 node or pnpm 这种作为反向代理,而是会使用 nginx,原因有几个:
- 高性能
- 漏洞少
- 资料多
- 功能多
简简单单写个 nginx 路由 dockerfile/nginx.conf,按照端口做路由,路由指向编译后的 dist 根路径
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8282;
server_name localhost;
location / {
root /app/packages/buitar/dist/;
index index.html;
}
}
server {
listen 8283;
server_name localhost;
location / {
root /app/packages/buitar-editor/dist/;
index index.html;
}
}
接着写 dockerfile/Dockerfile.nginx
FROM ubuntu:latest
RUN apt-get update && \
apt-get install -y curl git nginx && \
curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
apt-get install -y nodejs
RUN npm install -g pnpm
WORKDIR /app
COPY . .
COPY dockerfile/nginx.conf /etc/nginx/
RUN pnpm install --no-frozen-lockfile
RUN pnpm build
EXPOSE 8282 8283
CMD ["nginx", "-g", "daemon off;"]
执行 docker 构建命令
docker build -t jalr4ever/buitar:latest -f dockerfile/Dockerfile.nginx .
镜像成功构建,执行 docker run 测试建立容器
docker run --rm -p 8282:8282 jalr4ever/buitar
注意点:由于 docker build 命令的构建上下文指向的是 . 当前目录,因此 COPY nginx.conf 需要加上相对路径 dockerfile/
五、Dockerfile - 镜像体积优化
其实到上一章节话,镜像已经构建完成,并且可以使用了,不过镜像体积非常大,这种前端工程镜像理论上不应该大于 500MB,毕竟没有 AI 模型之类的
镜像体积优化从我已有的知识来看,主要是:
- 基础镜像 alpine:减少基础镜像体积
- 合并 RUN 语句:减少镜像 layer,从而减少体积
改造一版:
FROM alpine:latest
RUN apk update && \
apk add --no-cache nodejs npm curl nginx && \
npm install -g npm@latest pnpm && \
rm -rf /var/cache/apk/*
WORKDIR /app
COPY . .
COPY dockerfile/nginx.conf /etc/nginx/nginx.conf
RUN pnpm install --no-frozen-lockfile && \
pnpm build
EXPOSE 8282 8283
CMD ["nginx", "-g", "daemon off;"]
但是这样下来还有 1.34GB 的大小?是哪一层出现问题了,这里使用开源镜像体积分析工具,dive(此处不详解)
结果发现是 COPY . . 这一步就导致镜像新增了一个层,这一层高达 1GB,而且有很多文件是我们不需要的依赖,我们只需要几个 dist 目录下的编译产物
而占用大的是 node_modules 这些依赖目录,那我们执行 rm -rf 把这些删除掉?不可能,因为 COPY . . 已经是一个新的 layer,其他的 layer 是无法删除这个 layer 的数据的
此时,可以利用多阶段构建技术作为解决方案,镜像的体积会取决于最后一个 stage 的所有 layer 加起来的体积:
# stage-1: build
FROM node:alpine as builder
RUN npm install -g pnpm
WORKDIR /app
COPY . .
RUN pnpm install --no-frozen-lockfile && \
pnpm build
# stage-2: package-static
FROM nginx:alpine
COPY --from=builder /app/packages/buitar/dist /usr/share/nginx/html/buitar
COPY --from=builder /app/packages/buitar-editor/dist /usr/share/nginx/html/buitar-editor
COPY --from=builder /app/packages/svg-chord/dist /usr/share/nginx/html/svg-chord
COPY --from=builder /app/packages/to-guitar/dist /usr/share/nginx/html/to-guitar
COPY --from=builder /app/packages/tone-player/dist /usr/share/nginx/html/tone-player
COPY dockerfile/nginx.conf /etc/nginx/nginx.conf
EXPOSE 8282 8283
CMD ["nginx", "-g", "daemon off;"]
我们分成两个阶段:
- 编译
- 打包静态资源
执行 docker 构建命令
docker build -t jalr4ever/buitar:latest -f dockerfile/Dockerfile.nginx .
最后镜像的体积只有 300MB 不到
@jalr4ever ➜ /workspaces/buitar (main-jalr4ever-docker-support) $ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
jalr4ever/buitar latest cd38d535eec2 48 minutes ago 269MB
符合预期,至此,从 0 为 一个开源项目支持了 docker 部署功能