前端 Docker 实战:用 Docker 统一团队开发环境
去年 11 月,我们组来了第三个前端。他装完项目依赖,顺手提交了一个 yarn.lock,CI 直接红了 —— 他本地 Node 18 生成的 lockfile 跟 CI 的 Node 16 不兼容,2000 多行的 diff 根本没法 review。
那天下午我干了两件事:把 lockfile revert 掉,然后开始写项目的第一个 docker-compose.yml。
半年后再回头看,Docker 不是银弹,但当你在一个 5 人前端团队里同时维护 3 个 SPAs、1 个 H5、1 个 Flask 中间层的时候,它能解决的问题比你想象的多得多。这篇文章记录的是我们 KMS 项目从"肉身统一环境"到"容器统一环境"的完整踩坑过程,所有配置都是线上在跑的真实版本。
一、"我机器上能跑"是真的吗?
三年前我刚入职的时候,项目的 README 里有一段"环境要求":
- Node.js >= 16.0.0
- npm >= 8.0.0 或 yarn >= 1.22.0
- 本地需安装 MySQL 5.7+
看起来没什么问题,对吧?到了第二个同事入职,他装的是 Node 18 LTS,跑 yarn install 之后 lockfile 里多了 resolution 字段的变更,review 的人看了半天以为他改了依赖版本。第三个同事用的 M1 Mac,node-sass 直接装不上,折腾了一下午换成 sass(dart-sass)。
这还只是 Node 版本。更隐蔽的问题是:
- 系统依赖差异:Mac 上用
brew装的 MySQL 是 ARM 版,Linux CI 跑的是 x86,某个 ORM 的 native binding 在两边的行为不一致,测试环境数据库字段编码问题藏了两个月才被发现。 - 环境变量泄漏:
.env.local里写了数据库密码,某次演示时同事把屏幕投出来,大家看到了他本地的DB_PASSWORD=root123456。你可能会说他为什么不放.gitignore—— 放了,但是他用cp .env.example .env.local之后顺手改了值,提交代码的时候没注意到这个文件被 IDE 的全局搜索带了出来。 - "我本地能跑"幻觉:同一个分支,三个人的机器上有三种表现。一个人编译快,因为 Mac Studio M2 Ultra;一个人编译慢,因为 2019 年的 Intel MacBook Pro;还有一个人压根编译不过,因为他的 Node 版本少了
crypto的某个 polyfill。
我们做了一个简单的统计,在引入 Docker 之前的三个月:
| 问题类型 | 出现次数 | 平均排查时间 |
|---|---|---|
| Node 版本不一致导致 lockfile 冲突 | 7 | 25 分钟 |
| 系统依赖缺失/版本不匹配 | 4 | 45 分钟 |
| 环境变量配置错误 | 3 | 30 分钟 |
| “我本地能跑,你本地跑不了” | 几乎每天 | 无法统计 |
三个月加起来,花在环境问题上的时间超过 20 个工时。而这些时间完全可以用一套 Docker 配置消灭掉。
Docker 能解决的核心问题其实就一句:把"我的机器"变成"我们的机器"。Node 版本锁死在 node:20-alpine 镜像里,MySQL 在 compose 里一键拉起,环境变量通过 env_file 统一注入 —— 任何人执行 docker compose up 得到的是完全相同的运行时。
当然,有人会说:“前端开发用什么 Docker?Node 装一下不就完了。” 如果你的团队只有一个人,只维护一个项目,确实不需要。但当你需要同时启动前端、后端、数据库来做联调,当你需要新同事第一天就能跑起项目开始写代码,当你需要确保 CI 环境和本地环境用完全相同的 Node 版本和系统库时 —— Docker 的投入产出比是非常高的。我们团队从决定引入到全部迁移完成只用了三天,但之后省下的时间,远不止三天。
接下来我会把整个配置摊开来讲,从 compose 文件到热更新到 IDE 集成,每一步都带着踩坑的经验。
二、Docker Compose 拉起前端+后端+数据库
先看我们的项目结构。KMS 是一个知识管理平台,前端用 React 18 + Vite + TypeScript + Ant Design,后端用 Flask,数据库是 MySQL 8.0:
kms-workspace/
├── kms-frontend/ # React + Vite 前端
│ ├── Dockerfile
│ ├── package.json
│ └── vite.config.ts
├── kms-backend/ # Flask 后端
│ ├── Dockerfile
│ └── requirements.txt
├── docker-compose.yml
└── .env # 公共环境变量
docker-compose.yml
下面是实际在跑的配置文件,我去掉了敏感信息,保留了核心逻辑:
version: "3.8"
services:
# ==========================================
# MySQL 数据库
# ==========================================
mysql:
image: mysql:8.0
container_name: kms-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root123456}
MYSQL_DATABASE: ${DB_NAME:-kms_dev}
MYSQL_USER: ${DB_USER:-kms}
MYSQL_PASSWORD: ${DB_PASSWORD:-kms123456}
ports:
- "${DB_PORT:-3306}:3306"
volumes:
# 持久化数据库数据,容器删除后数据不丢失
- mysql_data:/var/lib/mysql
# 初始化 SQL 脚本,首次启动时自动执行
- ./kms-backend/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
# 只有当 MySQL 真正准备好接受连接时才算 healthy
start_period: 30s
# ==========================================
# Flask 后端
# ==========================================
backend:
build:
context: ./kms-backend
dockerfile: Dockerfile
container_name: kms-backend
restart: unless-stopped
ports:
- "${BACKEND_PORT:-5001}:5001"
volumes:
# 挂载源码,支持热更新
- ./kms-backend:/app
# 匿名卷保护虚拟环境,避免被 bind mount 覆盖
- /app/venv
environment:
- FLASK_ENV=development
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=${DB_USER:-kms}
- DB_PASSWORD=${DB_PASSWORD:-kms123456}
- DB_NAME=${DB_NAME:-kms_dev}
depends_on:
mysql:
condition: service_healthy
command: python app.py
# ==========================================
# React + Vite 前端
# ==========================================
frontend:
build:
context: ./kms-frontend
dockerfile: Dockerfile
container_name: kms-frontend
restart: unless-stopped
ports:
- "${FRONTEND_PORT:-5002}:5002"
volumes:
# 挂载源码
- ./kms-frontend:/app
# 关键:匿名卷保护 node_modules,防止被 bind mount 覆盖
- /app/node_modules
environment:
- VITE_API_BASE_URL=http://localhost:5001
- VITE_APP_TITLE=KMS知识管理平台
depends_on:
- backend
command: yarn dev --host 0.0.0.0 --port 5002
volumes:
mysql_data:
driver: local
这里有几个关键决策值得展开说。
为什么 depends_on 不够,必须加 healthcheck
很多人写 depends_on: - mysql 就以为万事大吉。实际上 depends_on 只确保容器启动了,不管 MySQL 是否准备好接受连接。容器启动了但 MySQL 还在初始化,Flask 连上去就会报 Connection refused。
condition: service_healthy 配合 MySQL 的 healthcheck,才能保证后端启动时数据库已经 ready。这个细节在开发环境不致命(反正你可以手动重启),但养成习惯后,写生产配置时就会少踩一个坑。
node_modules 的卷挂载策略
这一行是整篇配置里最容易踩坑的地方:
volumes:
- ./kms-frontend:/app # bind mount:宿主机源码映射到容器
- /app/node_modules # 匿名卷:保护容器内的 node_modules
为什么需要两行?Docker 的卷挂载顺序决定了:匿名卷 /app/node_modules 会覆盖 bind mount 里对应的 ./kms-frontend/node_modules 路径。如果只有第一行,容器内 /app/node_modules 映射到宿主机 ./kms-frontend/node_modules —— 但宿主机这个目录在构建阶段已经被 docker compose build 生成了,里面的 native binary(如 esbuild)是 Linux 架构的,如果在 Mac 上 rebuild 了镜像,可能跟宿主机已有的 Mac 版 node_modules 冲突。
更安全的做法是:构建阶段在镜像内装好依赖,运行时用匿名卷保护起来,不让宿主机的 node_modules 入侵。这个我们第五章会详细拆解。
开发阶段的 Dockerfile
前端的开发 Dockerfile,关键区别在于:不要多阶段构建。
# kms-frontend/Dockerfile(开发环境)
FROM node:20-alpine
# 安装系统依赖(如果项目用了 sharp、canvas 等需要 native 编译的库)
RUN apk add --no-cache python3 make g++
WORKDIR /app
# 先复制依赖描述文件,利用 Docker 层缓存
# 只要 package.json 和 yarn.lock 不变,这层就不会重新构建
COPY package.json yarn.lock ./
# 安装依赖
RUN yarn install --frozen-lockfile
# 源码通过 volume 挂载,不 COPY 到镜像里
# 这样修改代码不需要重新 build 镜像
EXPOSE 5002
CMD ["yarn", "dev", "--host", "0.0.0.0", "--port", "5002"]
值得注意的是 CMD 而不是 RUN。很多人习惯把启动命令写死在 Dockerfile 里,但 docker-compose.yml 的 command 可以覆盖 CMD,这样同一个 Dockerfile 可以在 compose 里用 command 灵活调整参数,不影响镜像本身。
生产环境的 Dockerfile 完全不同:需要多阶段构建,用 nginx 作为 base image,COPY 构建产物而不是挂载卷。但开发环境的核心诉求是"改代码立刻生效",所以源码通过 volume 挂载进来、依赖装在镜像里、启动命令外挂在 compose 里。这套配置在生产环境不适用,但在开发阶段是最优解。
后端的 Dockerfile 思路一样,这里不展开了:
# kms-backend/Dockerfile(开发环境)
FROM python:3.11-slim
WORKDIR /app
# 系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc default-libmysqlclient-dev && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install -r requirements.txt
EXPOSE 5001
CMD ["python", "app.py"]
到这里,一个新同事只需要:
git clone xxx
cd kms-workspace
cp .env.example .env
docker compose up -d
三分钟后,前端在 localhost:5002,后端在 localhost:5001,数据库 ready。不需要装 Node、Python、MySQL,不需要纠结版本。接下来我们看热更新怎么配。
三、热更新、卷挂载与环境变量
Docker 开发环境最大的体验差距是:改一行代码,要不要重新 build 镜像?答案是 不要。如果每次改 CSS 都要跑 docker compose build && docker compose up,那这玩意儿还不如不用。
Vite HMR 在 Docker 里的配置
Vite 的热更新依赖 WebSocket。在 Docker 里跑 Vite dev server 需要两件事:
1. --host 0.0.0.0
不加这个参数,Vite 默认只监听 localhost,容器的 localhost 不等于宿主机的 localhost,浏览器根本连不上。
2. HMR WebSocket 端口
Vite 默认 HMR 走 dev server 的同端口(比如 5002),但如果你的反向代理或 Docker 网络做了特殊配置,可能需要单独指定:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
host: true, // 等同于 --host 0.0.0.0
port: 5002,
// Docker 环境下 HMR 配置
watch: {
usePolling: true, // 某些文件系统(如 macOS + Docker Desktop)
interval: 1000, // 轮询间隔,默认值即可
},
// 如果需要单独配置 HMR
hmr: {
host: 'localhost',
port: 5002,
},
},
});
usePolling: true 是一个容易忽略但至关重要的选项。Docker Desktop on macOS 的卷挂载基于 osxfs,文件变更事件的传递有时候会延迟或丢失。启用轮询后,Vite 会主动检查文件变化而不是被动等待 inotify 事件。代价是 CPU 占用会高一点(约 2-5%),但换来的稳定性绝对值得。
我们最初没开这个选项,经常出现改了代码页面不更新、需要手动刷新的情况。三个前端每人每天手动刷新几十次,开了轮询之后这个问题完全消失。
卷挂载的坑:node_modules 覆盖问题(详细拆解)
这是 Docker 开发环境最常见也最隐秘的问题。让我们从头推演一遍。
你的 compose 文件里有这两行:
volumes:
- ./kms-frontend:/app
- /app/node_modules
完整的执行顺序是这样的:
docker compose build→ 在镜像内的/app下执行yarn install,生成/app/node_modules(Linux 二进制)docker compose up→ bind mount 把宿主机./kms-frontend映射到容器/app- 此时容器内
/app/node_modules被 bind mount 覆盖,变成宿主机./kms-frontend/node_modules - 但!匿名卷
/app/node_modules随后又把容器内这个路径覆盖回来
第四步是关键。如果没有匿名卷声明,容器运行时使用的 node_modules 就是宿主机上的那份。宿主机上可能:
- 根本没有
node_modules(没有本地安装过依赖) - 有 Mac 版本的
node_modules(native binary 跟 Linux 不兼容) - 有旧版本的
node_modules(跟 lockfile 不一致)
无论哪种情况,vite 都找不到正确的依赖,表现就是容器启动后报 Cannot find package 'vite' 或 esbuild 段错误。
解决方案的核心逻辑:依赖写在镜像里(build 阶段),运行时不碰它(匿名卷保护)。源码挂在外面(volume mount),修改立即生效。两者互不干扰。
我们团队还尝试过另一种方案:把 node_modules 也挂载出来,在宿主机上执行 yarn install。优点是 dev server 启动更快(不需要进容器装依赖),缺点是你又回到了"宿主机环境依赖"的老路上 —— M1 Mac 和 Intel Mac 的 native binary 又不一样了。所以我们最终选择了"依赖全在镜像里"的方案。
.env 管理策略
KMS 项目有三套环境变量:
kms-frontend/
├── .env # 公共默认值,提交到 Git
├── .env.development # Docker 开发环境专用
├── .env.local # 本地敏感信息,不提交 Git
└── .env.production # 生产环境
在 Docker compose 里,我们通过 env_file 注入:
frontend:
env_file:
- ./kms-frontend/.env.development
environment:
- VITE_API_BASE_URL=http://localhost:5001
注意 env_file 和 environment 的区别:env_file 指向一个文件,environment 是内联的 key-value。前者适合大量公共变量,后者适合少量需要覆盖或动态计算的变量。两者可以同时使用,environment 的优先级更高。
Vite 对 VITE_ 前缀的变量有特殊处理:只有 VITE_ 开头的变量会暴露给客户端代码(import.meta.env.VITE_XXX)。这是安全机制,防止你不小心把数据库密码暴露到前端 bundle 里。所以前端的 API 地址必须叫 VITE_API_BASE_URL,不能叫 API_BASE_URL。
有一个隐秘的点:Docker compose 里的 environment 是在容器启动时注入的,而 Vite 的 import.meta.env 是在构建时确定的。开发环境下 Vite 以 dev server 模式运行,每次请求时会重新读取环境变量,所以 runtime 注入没有问题。但如果你在 Docker 里跑 yarn build,环境变量必须 build time 注入,否则客户端代码读到的是 undefined。这个坑第五章会单独讲。
前后端联调:CORS 与 network
后端用 Flask,默认只响应同源请求。在 docker-compose 里,前端和后端虽然在同一个 network 上,但浏览器访问前端是 localhost:5002,前端 JS 发请求到 localhost:5001,这已经是跨域了。
Flask 开发环境的 CORS 配置:
# kms-backend/app.py
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app, origins=["http://localhost:5002"], supports_credentials=True)
docker-compose 内部有三个东西互联:
frontend容器(端口映射 5002:5002)backend容器(端口映射 5001:5001)mysql容器(端口映射 3306:3306)
Compose 会自动创建一个 {project}_default 的 bridge network,三个容器可以通过 service name 互相通信。所以后端连接数据库用的是 DB_HOST=mysql 而不是 localhost。这一点新手经常搞混 —— 在容器内部,localhost 指向容器自己,mysql 才是 MySQL 容器的地址。
整个请求链路如下:

每一段都在 Docker 网络层做了隔离,不会污染宿主机环境。
四、Dev Container:把 IDE 也标准化
环境统一了,代码跑起来了,但还有一个细节:插件和格式化规则。
我们组的 ESLint 规则是统一的(.eslintrc.cjs),Prettier 规则也是统一的(.prettierrc),但真正在编辑器里生效,依赖每个人自己装插件、调设置。有人用的 VS Code,有人用的 Cursor,有人用的 WebStorm,有人 Prettier 设成了 format on save,有人设成了 format on paste。
一个典型的场景:同事 A 用 Cursor,没有装 ESLint 插件,他写的代码在本地看着没问题,推到 GitLab 之后 CI eslint 报 15 个 error。因为他的编辑器根本没有提示。
Dev Container 可以彻底解决这个问题:把 VS Code 插件列表写进配置,任何人打开项目,插件自动安装、格式化规则自动生效。
.devcontainer.json 配置
{
"name": "KMS Development",
"dockerComposeFile": "../docker-compose.yml",
"service": "frontend",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"dsznajder.es7-react-js-snippets",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-typescript-next"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"files.eol": "\n"
}
}
},
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"forwardPorts": [5002, 5001],
"postCreateCommand": "yarn install --frozen-lockfile"
}
这份配置做了几件事:
dockerComposeFile指向已有的 compose 文件,Dev Container 不会新建容器,而是 attach 到 compose 启动的frontend服务里。extensions统一了 VS Code 插件列表,同事打开的瞬间自动安装。settings强制开启 formatOnSave、ESLint auto fix,TypeScript 版本锁定在项目的node_modules里。这避免了 VS Code 自带 TypeScript 版本和项目版本不一致导致的类型检查差异。forwardPorts自动转发端口,容器内的 5002 和 5001 直接从宿主机浏览器访问。postCreateCommand确保容器启动后的依赖完整。
真正的"第一天"体验
有了这套配置,一个新同事加入团队的流程变成了:
- 装 Docker Desktop(一次性)
git clone <repo-url>- VS Code 打开项目文件夹 → 右下角弹出 “Reopen in Container” → 点一下
- 等几分钟(拉镜像、装依赖)
- 浏览器打开
http://localhost:5002→ 开始写代码
不需要装 Node,不需要装 MySQL,不需要配置 ESLint,不需要手动设置 Prettier。环境问题从"半个工作日的噩梦"变成了"Docker 拉镜像时去泡杯咖啡"的时间。
你可能会说:“那我用 Cursor / WebStorm 怎么办?” Dev Container 目前在 VS Code 系(VS Code、Cursor、GitHub Codespaces)支持最好。如果你团队混用多种编辑器,至少可以做到 compose + Dockerfile 统一运行时,插件部分各管各的。我们组的实际操作是:在 compose 配置上强制统一(否则联调不通),Dev Container 作为"推荐配置"而非强制。
我觉得这是一个务实的平衡。工具统一要追求的是消除能导致 bug 的差异(Node 版本、依赖版本、运行时行为),而不是无差别地消灭一切差异(谁用什么编辑器、代码主题颜色)。后者属于个人偏好,不值得花精力去统一。
五、开发环境的 5 个踩坑清单
这一章是我们在实际使用中踩过的 5 个坑,每个都按"现象 → 根因 → 解决"的结构展开。
坑 1:node_modules 覆盖导致 Vite 找不到依赖
现象:docker compose up 之后,前端容器日志里报:
Error: Cannot find module 'vite'
明明 Dockerfile 里写了 yarn install,构建时也确认安装成功了。
根因:bind mount ./kms-frontend:/app 把宿主机的空目录(没有 node_modules)覆盖了容器内的 /app/node_modules。
解决:在 compose 的 volumes 里加一行匿名卷:
volumes:
- ./kms-frontend:/app
- /app/node_modules # 这一行保护容器内的 node_modules
匿名卷的优先级高于 bind mount,容器启动后 /app/node_modules 会被恢复为镜像里的版本。如果还是不行,删掉匿名卷重新创建:
docker compose down -v # -v 会删除匿名卷
docker compose up -d # 重新创建
坑 2:端口冲突排查
现象:docker compose up 报错 port is already allocated。
根因:三种可能 ——
- 宿主机已经有进程占用了 5002 或 5001(比如之前手动跑的
yarn dev) - 之前的 Docker 容器没有正常退出,端口还在占用
- macOS 的 AirPlay Receiver 默认占用 5000 端口(我们遇到了)
解决:
# 1. 检查宿主机端口占用
lsof -i :5002
# 如果是 AirPlay:系统设置 → 通用 → 隔空播放接收器 → 关闭
# 2. 检查 Docker 容器
docker ps -a | grep kms
docker rm -f <container-id>
# 3. 如果都不行,换端口
# docker-compose.yml 中改为:
# ports:
# - "${FRONTEND_PORT:-5003}:5002"
建议把 5000-5002 这段端口留给 Docker 用,本地不要再手动跑 dev server。我们在 .husky/pre-commit 里加了一个检查:
#!/bin/sh
# .husky/pre-commit
if lsof -i :5002 > /dev/null 2>&1; then
echo "❌ 端口 5002 被占用,请检查是否有本地 dev server 在运行"
echo " 提示:使用 docker compose up 启动开发环境"
exit 1
fi
坑 3:构建缓存不生效
现象:每次 docker compose build 都要重新下载依赖,耗时 3-5 分钟。
根因:Dockerfile 中 COPY . . 放在依赖安装之前,任何文件改动都会导致缓存失效。
错误写法:
WORKDIR /app
COPY . . # 先复制所有文件
RUN yarn install # 后装依赖 —— 只要任何文件变了,缓存全部失效
正确写法:
WORKDIR /app
COPY package.json yarn.lock ./ # 先复制锁文件
RUN yarn install --frozen-lockfile # 装依赖 —— 这层会缓存
COPY . . # 最后复制源码 —— 只有这层会失效
利用 Docker 的分层缓存机制:每一层都是独立的缓存单元。package.json 和 yarn.lock 不常变,对应的 yarn install 层就几乎不会被重新构建。真正频繁变化的是源码 COPY . .,但这一层极快,不需要重新下载依赖。
这个优化让我们从每次 build 5 分钟降到了 10 秒(依赖层命中缓存)。开发阶段频繁 rebuild 的时候,体验提升非常明显。
坑 4:Docker Desktop 默认内存不足
现象:docker compose up 之后前端容器频繁被 OOM Kill,日志里出现 exit code 137(典型的 OOM kill 信号)。
或者 Vite 启动时报 JavaScript heap out of memory。
根因:Docker Desktop on macOS/Windows 默认分配的内存是 2GB。我们同时跑 MySQL(约 500MB)+ Flask(约 200MB)+ Vite dev server + HMR(约 800MB-1.2GB),高峰期轻松超过 2GB。
解决:
- Docker Desktop → Settings → Resources → Memory → 调到 6GB 或更高(16GB 以上的机器建议 8GB)
- 如果内存紧张,可以在 compose 里限制单个容器的内存:
frontend:
deploy:
resources:
limits:
memory: 2g
reservations:
memory: 512m
- Vite 开发阶段可以手动设置 Node 内存上限:
frontend:
command: node --max-old-space-size=2048 node_modules/.bin/vite --host 0.0.0.0 --port 5002
我们团队 5 个人,2 台 16GB MacBook Pro、2 台 32GB MacBook Pro、1 台 64GB Mac Studio。16GB 的同事开了 Docker + VS Code + Chrome 十几个 tab + Figma 之后确实紧张。最后定了一个最低配置标准:开发机 16GB 以下的不建议用全套 Docker 开发环境,可以用 compose 只拉数据库和后端,前端在本地跑。
坑 5:环境变量在 build time vs runtime 的差异
现象:在 Docker compose 的 environment 里设置了 VITE_API_BASE_URL=http://localhost:5001,但跑 docker compose exec frontend yarn build 之后,生产包里 import.meta.env.VITE_API_BASE_URL 是 undefined。
根因:Vite 的 import.meta.env.VITE_XXX 是在构建时(yarn build)被静态替换为具体值的,不是在运行时读取的。Compose 里的 environment 是运行时注入,构建时不存在。
解决:区分两种场景:
开发环境(yarn dev):Vite dev server 在运行时动态读取环境变量,environment 注入完全没问题。这也是我们日常开发的方式。
构建环境(yarn build):必须在构建时设置环境变量。两种做法:
- 在 Dockerfile 里用
ARG+ENV:
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
构建时传入:
docker build --build-arg VITE_API_BASE_URL=https://api.kms.example.com .
- 在 compose 里创建一个 build 专用的 service,用
args传递:
frontend-build:
build:
context: ./kms-frontend
args:
VITE_API_BASE_URL: ${VITE_API_BASE_URL}
我们实际采用的方案是:日常开发用 environment(runtime 注入),CI/CD 构建用 build args(build time 注入)。两者不混用,避免在这个细节上浪费时间排查。
更多推荐
所有评论(0)