工程经验 - 开源项目的 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 部署功能