Engineering

Vite + Docker 运行时环境变量注入通用方案

本文档介绍了一种通用的解决方案,用于在 Docker 容器启动时(Runtime)动态注入环境变量到 Vite 构建的静态前端应用中,解决了静态资源构建后无法感知部署环境配置的问题。

Vite + Docker 运行时环境变量注入通用方案

本文档介绍了一种通用的解决方案,用于在 Docker 容器启动时(Runtime)动态注入环境变量到 Vite 构建的静态前端应用中,解决了静态资源构建后无法感知部署环境配置的问题。

1. 核心原理

  1. 本地开发: 使用 public/env-config.js 提供默认配置。
  2. 构建部署: index.html 引入 env-config.js,该文件在 Docker 容器启动时由脚本根据环境变量动态生成/覆盖。
  3. 应用读取: 前端代码统一通过 window.__ENV__ 读取配置,并回退到 import.meta.env

2. 实施步骤与代码清单

Step 1: 准备默认配置文件

public 目录下创建 env-config.js,用于本地开发时的默认值。

public/env-config.js

window.__ENV__ = {
  // 本地开发默认值
  VITE_API_BASE_URL: "http://localhost:3000",
  VITE_APP_TITLE: "Local Dev App"
};

Step 2: 在入口 HTML 中引入

index.html<head> 标签中引入该脚本。

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <!-- 引入配置文件,注意路径需根据 base 配置调整,如 /admin/env-config.js -->
    <script src="/env-config.js"></script>
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

Step 3: 添加 TypeScript 类型声明

为了让 TS 识别 window.__ENV__,创建类型声明文件。

src/types/env.d.ts

export {};

declare global {
  interface Window {
    __ENV__?: {
      VITE_API_BASE_URL?: string;
      VITE_APP_TITLE?: string;
      [key: string]: string | undefined;
    };
  }
}

Step 4: 封装配置读取工具

创建一个工具函数来统一读取配置,优先读取运行时注入的值。

src/utils/env.ts

/**
 * 获取环境变量,优先读取运行时配置 (window.__ENV__),其次读取构建时配置 (import.meta.env)
 * @param key 环境变量名
 * @returns 变量值
 */
export const getEnv = (key: string): string => {
  // 1. 尝试从运行时注入的 window.__ENV__ 读取
  if (window.__ENV__ && window.__ENV__[key]) {
    return window.__ENV__[key]!;
  }

  // 2. 尝试从构建时 import.meta.env 读取
  return import.meta.env[key] || '';
};

// 使用示例
export const API_BASE_URL = getEnv('VITE_API_BASE_URL');

Step 5: 编写 Docker 启动脚本

创建 entrypoint.sh,用于在容器启动时提取环境变量并写入文件。

entrypoint.sh

#!/bin/sh

# 配置 Nginx 目录下的目标文件路径
# 如果你的 Vite config 设置了 base: '/admin/',这里可能需要调整路径
CONFIG_FILE="/usr/share/nginx/html/env-config.js"

# 1. 确保文件存在(如果不存在则创建空对象)
if [ ! -f "$CONFIG_FILE" ]; then
  echo "window.__ENV__ = {};" > "$CONFIG_FILE"
fi

# 2. 追加换行符,防止拼接错误
echo "" >> "$CONFIG_FILE"

# 3. 动态遍历环境变量
# 筛选以 VITE_ 开头的变量 (根据需要修改 grep 规则)
env | grep -E "^VITE_" | while read -r line; do
  # 提取 KEY 和 VALUE
  key=${line%%=*}
  value=${line#*=}
  
  # 对双引号进行转义,防止破坏 JS 语法
  value=$(echo "$value" | sed 's/"/\\"/g')
  
  # 使用追加方式覆盖配置
  echo "window.__ENV__['$key'] = \"$value\";" >> "$CONFIG_FILE"
done

# 4. 启动主进程 (通常是 Nginx)
exec "$@"

Step 6: 修改 Dockerfile

使用多阶段构建(Multi-stage Build)来构建应用并设置启动脚本。

Dockerfile

# ==========================================
# Build Stage (构建阶段)
# ==========================================
FROM node:20-alpine AS builder

# 启用 pnpm (也可替换为 npm 或 yarn)
RUN corepack enable && corepack prepare pnpm@latest --activate

WORKDIR /app

# 优先复制依赖定义文件以利用 Docker 缓存
COPY package.json pnpm-lock.yaml ./

# 安装依赖
RUN pnpm install --frozen-lockfile

# 复制源代码
COPY . .

# 执行构建
RUN pnpm build

# ==========================================
# Production Stage (生产运行阶段)
# ==========================================
FROM nginx:stable-alpine

# (可选) 安装 curl 用于健康检查
RUN apk add --no-cache curl

# 1. 从构建阶段复制构建产物到 Nginx 目录
COPY --from=builder /app/dist /usr/share/nginx/html

# 2. (可选) 复制自定义 Nginx 配置
# COPY nginx.conf /etc/nginx/conf.d/default.conf

# 3. 复制 entrypoint 脚本
COPY entrypoint.sh /entrypoint.sh

# 4. 赋予执行权限 (关键步骤)
RUN chmod +x /entrypoint.sh

# 5. 设置容器启动入口
ENTRYPOINT ["/entrypoint.sh"]

# 6. 暴露端口
EXPOSE 80

# 7. 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]

3. 验证与使用

构建镜像后,在启动容器时通过 -e 传递环境变量,即可动态改变前端配置。

docker run -d -p 8080:80 \
  -e VITE_API_BASE_URL="https://api.production.com" \
  -e VITE_APP_TITLE="Production App" \
  my-vite-app

启动后,访问页面并查看生成的 env-config.js 或在控制台输入 window.__ENV__ 验证结果。


4. 安全注意事项 ⚠️

  1. 公开可见: 这种方式注入的所有变量最终都会出现在用户的浏览器中。绝对不要通过此方式注入敏感信息(如 AWS_SECRET_KEY, DB_PASSWORD, OAUTH_CLIENT_SECRET 等)。
  2. 筛选规则: 建议在 entrypoint.sh 中严格限制 grep 规则(如仅允许 ^VITE_^PUBLIC_),防止意外泄露容器内的其他系统环境变量。