文章

故城瞎折腾系列第一期【你要不要动手封装个前端Docker容器玩一玩】

1. 背景

  • 一年多前我分享的一片前端Docker容器化入门反响不错,被 Docker 中文社区转载
  • 很多人估计看完都没有找到下手点,所以我将我常用的Docker容器分享出来,希望能帮助大家使用容器化管理前端发布
  • 故城已经利用这个Docker容器开发了很多前端SPA项目了

文章链接:前端应用 Docker 容器化最佳实践
文章链接:30分钟手把手带你入门前端容器化(Docker)

2. 导语

  • 由于故城更倾向于做一些篇幅内容较短的技术文章,所以暂时不分析实现原理,也好让自己更好的坚持下去
  • 写过文章的同学都知道,字数写多了就很容易犯强迫症,就很容易纠结文章内容,反复读写多遍,导致很难坚持下去,故城很多年以前也写出过一些阅读量不错的技术文章,由于篇幅很长,写作量大,所以未能坚持下来
  • 此篇文章只分享 Docker 容器如何使用,感兴趣自己可以进一步封装
  • 废话不多说先上仓库:Nginx-Runner,对你有帮助请 star、fork 支持一下

以下是我的【故城瞎折腾系列】文章

第一篇:故城瞎折腾系列第一期【你要不要动手封装个前端Docker容器玩一玩】
第二篇:故城瞎折腾系列第二期【都2024年了,你还在手动部署前端项目吗】
第三篇:故城瞎折腾系列第三期【你看我这样用Nginx部署前端Docker项目,姿势对不对】

3. Docker容器(Nginx-Runner)包含的功能

  • 封装了前端 web 应用基本的 nginx 配置
  • 利用 docker env 提供了前端环境变量注入的功能

4. Nginx-Runner如何使用

4.1. 在项目根目录新建Dockerfile

pnpm缓存,使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# syntax = docker/dockerfile:experimental
FROM --platform=${BUILDPLATFORM:-linux/amd64,linux/arm64} node:20-buster AS builder

ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

WORKDIR /src
COPY ./ /src

RUN --mount=type=cache,target=/src/node_modules,id=myapp_pnpm_module,sharing=locked \
    --mount=type=cache,target=/pnpm/store,id=pnpm_cache \
        pnpm install

RUN --mount=type=cache,target=/src/node_modules,id=myapp_pnpm_module,sharing=locked \
        pnpm run build

FROM --platform=${BUILDPLATFORM:-linux/amd64,linux/arm64} ghcr.io/rookie-luochao/nginx-runner:latest

COPY --from=builder /src/dist /app

无缓存,使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# syntax = docker/dockerfile:experimental
FROM --platform=${BUILDPLATFORM:-linux/amd64,linux/arm64} node:20-buster AS builder

ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

WORKDIR /src
COPY ./ ./

# RUN两次方便观察install和build, 也可以用pnpm cache and locked
RUN pnpm install
RUN npm run build

FROM --platform=${BUILDPLATFORM:-linux/amd64,linux/arm64} ghcr.io/rookie-luochao/nginx-runner:latest

COPY --from=builder /src/dist /app

4.2. 使用 github-action 构建 docker 镜像

项目根目录执行:新建.github文件夹 => 在.github文件夹下面新建workflows文件夹 => 新建 docker-image-ci.yml文件 然后贴入一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
name: Docker Image CI

on:
  push:
    tags:
      - v*

  # 这个选项可以使你手动在 Action tab 页面触发工作流
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: get version
        id: vars
        run: echo ::set-output name=version::${GITHUB_REF/refs\/tags\/v/}

      - uses: actions/checkout@v4

      - name: set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: set up docker buildx
        uses: docker/setup-buildx-action@v3

      - name: login ghrc hub
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: $
          password: $

      - name: build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          platforms: linux/amd64,linux/arm64
          tags: |
            ghcr.io/$/webapp:$
            ghcr.io/$/webapp:latest

4.3. 配置 github-action Token,并触发 docker 镜像构建

  1. 将项目推到 github 仓库
  2. 需要申请一个 github Token(建议权限全部勾选上)
  3. 点击仓库 Tab 页面的 Settings
  4. 点击左侧边栏:Secrets and variables
  5. 点击 Actions
  6. 点击 New repository secrets 添加 github Token
  7. 添加 GHCR_TOKEN 对应的 TOKEN,github-action脚本里面会用到
  8. 新建仓库 release 去触发 docker 镜像构建

4.4. 如何运行 Docker 镜像

这里以故城的 openapi-ui 项目为例子

  1. 拉取 docker 镜像到本地,docker pull ghcr.io/rookie-luochao/openapi-ui:latest
  2. 执行 docker 镜像,docker run -d -p 80:80 -e APP_CONFIG=env=zh,appNameZH=简洁美观的接口文档 ghcr.io/rookie-luochao/openapi-ui:latest
1
2
3
4
5
# pull Docker image
docker pull ghcr.io/rookie-luochao/openapi-ui:latest

# start container, nginx reverse proxy custom port, for example: docker run -d -p 8081:80 ghcr.io/rookie-luochao/openapi-ui:latest
docker run -d -p 80:80 -e APP_CONFIG=env=zh,appNameZH=简洁美观的接口文档 ghcr.io/rookie-luochao/openapi-ui:latest

5. Nginx-Runner如何注入环境变量,兼容 .env 文件设置的环境变量

5.1. 改造index.html文件,加入环境变量占位符

如下代码,包含meta标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/logo_mini.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta
      name="keywords"
      content="openapi-ui, swagger-ui, openapi, swagger, openapi3, openapi31, api-documentation, openapi-specification"
    />
    <meta name="description" content="openapi ui document/specification, swagger ui document/specification" />
    <meta name="env" content="__ENV__" />
    <meta name="app_config" content="__APP_CONFIG__" />
    <title>openAPI UI</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>

5.2. 增加获取环境变量工具函数

获取环境变量工具函数的完整代码 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import appConfig, { IConfig } from "@/config";

export function getConfig(): IConfig {
  const mateEnv = import.meta.env;
  const defaultAppConfig = {
    appName: mateEnv?.VITE_appName || "",
    appNameZH: mateEnv?.VITE_appNameZH || "",
    baseURL: mateEnv?.VITE_baseURL || "",
    version: mateEnv?.VITE_version || "",
    env: mateEnv?.VITE_env || "",
  };

  // dev mode get env var by src/config.ts file, prod mode get env var by mate, write the mate tag of HTML through docker arg var
  // mate tag name is:app_config, content format is:appName=webapp,baseURL=https://api.com,env=,version=
  if (import.meta.env.DEV) {
    return appConfig;
  } else {
    const appConfigStr = getMeta("app_config");

    if (!appConfigStr) return defaultAppConfig;

    return parseEnvVar(appConfigStr);
  }
}

function getMeta(metaName: string) {
  const metas = document.getElementsByTagName("meta");

  for (let i = 0; i < metas.length; i++) {
    if (metas[i].getAttribute("name") === metaName) {
      return metas[i].getAttribute("content");
    }
  }

  return "";
}

function parseEnvVar(envVarURL: string) {
  const arrs = envVarURL.split(",");

  return arrs.reduce((pre, item) => {
    const keyValues = item.split("=");

    return {
      ...pre,
      [keyValues[0]]: keyValues[1],
    };
  }, {} as IConfig);
}

完整例子可以直接使用故城的 react + docker模板

6. 结语

  • Nginx-Runner的基本功能和使用
  • Nginx-Runner如何注入前端环境变量
  • 参考Docker入门
  • 看都看完了,还不动手操作一波
本文由作者按照 CC BY 4.0 进行授权