运行时环境变量注入指南

解决前端构建时无法感知运行时配置值的结构性矛盾。一份镜像,多套环境——通过占位符 + sed 运行时替换,适配 dev/test/prod 任意环境

#Docker#Kubernetes#前端部署#环境变量#CI/CD#DevOps

运行时环境变量注入指南

面向前端开发与 DevOps 部署的通用方案,解决"构建时不知道运行时配置值"的问题。

1. 概述

1.1 问题背景

前端代码需要引用后端 API 地址、第三方服务密钥等配置值。开发环境用 localhost,生产环境用 K8s 内部域名。但在实际部署中,存在一个结构性矛盾:

构建与部署是两件事,但配置值在构建时就被固化了。

具体来说:

  1. 前端构建工具(Vite/Create-React-App/Webpack)将 import.meta.envpnpm build 时内联到 JS 产物中。构建完成后,dist/ 是纯静态文件,无法再修改其中的配置值。

  2. K8s 只能注入容器进程环境变量,无法修改 JS 文件内容。容器启动时,K8s 会将定义的环境变量注入进程环境空间,但前端 JS 跑在用户浏览器里,读不到容器环境变量。

  3. 传统方案是每个环境构建一次镜像——dev 打一次,test 打一次,prod 再打一次。这导致:

问题说明
产物不一致不同时间构建,可能拉取到不同依赖版本、不同 commit
维护成本高每次发版需要构建 N 个镜像,N 套版本号
不可追溯出问题后难以确认 prod 镜像和 test 镜像是否真正一致

1.2 方案动机

本方案的核心目标:一份镜像,多套环境

图表加载中…

构建时留(占位符),运行时(sed 替换)。同一份 JS bundle,通过不同环境变量适配任意环境。

1.3 为什么 K8s 不能直接注入前端配置

这是很多初学者的疑问。K8s 明明支持 env 配置环境变量,为什么还要搞这么复杂?

图表加载中…
  • K8s 环境变量注入到容器进程的环境空间
  • JS 在用户浏览器的 JS 引擎中执行
  • 两者是完全不同的进程空间,无法跨进程读取

entrypoint.sh 就是中间人——在容器启动时,把容器环境变量写入 JS 文件。这样 JS 加载时,文件里的字符串已经是正确值。

1.4 方案流程

Plain Text
构建时:写入占位符 __VAR_NAME__ → 打进 JS bundle
运行时:容器启动,读取 K8s 注入的环境变量 → sed 替换占位符为真实值 → 启动 Nginx

同一份镜像,通过不同环境变量,适配 dev / test / prod 等任意环境。

1.5 适用场景

  • Vite、Create-React-App、Webpack 等前端构建工具
  • Docker 多阶段构建 + Nginx 静态服务
  • K8s / Docker Compose 等支持容器环境变量注入的编排平台

2. 架构原理

为什么占位符能被替换

占位符是普通字符串,打包进 JS 后仍是普通字符串。sed 在 JS 文件中搜索并替换,等价于文本编辑。JS 引擎执行的是替换后的代码。

.env 文件

.env 文件在构建时被读取并固化。修改 .env 需要重新构建镜像。


3. 实施步骤

步骤 1:代码中使用占位符

在源码中,通过 import.meta.env 引用变量。构建工具会将其替换为 ENV 定义的值。

TYPESCRIPT
// src/config/env.ts
const API_BASE_URL = import.meta.env.VITE_API_URL;
export { API_BASE_URL };

构建后,JS 产物中的值为 __VITE_API_URL__(占位符)。

步骤 2:Dockerfile 中声明占位符

Dockerfile
# 构建阶段
FROM node:18 as builder
WORKDIR /usr/src/app

COPY package.json ./
RUN pnpm install

COPY ./ ./

# 声明环境变量,值为占位符(双下划线包裹)
ENV VITE_API_URL=__VITE_API_URL__
RUN pnpm build

# 运行阶段
FROM nginx:latest
WORKDIR /usr/share/nginx/html/
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html/

COPY deploy/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

关键点ENV 的值是占位符字符串本身,不是要引用的真实地址。真实地址由运行时注入。

步骤 3:编写 entrypoint.sh

Bash
#!/bin/sh
set -e

HTML_DIR=/usr/share/nginx/html

# 通用替换函数:在所有 JS 文件中将占位符替换为环境变量真实值
replace_env() {
  placeholder="$1"
  value="$2"
  find "$HTML_DIR" -name '*.js' -exec sed -i "s|${placeholder}|${value}|g" {} +
}

# 执行替换(每个变量一行)
replace_env "__VITE_API_URL__" "${VITE_API_URL}"
replace_env "__VITE_WS_URL__" "${VITE_WS_URL}"

# 生成 Nginx 配置(如需要模板替换)
envsubst < /etc/nginx/mysite.template > /etc/nginx/conf.d/default.conf

# 启动 Nginx
exec nginx -g 'daemon off;'

关键点

  • shebang 用 #!/bin/sh,而非 #!/bin/bash——多数生产镜像(Alpine、distroless、Slim)不提供 bash 环境
  • set -e:替换失败时容器启动失败,避免静默错误
  • 分隔符用 | 而非 /,避免 URL 中的 / 导致 sed 语法错误
  • find ... {} + 批量执行,比 \; 逐个执行更高效
  • ash(Alpine 默认 shell)不支持 local 关键字,函数内变量直接赋值即可
  • 新增变量只需添加一行 replace_env 调用

步骤 4:K8s 部署时注入环境变量

YAML
apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
    - name: frontend
      image: harbor.example.com/project/frontend:latest
      env:
        - name: VITE_API_URL
          value: "https://api.prod.example.com"
        - name: VITE_WS_URL
          value: "wss://ws.prod.example.com"

容器启动时,K8s 将环境变量注入容器进程。entrypoint.sh 读取 ${VITE_API_URL} 执行替换。


4. 如何添加新变量

假设需要新增 VITE_OAUTH_URL

位置操作示例
源码import.meta.env.VITE_OAUTH_URL 引用const oauthUrl = import.meta.env.VITE_OAUTH_URL
Dockerfile添加 ENV 声明ENV VITE_OAUTH_URL=__VITE_OAUTH_URL__
entrypoint.sh添加 replace_env 调用replace_env "__VITE_OAUTH_URL__" "${VITE_OAUTH_URL}"
K8s添加 env 条目- name: VITE_OAUTH_URL value: "https://oauth.xxx"

顺序:源码 → Dockerfile → entrypoint.sh → K8s 配置,重新构建镜像后部署。


5. 模板文件

Dockerfile 模板

Dockerfile
# ===== 构建阶段 =====
# 替换 node:18 为项目实际 Node 版本
FROM node:18 as builder
WORKDIR /usr/src/app

COPY package.json ./
RUN pnpm install

COPY ./ ./

# 按需添加/删除 ENV 声明,值格式: __VITE_XXX__
ENV VITE_API_URL=__VITE_API_URL__
# ENV VITE_WS_URL=__VITE_WS_URL__
# ENV VITE_OAUTH_URL=__VITE_OAUTH_URL__

RUN pnpm build

# ===== 运行阶段 =====
# 替换 nginx:latest 为项目实际基础镜像
FROM nginx:latest
WORKDIR /usr/share/nginx/html/
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html/

COPY deploy/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

entrypoint.sh 模板

Bash
#!/bin/sh
set -e

HTML_DIR=/usr/share/nginx/html

# 通用替换函数(使用 /bin/sh,兼容 Alpine)
replace_env() {
  placeholder="$1"
  value="$2"
  find "$HTML_DIR" -name '*.js' -exec sed -i "s|${placeholder}|${value}|g" {} +
}

# 按需添加/删除 replace_env 调用
replace_env "__VITE_API_URL__" "${VITE_API_URL}"
# replace_env "__VITE_WS_URL__" "${VITE_WS_URL}"
# replace_env "__VITE_OAUTH_URL__" "${VITE_OAUTH_URL}"

# 如有 Nginx 模板需求,取消注释并配置模板路径
# envsubst < /etc/nginx/mysite.template > /etc/nginx/conf.d/default.conf

exec nginx -g 'daemon off;'

6. 注意事项

占位符命名规范

  • 格式:__VITE_XXX__(双下划线包裹,全大写)
  • 避免使用单下划线或无前缀名称(可能误替换 JS 中的普通字符串)
  • 占位符需与 Dockerfile ENV 值、entrypoint.sh 调用三处保持一致

sed 分隔符

使用 | 而非默认 /

Bash
# 错误:URL 中的 / 与分隔符冲突
sed -i "s/__VAR__/https://api.example.com/g" file.js

# 正确:使用 | 作为分隔符
sed -i "s|__VAR__|https://api.example.com|g" file.js

空值处理

如果 K8s 未注入对应环境变量,${VITE_XXX} 为空字符串,替换后 JS 中值变为空:

JAVASCRIPT
const API_BASE_URL = ""

会导致请求失败。部署时务必确保所有声明的环境变量都有值。

可在 entrypoint.sh 中添加校验:

Bash
if [ -z "${VITE_API_URL}" ]; then
  echo "ERROR: VITE_API_URL is not set"
  exit 1
fi

镜像无 bash 环境——必须使用 sh 脚本

陷阱:很多生产镜像不提供 /bin/bashentrypoint.sh 若写成 #!/bin/bash,容器启动直接失败:

Plain Text
standard_init_linux.go:211: exec user process caused "no such file or directory"

这个报错极具误导性——文件明明存在,但内核找不到 shebang 指定的解释器。

常见无 bash 的镜像

镜像原因
nginx:*-alpineAlpine Linux 默认只含 ash(/bin/sh)
node:*-alpine同上
nginx:*-slimDebian Slim 精简版,不含 bash
distroless/staticGoogle distroless 镜像,无 shell
scratch空镜像,无任何 shell

三条规则

规则 1:shebang 统一用 #!/bin/sh

Bash
# 错误:alpine/slim/distroless 中不存在 /bin/bash
#!/bin/bash

# 正确:POSIX sh 是所有镜像的最大公约数
#!/bin/sh

规则 2:避免 bash 专属语法

ash(Alpine 默认 shell)和 dash(Debian Slim 默认 shell)只支持 POSIX sh 子集。以下语法在 bash 中合法,但在 sh 中会报错:

bash 语法sh 替代方案
local var=value直接赋值 var=value
[[ ... ]]使用 [ ... ]
function name() {name() {
${var:-default} 数组操作用字符串逻辑替代
source file.sh. file.sh
Bash
# 错误:ash 不支持 local
replace_env() {
  local placeholder=$1    # 语法错误
  local value=$2
}

# 正确:直接赋值
replace_env() {
  placeholder="$1"
  value="$2"
}

规则 3:find -exec+ 而非 \;

alpine 的 find 支持 + 语法(批量执行),比 \;(逐个执行)更高效:

Bash
# 推荐:+ 批量执行 sed
find "$HTML_DIR" -name '*.js' -exec sed -i "s|${placeholder}|${value}|g" {} +

# 兼容:\; 逐个执行(也能工作,但更慢)
find "$HTML_DIR" -name '*.js' -exec sed -i "s|${placeholder}|${value}|g" {} \;

如果必须用 bash 怎么办

如果脚本确实依赖 bash 特性(如数组、正则匹配),在 Dockerfile 中显式安装:

Dockerfile
# Alpine
RUN apk add --no-cache bash

# Debian Slim
RUN apt-get update && apt-get install -y bash

然后 shebang 用 #!/bin/bash。但这会增加镜像体积,建议优先重构为 sh 兼容脚本。

修复后的完整 entrypoint.sh 模板

本文"模板文件"章节中的 entrypoint.sh 模板已使用 #!/bin/sh,可以直接复制使用。

误判 Nginx 启动日志为错误

entrypoint.sh 执行成功后,Nginx 会输出 [notice] 级别日志:

Plain Text
2026/04/16 10:46:45 [notice] 1#1: using the "epoll" event method
2026/04/16 10:46:45 [notice] 1#1: nginx/1.27.1
2026/04/16 10:46:45 [notice] 1#1: built by gcc 13.2.1 20240309
2026/04/16 10:46:45 [notice] 1#1: OS: Linux 5.4.0-216-generic

这些是 Nginx 正常启动日志,不是错误。如果容器随后退出,问题出在 entrypoint.sh 阶段(变量未设置 / shebang 错误),而非 Nginx。

CSS 文件中的变量

如果变量出现在 CSS 文件中(如背景图 URL),需额外替换 CSS:

Bash
# find 已覆盖所有 .js 文件,需追加 .css
find "$HTML_DIR" -name '*.css' -exec sed -i "s|__VITE_BG_URL__|${VITE_BG_URL}|g" {} +

特殊字符转义

如果环境变量值中包含 sed 特殊字符(如 &\|),需转义:

Bash
# & 在 sed 替换值中表示匹配内容本身,需转义为 \&

通常 URL 中不含这些字符,无需特殊处理。


7. 本地测试

构建镜像

Bash
# 在 deploy 同级目录执行
docker build -t my-frontend:latest -f deploy/Dockerfile .

运行容器

Bash
docker run \
  -e VITE_API_URL=https://api.example.com \
  -e VITE_WS_URL=wss://ws.example.com \
  -p 8080:80 \
  my-frontend:latest

验证替换结果

Bash
# 进入容器
docker exec -it <container_id> sh

# 检查 JS 文件中占位符是否被替换
grep -r "__VITE_API_URL__" /usr/share/nginx/html/
# 无输出 = 替换成功
# 有输出 = 未替换(检查环境变量是否正确注入)

# 查看实际值
grep -r "api.example.com" /usr/share/nginx/html/

清理

Bash
docker stop <container_id>
docker rm <container_id>

最后更新于: 2026-04-17