前端项目的部署方式这几年变化很大,从最早的 FTP 上传静态文件,到后来的 CI/CD 流水线,再到现在的容器化部署,每一步演进都在解决同一个问题:如何让应用在任何一台服务器上以完全一致的方式运行。Vue 项目作为典型的单页应用,最终产物是一堆静态文件,看起来似乎不需要容器化,但当你需要保证构建环境统一、配置参数可注入、回滚快速便捷时,Docker 的价值就体现出来了。本文以一个 Vue 3 项目配合 Vite 构建为例,完整走一遍容器化的全流程。

一、为什么 Vue 项目值得放进 Docker 容器
先说结论:Vue 项目虽然产物只是静态文件,但容器化带来的收益远不止打包文件这么简单。第一个收益是构建环境的绝对一致。本地开发用 Node 18,CI 服务器上是 Node 16,这种版本差异导致的构建失败几乎是每个团队都踩过的坑。把构建过程固定在 Docker 镜像内,指定好 Node 版本和依赖版本,无论谁在什么机器上构建,结果都完全一样。
第二个收益是配置的动态注入能力。传统做法是把 API 地址写死在代码里,或者打包时通过环境变量硬编码进去,一旦需要切换后端地址就得重新构建。容器化之后,可以在容器启动时通过 Nginx 模板或者运行时脚本动态生成配置文件,一个镜像跑遍开发、测试、生产三套环境。
第三个收益是运维层面的。镜像本身就是一个版本化的制品,配合镜像仓库的标签管理,回滚只需要把上一个版本的镜像重新拉起来即可,不需要在服务器上翻找历史构建产物。这一点在没有专门运维人员的小团队里尤其实用。
二、编写基础的 Dockerfile
最直接的思路是分两个阶段:第一阶段安装依赖并执行构建,第二阶段把构建产物拷进 Nginx 镜像。这就是所谓的多阶段构建,能显著减小最终镜像体积。先看一个基础版本:
# 构建阶段 FROM node:18-alpine AS build-stage WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # 运行阶段 FROM nginx:stable-alpine AS production-stage COPY --from=build-stage /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
这份 Dockerfile 有几个细节值得展开。第一,COPY package*.json ./ 单独复制依赖清单再执行 npm ci,是为了利用 Docker 的层缓存机制:只要 package.json 没变,下次构建就直接命中缓存,跳过最耗时的依赖安装步骤。npm ci 比 npm install 更严格,它严格按照 package-lock.json 安装,适合构建场景。
第二,npm ci 之前没有执行 npm audit fix 之类的命令是有意为之,构建过程应该尽量保持确定性,任何可能改动 lock 文件的操作都应该避免。第三,选择 alpine 版本的 Node 和 Nginx 镜像,是为了控制体积,node:18-alpine 只有不到 130MB,而完整版 node:18 超过 1GB。
执行构建和运行的命令如下:
# 构建镜像,注意结尾的句点表示构建上下文为当前目录 docker build -t vue-app:1.0.0 . # 启动容器并做端口映射 docker run -d -p 8080:80 --name vue-web vue-app:1.0.0
打开浏览器访问服务器的 8080 端口,就能看到页面了。不过先别高兴太早,这个版本在刷新非根路径时会返回 404,这正是下面要解决的问题。
三、处理 history 路由模式和 Nginx 配置
Vue Router 使用 history 模式时,URL 是标准的路径形式,比如 /user/list。用户在该页面按 F5 刷新,浏览器会向 Nginx 请求这个路径,但服务器上根本不存在对应的文件,Nginx 自然返回 404。解决思路是把所有找不到的路径统一回退到 index.html,交给前端路由接管。
做法是自定义一份 Nginx 配置文件,在构建时拷贝进镜像。在项目根目录创建 nginx.conf:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 关键配置:找不到文件时回退到首页
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源长缓存,文件名带 hash 可以放心设置
location ~* \.(js|css|png|jpg|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# 开启 gzip 压缩
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
}
然后在 Dockerfile 的运行阶段加一行拷贝指令:
FROM nginx:stable-alpine AS production-stage COPY --from=build-stage /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
try_files $uri $uri/ /index.html 的含义是:先按 URI 找文件,再找目录下的 index,都找不到就返回 index.html 的内容,同时 HTTP 状态码仍是 200。注意区分它和直接 rewrite 的差别——rewrite 会产生 301 或 302 跳转,用户地址栏会变成根路径,体验明显更差。另外静态资源缓存那一段要留意:Vite 构建产物的文件名自带内容 hash,内容变了文件名就变,所以设置一年过期都安全;但 index.html 千万不能强缓存,否则发版后用户拿到的还是旧版本的资源引用。
四、环境变量注入与 API 代理
前端容器化最容易被问到的场景:测试环境和生产环境的后端地址不同,难道要构建两次镜像吗?答案是不需要。思路是把后端地址做成运行时可变的配置,常见做法是利用 Nginx 的环境变量模板机制。
先在构建阶段生成一份配置模板文件 config.js.template:
// 该文件会在容器启动时被 envsubst 处理,替换为真实值
window.APP_CONFIG = {
baseURL: '${API_BASE_URL}'
};
在 Vue 代码中通过 window.APP_CONFIG.baseURL 读取后端地址,而不是使用 import.meta.env。启动容器时传入环境变量:
docker run -d -p 8080:80 \ -e API_BASE_URL=https://api.ipipp.com \ -e NGINX_ENVSUBST_OUTPUT_DIR=/usr/share/nginx/html \ vue-app:1.0.0
nginx 官方镜像内置了 envsubst 逻辑,启动时会把 /etc/nginx/templates 目录下所有模板文件中的环境变量替换后输出。这里利用了它顺便生成前端的 config.js,把模板放到 templates 目录并让输出目录指向静态资源目录即可。这样同一个镜像,换一个环境变量就能指向不同的后端,真正做到一次构建多处运行。
如果后端接口存在跨域限制,还可以直接在 Nginx 里配置反向代理,把 /api 请求转发给后端服务,前端用相对路径请求,连跨域问题都一并省掉了。这种方案适合前后端部署在同一台机器或同一集群内的场景。
五、镜像优化与常见踩坑
做好前面几步后,镜像体积和构建效率还有优化空间。首先是 .dockerignore 文件,它是容器化版的 .gitignore,必不可少的条目包括 node_modules、.git、dist 目录。如果不忽略 node_modules,构建上下文会把本地几百 MB 的依赖全发给 Docker 守护进程,既慢又可能污染镜像内容。
node_modules dist .git .vscode npm-debug.log
其次是镜像分层与缓存策略。把变动频率低的操作放在 Dockerfile 前面,变动频繁的放在后面,这是基本原则。比如先装依赖再拷贝源码,就是为了让依赖层在代码改动时依然能命中缓存。经过多阶段构建后,最终镜像只包含 Nginx 和静态文件,通常能控制在 50MB 以内,拉取和启动都非常快。
常见踩坑点归纳几个:一是容器内 Nginx 必须以前台方式运行,也就是 daemon off;,否则容器启动后立即退出,这是初学者最常遇到的问题;二是端口映射方向别搞反,-p 8080:80 左边是宿主机端口右边是容器端口;三是 Dockerfile 放在项目根目录、构建命令在 Dockerfile 所在目录执行,否则 COPY . . 的相对路径会找不到文件;四是如果构建时 npm 报网络超时,可以在国内服务器上给 npm 配置镜像源,或者构建命令加 --network=host 参数解决 DNS 解析问题。
整套流程跑通之后,后续接入 CI/CD 只需要把 docker build 和 docker push 写进流水线脚本,服务器上执行 docker pull 加重启即可完成发布。Vue 项目的容器化并不复杂,关键在于理解多阶段构建的分工和 Nginx 托管静态文件的细节,把这两块吃透,任何前端项目的容器化改造都能照搬这套模式。