
前端项目部署这件事说难不难说简单也真没那么简单。你本地跑得好好的 React Vite 项目传到服务器上可能就是另一副面孔Node 版本不对、npm install 装到一半崩了、跑起来首页倒是能开一刷新子路由就 404。这些问题的根源只有一个——环境不一致。Docker 解决的就是这个问题它把你的应用连同运行环境一起打包成一个标准镜像到哪台机器上都是同一个跑法不再有“在我电脑上能跑”这种说法。这篇文章从零开始不预设你已经用过 Docker只要你会基础的 React 开发和 Linux 命令就能跟着一步步把项目容器化走完从安装 Docker、写 Dockerfile、构建镜像、启动容器到配置 Nginx 的完整链路。下面我会把每一步的“为什么”也讲清楚不只是给你一段能用的配置而是让你理解这套方案背后的取舍。文章最后整理了我在实际部署中踩过的坑和排查思路看完能帮你省下至少一个下午的 debug 时间。1. 把前端项目装进 Docker到底解决了什么问题1.1 从一次“在我电脑上能跑”说起几年前我在团队里接手一个已经迭代了两年的后台管理系统代码从前端到部署脚本全是前任留下的。本地跑没问题但每次发布上线都要经历一轮折腾先确认服务器上的 Node 版本再跑 npm install运气不好还要处理 Python 环境的 node-sass 编译问题整套下来小半天就没了。后来我养成了一个习惯——项目交接也好、团队协作也好第一件事就是把运行环境固定下来。Docker 不是银弹但它确实是目前处理“环境一致性”最成熟的方案镜像把代码、运行时、依赖、配置文件全部固化任何一台装有 Docker 的机器都能还原出完全相同的运行环境。对前端项目来说容器化带来的收益主要集中在三个方面构建环境可复现开发用 Node 18生产也用 Node 18依赖锁定版本CI 里构建的产物和本地构建的产物理论上完全一致。部署过程标准化服务器上不再需要手动安装 Node、Nginx只需要docker run一个命令就能把服务拉起来。回滚变成切镜像旧版本镜像还在出问题直接切回去不用重新拉代码重新构建。1.2 方案选型为什么是 nginx 多阶段构建前端容器化有一个经典的组合Node 镜像负责构建Nginx 镜像负责运行中间用多阶段构建把两者串联起来。这套方案能成为主流不是偶然。先看 Nginx。前端构建产物是纯静态文件理论上任何静态文件服务器都能跑Python 的http.server都能胜任。但生产环境要考虑的远不止“能访问”SPA 路由回退要配、静态资源缓存要配、Gzip 压缩要配、HTTPS 证书要挂。Nginx 在这些方面有几十年的积累配置文件写清楚之后非常稳定可靠这是 Node 写的serve这类轻量方案比不了的。再看多阶段构建。如果你只写一个FROM node:18然后把整个 node_modules 打进镜像再跑npm run start镜像会有多大Node 18 完整镜像大概 1GB 左右还不算 node_modules 的体积。而多阶段构建的思路是第一阶段用 Node 镜像构建出静态资源第二阶段把静态资源拷贝进一个干净的 Nginx 镜像里。最终镜像里只有 Nginx 和 dist 文件夹体积一下压缩到几十 MB攻击面也更小——生产容器里根本没有 Node 进程可打。提示多阶段构建不是前端专用技巧任何“需要编译期依赖但不希望带到运行期”的场景都适用。比如 Go 项目用 golang 镜像交叉编译再用 scratch 镜像运行同一个思路。1.3 场景拆分开发环境和生产部署要分开对待刚开始学 Docker 的人很容易犯一个错误想用 Docker 把本地开发也包进去。我试过在 Docker 里跑 Vite dev server体验非常割裂——热更新慢、断点调试不好使、文件监听偶尔失灵。我的建议是明确区分两条链路本地开发继续用本机的 Node npm run dev。Vite 的热更新和浏览器调试体验是容器暂时给不了的。生产部署用 Docker 走构建 Nginx 静态托管的方案。具体的做法是本地开发完全不用 Docker代码推到仓库后由 CI 或手动执行 Docker 构建构建出的镜像拿到服务器上跑。这样兼顾了开发体验和生产一致性也是目前前端 Docker 化的标准姿势。2. 环境准备与镜像的核心构件2.1 Docker Desktop 安装与那两个 Windows 专属坑Docker 本身是 Linux 上的技术Windows 和 macOS 想用就得靠一层虚拟机。macOS 上 Docker Desktop 基本是双击安装、一气呵成而 Windows 上就麻烦一些最常见的就是启动时报Virtualization support not detected。这个报错的含义是 Docker Desktop 依赖的虚拟化功能没开启通常有三个原因BIOS 里的 CPU 虚拟化被关掉了。重启进 BIOS找 Intel VT-x 或 AMD-V 的开关设置成 Enabled。Windows 功能里没启用 WSL2 或 Hyper-V。在“启用或关闭 Windows 功能”里勾选“适用于 Linux 的 Windows 子系统”如果是老版本系统还得手动开 Hyper-V。Docker Desktop 设置的 backend 不对。新版多数默认用 WSL2 backend如果之前装过旧版 Docker Toolbox可能还残留着 Hyper-V backend 的冲突。装好之后验证一下环境和版本docker --version docker compose version docker infodocker info能输出大量运行时信息如果正常显示Server Version就说明 Docker 引擎已经跑起来了。2.2 Dockerfile 逐行拆解多阶段构建到底多写了什么写 Dockerfile 是整套流程的核心环节我直接给一份生产可用版本再逐行解释# 第一阶段构建前端静态资源 FROM node:18-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --registryhttps://registry.npmmirror.com COPY . . ARG VITE_APP_API_URL ENV VITE_APP_API_URL$VITE_APP_API_URL RUN npm run build # 第二阶段Nginx 托管静态资源 FROM nginx:1.25-alpine COPY --frombuild /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]逐行来看几个关键点FROM node:18-alpine AS build以 18 版本为基础镜像-alpine是精简版体积更小。给它起了个名字叫build后续阶段可以引用。COPY package.json package-lock.json ./先把依赖清单拷进容器。这里单独一步是有讲究的——Docker 构建有缓存机制只要这一层涉及的文件没变后续步骤就能直接复用缓存。如果先把全部代码拷进去再执行npm install那么每次代码变动都会导致依赖重新安装一遍构建慢到怀疑人生。RUN npm cinpm ci会严格按 lock 文件安装依赖不会像npm install那样自作主张升级版本。配合package-lock.json能保证构建环境依赖完全一致。RUN npm run build执行 Vite 构建生成 dist 目录。FROM nginx:1.25-alpine第二阶段从 Nginx 镜像开始--frombuild表示从第一阶段构建的临时镜像里拷贝文件。到这里Node 和 node_modules 全被丢掉最终镜像里只有 Nginx 和编译好的静态文件。npm ci这里有个细节它要求package.json和package-lock.json必须同步否则会直接报错。这其实是好事逼着你提交 lock 文件、保证依赖可复现。2.3 别忘了 .dockerignore 和 npm 镜像源很多新手写 Dockerfile 只关注内容忘了.dockerignore文件结果构建时把本地的 node_modules 也打进构建上下文了。前面COPY . .这行的意思是把当前目录所有文件都拷进容器如果没有忽略规则那速度慢不说还可能出现 Windows 下的原生依赖被拷进 Linux 容器导致构建失败。.dockerignore和.gitignore的作用类似至少要包含这些node_modules dist .git .gitignore .editorconfig Dockerfile .dockerignore .env .env.local *.log .DS_Store至于 npm 镜像源国内网络环境下一句npm ci可能就是几分钟的等待。我习惯在 Dockerfile 里直接用--registry参数指定镜像源这样不依赖宿主机 npm config。如果你的用户不在乎这点网络差异去掉也完全可以。2.4 nginx.confSPA 路由不回退 404 的关键React 项目默认是单页应用前端路由由 JS 控制浏览器拿到的是整个应用框架再按路径渲染组件。问题在于用户直接访问www.example.com/user/123时浏览器会先向服务器发起对/user/123这个路径的 HTTP 请求服务器上根本没有这个文件Nginx 默认会返回 404。所以 Nginx 配置必须做一件事——所有路径都回退到index.htmlserver { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # Gzip 压缩减少传输体积 gzip on; gzip_types text/plain text/css application/javascript application/json image/svgxml; gzip_min_length 1024; # 所有请求回退到 index.html location / { try_files $uri $uri/ /index.html; } # 静态资源带指纹强缓存 7 天 location /assets/ { expires 7d; add_header Cache-Control public, no-transform; } }try_files $uri $uri/ /index.html是 SPA 部署的灵魂先找对应文件再找对应目录都找不到就返回 index.html把路由交给前端处理。/assets/单独配置是因为 Vite 构建产物里的 JS、CSS 都带哈希指纹——文件名变了旧缓存自动失效没有指纹的入口文件index.html反而不能强缓存否则更新后用户还在看旧页面。3. 从零开始完整实操构建、运行、编排3.1 第一步初始化一个 Vite React 项目先创建一个测试项目来走完整流程。Vite 是现在首选的 React 脚手架比 CRA 快得多配置文件也清晰npm create vitelatest react-docker-demo -- --template react cd react-docker-demo npm install项目结构里最核心的几个文件src/React 源码vite.config.js构建配置package.json依赖和脚本index.html入口页面先本地跑一遍确认项目本身没问题npm run dev浏览器打开http://localhost:5173能看到 Vite 默认的 React 欢迎页就说明项目初始化成功。3.2 第二步构建前端产物并本地验证在写 Docker 之前先用 Vite 原生的构建命令验证产物正常npm run build构建完成后会在项目根目录生成dist/文件夹里面是压缩优化过的静态资源。你可以参考 Searx 这篇文章里的方法在本地静态跑一下验证效果。这里要注意一个细节Vite 默认的base是根路径/如果你的前端部署在域名根路径下什么都不用改。如果要部署到类似http://example.com/admin/的子路径需要在vite.config.js里设base: /admin/同时 Nginx 的 location 也要对应调整。这个坑很容易被忽略等你部署完发现图片全挂了、路由全 404 才反应过来。3.3 第三步编写 Dockerfile 和 Nginx 配置在项目根目录创建Dockerfile内容就用我上面给的那份。然后再创建nginx.conf内容也用上面那份。如果你需要配置后端 API 转发比如把/api开头的请求反代到后端服务需要在 nginx.conf 里加上location /api/ { proxy_pass http://backend:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }这个配置等于把容器内的/api请求转发到backend主机的 8080 端口。这里的backend是 docker-compose 里的服务名在 Docker 内网环境里能直接解析。3.4 第四步构建镜像与启动容器项目根目录执行构建docker build -t react-docker-demo:1.0 .-t给镜像打标签格式是名称:标签1.0 这样自己管理版本号即可。首次构建会花几分钟下载基础镜像之后构建就快多了。构建完成后查看镜像列表docker images你会在列表里看到 Nginx 镜像和最终构建产物镜像最终镜像体积应该在 50MB 左右如果超过 200MB大概率是中间阶段的临时层被误打进了镜像。启动容器docker run -d -p 8080:80 --name react-demo-container react-docker-demo:1.0参数拆解-d后台运行-p 8080:80把容器的 80 端口映射到宿主的 8080 端口浏览器访问http://localhost:8080就能看到页面--name给容器命名方便后续管理运行后查看日志确认 Nginx 启动成功docker logs react-demo-container再进入容器里检查文件是否正确拷贝docker exec -it react-demo-container sh ls /usr/share/nginx/html能看到index.html和assets/目录就说明构建阶段和拷贝阶段都正常。3.5 第五步用 docker-compose 把前后端塞进一套编排真实项目里前端通常不是单独部署的后面多半还跟着一个后端接口服务。与其一个个docker run不如用 docker-compose 把服务编排在一起。在项目根目录创建docker-compose.ymlversion: 3.8 services: frontend: build: . ports: - 8080:80 depends_on: - backend backend: image: node:18-alpine working_dir: /app volumes: - ./backend:/app command: sh -c npm install npm run server environment: - PORT8080然后在backend/目录里放一个简单的 Node HTTP 服务。启动整个编排docker compose up -ddepends_on保证后端先启动再启动前端但要注意这个指令只决定启动顺序不负责健康检查。生产环境建议给后端加healthcheck等健康检查通过后才把流量切过去。用 compose 管理还有一个好处docker compose down一键停掉所有服务环境清理非常干净。3.6 进阶运行时注入环境变量的正确姿势所有前端项目都会遇到这个问题开发环境的 API 地址和生产的地址不一样但你又不想分别构建两套镜像。很多人的做法是构建时用--build-arg传参数这确实可行但有个弊端——环境变了就得重新构建镜像。更优雅的做法是把环境变量注入推迟到容器启动时。Nginx 本身有envsubst能力可以在容器启动时用环境变量替换模板占位符。思路是这样的把 nginx.conf 写成一个模板文件变量用$占位然后写一个 entrypoint 脚本在启动前做替换。也可以利用 Nginx 官方的镜像特性它自带/docker-entrypoint.d/机制支持模板文件替换。如果你不想搞得太复杂构建时用ARG ENV传参是最快路径但记住镜像一旦构建环境变量就焊死在里面了。换环境就要重新构建更适合 CI/CD 流程中分环境构建的场景。4. 高频踩坑记录与排查思路4.1 启动 Docker Desktop 直接报 Virtualization support not detected这是我们开头提到的问题。完整报错一般是Docker Desktop - Virtualization support not detected排查步骤按顺序来重启进 BIOS确认 Intel VT-x 或 AMD-V 是 Enabled。Windows 搜索“启用或关闭 Windows 功能”勾选“适用于 Linux 的 Windows 子系统”老系统还要勾选“Hyper-V”重启。打开 PowerShell 执行wsl --status确认 WSL2 正常。最后再启动 Docker Desktop。这个问题是 Windows 上装 Docker 的第一大拦路虎根因基本都是系统虚拟化相关组件没配齐。4.2 容器起来了但网页打不开或网络不通docker run成功端口也映射了但访问localhost:8080就是打不开。先确认端口映射是否生效docker ps看PORTS列如果显示0.0.0.0:8080-80/tcp说明映射正常。如果显示的是127.0.0.1:8080-80/tcp那说明只监听了本机回环地址外部访问不到。容器内部网络不通是另一个常见问题。先进入容器测试网络docker exec -it react-demo-container sh ping 8.8.8.8如果 ping 不通多半是宿主机 DNS 配置问题。在/etc/docker/daemon.json里添加{ dns: [8.8.8.8, 114.114.114.114] }重启 Docker 服务后生效。容器网络配置这块跟 Zabbix 这类系统监控工具部署时遇到的网络问题排查思路很类似——先通宿主机再通容器内逐层定位。4.3 端口占用8080 端口已经被系统服务占了在 Windows 上还会遇到一种特殊情况8080 端口明明没装什么软件但就是被占用。系统更新后 Hyper-V 会保留一批端口范围导致你无法监听。排查思路netstat -ano | findstr :8080如果看到 PID 叫System且 PID 是 4大概率就是 Hyper-V 的端口保留。可以用netsh interface ipv4 show excludedportrange protocoltcp查看保留端口范围。最简单的处理办法是换一个映射端口比如-p 8081:80。一定要用 8080 的话可以用netsh int ipv4 set dynamicport tcp start10000 num20000把这段端口排除出保留范围再重启系统不过没必要为了一个端口这么折腾换端口最省事。4.4 刷新页面就 404SPA 路由的回退问题这是几乎所有前端 Docker 化的人都会遇到的经典问题访问首页没问题点击跳转也没问题一按 F5 就 404。原因在上面讲 nginx.conf 时说过了——浏览器直接向服务器请求/user/123这个路径服务器没找到文件就返回 404。没有加try_files的 Nginx 配置就是这个结果。确认你的 nginx.conf 里是否有这样一行location / { try_files $uri $uri/ /index.html; }加上之后重新构建镜像、重启容器就解决了。这个坑太常见了导致我第一次遇到的时候完全不慌心里清楚就是配置缺失的问题。4.5 镜像体积失控与构建速度慢构建出来的镜像体积大通常有三个原因没有用多阶段构建把整个 node_modules 塞进了最终镜像。基础镜像 tag 用错用了完整的node:18而不是node:18-alpine。缺少 .dockerignore本地 node_modules 被拷进构建上下文又被打进镜像。构建速度慢则多半是缓存没命中。Dockerfile 里COPY package.json package-lock.json ./单独一步的目的就是利用 Docker 层缓存——只要这两份文件没变之后到RUN npm ci都不会重复执行。但有一个容易忽略的细节Docker 的层缓存非常“敏感”上面任何一层文件变了后面所有缓存都会失效。所以 Dockerfile 里应该把“变化频繁的代码复制”放到最靠后的位置把“变化不频繁的依赖清单”放在最前面。COPY . .这一行不要写在npm ci前面否则缓存就形同虚设了。这套流程我在实际项目里跑了很多遍从最开始手忙脚乱地在服务器上手动装 Node、装 Nginx、传代码到后来变成一条docker builddocker run搞定整个发布过程压缩到了几分钟。踩过的坑里印象最深的还是那一次线上发布后用户反馈刷新页面就白屏当时第一反应是代码有问题折腾了半天才发现是 Nginx 没有配try_files。技术方案本身不复杂复杂的是环境之间的隐性差异。最后再分享一个小技巧生产环境一定要用固定版本号的镜像标签比如react-demo:1.0.0不要用latest。不然哪天执行docker pull拉了一个意外的新版镜像线上发布没经过验证就变了到时候谁也说不清线上跑的是什么代码。镜像标签就是线上环境的版本记录跟 Git tag 一样值得认真对待。