去年我给一个 Vue 项目做交付,代码在本地一切正常,放到测试服务器上却白屏了。对方后端同事半开玩笑地问我:“你本地能跑有什么用?”那是我第一次认真研究 Docker。后来我把项目镜像化,本地跑通的镜像原样推到服务器,白屏、缺依赖、版本不一致这类问题基本绝迹。现在回头看,前端学 Docker 根本不用啃整本运维书,抓住“打包-搬运-运行”这条线,就能完成从开发到服务器部署的闭环。这篇文章就是按这个思路来的,照着做,你也行。
1. 先解决一个问题:前端为什么要折腾 Docker
1.1 从“在我电脑上是好的”说起
做前端的人大概率都经历过这样的场面:项目在本地 npm run dev 跑得好好的,发到服务器或者同事机器上,要么 node 版本不对,要么 npm 装依赖时报错,要么接口地址写死导致白屏。以前我们习惯用“环境问题”来解释,但这个解释本身就很要命——环境不一致是项目交付最大的隐性成本。
Docker 解决的不是“修好某一台机器”,而是把整个运行环境连同代码一起打包。前端项目虽然最后多半是静态文件,但构建过程依赖 node 版本、npm 包、系统依赖,部署时又依赖 nginx 配置、端口、路径规则。这些东西只要有一处不一致,线上就可能出幺蛾子。用 Docker 后,你在本地构建出来的镜像,和服务器上跑的是同一套东西,不存在“本地好的,线上坏了”的扯皮空间。
1.2 镜像、容器、仓库一次讲明白
很多初学者一上来就被这三个概念吓退了,其实用吃一顿饭来类比就很好懂:
- 镜像(Image)就是“菜谱+半成品食材”,它把你代码、依赖、环境配置、启动命令全封进去。镜像不是拿来直接吃的,先得有它。
- 容器(Container)就是按菜谱实际“炒出来的一盘菜”,它是镜像的实例。同一个镜像可以同时跑多个容器,互相之间独立,谁也不干扰谁。
- 仓库(Registry)就是“菜谱库”,比如 Docker Hub,你可以把镜像传上去,到服务器上再拉下来。也可以自己搭私有仓库,或者用云厂商的镜像托管服务。
对应到命令上,docker build 是根据 Dockerfile 生成镜像,docker run 是基于镜像启动容器,docker push 是把镜像传到仓库,docker pull 是从仓库拉取镜像。整个流程就是:本地做镜像 -> 推到仓库 -> 服务器拉下来跑起来。
1.3 前端能用 Docker 解决哪些实际问题
这里我得说一句公道话:不是每个前端都必须成为 Docker 专家,但它确实能帮你省下不少事。我实际用下来,主要解决了这几类问题:
- 本地联调环境太乱:项目要连 MySQL、Redis,甚至要跑几个 mock 服务,用 docker run 一条命令搞定,不需要在本机装一堆乱七八糟的服务。
- 构建环境不统一:Dockerfile 里直接指定 node:20-alpine,团队成员不用自己折腾 Node 版本,新同事 clone 下来 npm install 完就能跑。
- 部署交付看得到结果:本地 build 出来的镜像推到服务器,docker run 一启动就是线上效果,白屏、404、路由刷不出这类问题能提前暴露。
- 顺手帮后端和运维减负:后端的 Java 服务、中间件也能用同样的容器方式部署,整个项目的交付方式会变得非常统一。
2. 本地环境准备:安装 Docker 和第一堂课
2.1 Windows/macOS 用 Docker Desktop 的正确姿势
Window 上最常用的方式是安装 Docker Desktop,它自带图形界面,可以在系统托盘里管理容器和镜像。别嫌它重,对新手来说,图形化比纯命令行友好太多。
下载安装包时注意两点:一是我建议直接从 Docker 官网下载稳定版,不要整那些套壳的工具;二是 Windows 一定优先选 WSL2 后端,而不是老的 Hyper-V,性能更好,启动也更稳。安装过程中勾选 Enable WSL 2 Features 即可。
如果你的电脑 CPU 虚拟化没开启,或者电脑太老不支持 WSL2,启动 Docker Desktop 时大概率会碰到这条提示:
Docker Desktop failed to start because virtualisation support wasn't detected.
这种问题大部分情况不是 Docker 坏了,而是 BIOS/UEFI 里没开虚拟化。重启进 BIOS,找到 Intel VT-x 或 AMD-V 的选项,开启后保存退出。然后在 Windows 功能里勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”,执行wsl --update,再重新启动 Docker Desktop。
macOS 用户就简单多了,Intel 和 Apple Silicon 芯片都支持 Docker Desktop,直接安装,启动时可能要求授权你的 Mac 密码,给它就好。
2.2 Linux 服务器安装 Docker(含镜像加速配置)
服务器上不需要装 Docker Desktop,装 Docker Engine 就够了。如果你用的是 Ubuntu/Debian 这类系统,官方其实提供了一行安装脚本:
curl -fsSL https://get.docker.com | sh systemctl enable docker systemctl start docker执行完后,输入docker version能看到 client 和 server 信息,服务端没问题就说明装好了。CentOS/RHEL 系的安装步骤稍多,建议去官方文档按对应版本执行,没必要在这里背命令。
国内服务器拉公共镜像偶尔会慢,这个不是网络问题,是镜像源访问速度的原因。解决办法是给 Docker 配置镜像加速地址,编辑/etc/docker/daemon.json,没有这个文件就新建:
{ "registry-mirrors": ["https://你的加速地址"] }加速地址从哪里来?云厂商的控制台里一般都有,比如阿里云容器镜像服务里就能找到专属加速地址,填进去后执行systemctl restart docker生效。如果你的服务器本身就在国内,这一步也顺手但别跳过。
2.3 第一个容器:用 Nginx 跑一个最小页面
装好 Docker 后,不用急着写 Dockerfile,先跑一个最基础的镜像感受一下:
docker run -d --name web-demo -p 8080:80 nginx:1.27-alpine这条命令干了三件事:从 registry 拉取 nginx 镜像,用-d在后台启动一个叫 web-demo 的容器,再把宿主机的 8080 端口映射到容器里的 80 端口。执行完打开浏览器访问http://localhost:8080,你会看到 nginx 默认的欢迎页。
这个页面就是 nginx 容器内80端口返回的,和宿主机的系统一点关系都没有。你可以试试:
docker ps docker exec -it web-demo shdocker ps看正在运行的容器,docker exec -it web-demo sh直接进到容器内部查看文件系统。进去后这个环境是 nginx 镜像自带的精简 Linux,和你本机完全隔离。第一次进去你可能连ls都怀疑自己打错了,因为很多命令路径都精简过,这很正常。
3. 前端项目容器化:从 Dockerfile 到本地跑通
3.1 单阶段 vs 多阶段构建:为什么前端要选后者
前端项目要做成镜像,最简单的做法是一个 Dockerfile 从头装到脚:用 node 装依赖,跑构建,然后装一个 nginx,再把构建产物放进去。这样写没错,但镜像体积会非常吓人,node_modules、构建中间文件全被塞进镜像里,动辄几个 GB,推到服务器既慢又浪费网络资源。
多阶段构建的思路是:一个基础镜像只负责“生产”构建产物,另一个干净的运行镜像只负责“消费”这些产物。最终镜像里只有 nginx 和 dist 目录,没有 node,没有源码,没有 node_modules。我用一个生活例子解释:你只需要端上桌的那盘菜,不会连后厨、煤气灶、备菜台一起端上来。
3.2 写一份能用的 Dockerfile(Vite/Vue/React 通用)
以最常见的 Vite 项目为例,这份 Dockerfile 基本能覆盖 Vue 3 和 React 的静态部署场景:
# 构建阶段 FROM node:20-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build # 运行阶段 FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]说明几个关键点:
AS build给构建阶段起了个别名,下面COPY --from=build就能从那个阶段拷文件。npm ci严格根据 lock 文件安装依赖,比npm install更适合构建环境,能避免依赖版本漂移。daemon off;让 nginx 前台运行,这样 Docker 才能感知 nginx 进程,容器不会跑起来就退出。
前端是 history 路由(vue-router 或 react-router 默认模式)时,刷新二级页面会 404,根因是 nginx 找不到对应的静态文件路径。要解决,在项目根目录放一份nginx.conf:
server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /assets/ { expires 30d; add_header Cache-Control "public, immutable"; } }try_files $uri $uri/ /index.html;的意思是,如果请求路径不是实际文件,就回退到 index.html,由前端路由接管。这个配置我建议直接写进 Dockerfile,别等部署完才想起来,否则又要重新打一次镜像。
3.3 用 .dockerignore 和镜像瘦身
写 Dockerfile 的同时,项目根目录一定要放.dockerignore,它的作用和.gitignore类似,告诉 Docker 哪些文件不要进入构建上下文:
node_modules dist .git .gitignore *.log Dockerfile .dockerignore如果不写这个文件,COPY . .会把整个项目目录打包进构建上下文,里可能有本地 node_modules、无用的截图、日志文件,体积大不说,还可能把本地装的依赖带进去污染构建。我自己最早踩过这个坑,镜像硬是比正常大了十倍不止。
镜像瘦身还能做几件事:一是运行阶段尽量选择alpine或slim标签,体积小很多;二是构建阶段不要装全局包,用npx临时调用工具;三是不要用npm install而用npm ci,某些场景下能避免把 devDependencies 也带进运行阶段。当然,如果项目本身用了 pnpm,Dockerfile 里的构建命令要改成pnpm install和pnpm build,同时锁文件也要一并 COPY 进构建阶段。
3.4 本地构建与运行验证
Dockerfile 写完,先本地 build 一次:
docker build -t my-frontend .-t给镜像打标签。构建过程中你会看到一层一层执行,出现naming to ... docker.io/library/my-frontend:latest就是成功了。然后启动容器:
docker run -d -p 8080:80 --name frontend-test my-frontend curl http://localhost:8080浏览器访问http://localhost:8080,如果页面能打开,F12 切到 Network 面板看看静态资源和接口请求。这一步务必在本地做完整验证,因为后面推送到服务器的镜像就是这个东西,本地测不出问题,服务器上十有八九也正常。
以后改代码重新发布,只需重新执行构建再跑新容器,注意旧的同名容器要先停掉,否则端口冲突:
docker stop frontend-test docker rm frontend-test docker run -d -p 8080:80 --name frontend-test my-frontend4. 容器编排与数据管理:Docker Compose 入门
4.1 为什么单条 docker run 不够用
你可能会想:前端就一个静态站点,直接 docker run 不就行了?确实,只部署一个前端镜像用不上 Compose。但现实场景往往不是孤立的:你可能有后端 API 容器,有 MySQL、Redis,还要配置环境变量、数据卷、内部网络。这时一条一条敲 docker run 很折磨人,也容易敲错。
Docker Compose 的作用是用一个 YAML 文件声明一组容器,一条命令全部启动。它跟前端 package.json 很像,你不需要记住每个依赖怎么启动,写清楚谁是谁,一键 up 就完事。
本地联调时,我还会用 Compose 一次性把前端、后端 API、MySQL、Redis 全拉起来,模拟线上环境。
4.2 写一个 compose 文件把前端和 API 串起来
在项目根目录新建docker-compose.yml:
services: frontend: build: . ports: - "8080:80" depends_on: - api api: image: my-backend:1.0 ports: - "8081:8080" environment: - DB_HOST=mysql - DB_PORT=3306 - DB_PASSWORD=root123 mysql: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD=root123 volumes: - mysql-data:/var/lib/mysql ports: - "3306:3306" volumes: mysql-data:这里的depends_on只是控制启动顺序,不是等 api 完全健康才启动线上业务,不过端口一般起来得快。Compose 会自动创建默认网络,所以容器之间可以直接用服务名互相访问,比如 api 容器里连接数据库,地址写mysql而不是写 IP。这对前端来说尤其友好,你再也不用去背本机的数据库 IP 了。
执行:
docker compose up -d-d表示后台运行。要停掉全部服务,执行docker compose down,数据卷里的数据默认保留,不会误删。
如果前端联调时只想单独把 MySQL 拉起来,不想启动前后端,可以直接用这样一条 docker run,省得每次写 Compose:
docker run --name mysql-dev -e MYSQL_ROOT_PASSWORD=root123 -p 3306:3306 -v mysql-data:/var/lib/mysql -d mysql:8.0这里-v mysql-data:/var/lib/mysql就是数据卷,容器删了,数据库文件还能留在宿主机,下次启动可以继续用。很多前端第一次接触数据卷时觉得抽象,你就把它想成 U 盘,容器是台临时机器,U 盘里的文件不会因为机器重启而消失。
4.3 数据卷与网络的基本玩法
数据卷分命名卷和绑定挂载两种。命名卷交给 Docker 管理,适合数据库这种“我只管存,不想关心它存在哪”的场景。绑定挂载则可以直接指向宿主机目录,比如前端调试时把本地 dist 目录挂进容器,改完构建结果刷新就生效,不用反复构建新镜像:
docker run -d -p 8080:80 -v /path/to/dist:/usr/share/nginx/html nginx:alpine容器网络这块,前端通常不用碰太深,但最好知道:同一台机器上,容器之间默认通过 Compose 网络互相访问;容器和宿主机通过端口映射互访;对外提供服务必须显式-p 宿主机端口:容器端口。后端同事如果让你把前端镜像的 API 地址指到某个服务上,多半就是网络配置问题,你可以拿着docker network ls去看有哪些网络。
5. 从本地到服务器:部署闭环全流程
5.1 镜像仓库:把镜像从本地搬到服务器
本地镜像只存在于你电脑里,服务器看不到,所以需要一个中转站。最省事的办法是注册一个 Docker Hub 账号,免费额度对个人项目够了。先把镜像打个新标签:
docker tag my-frontend yourdockerhub用户名/my-frontend:v1 docker login docker push yourdockerhub用户名/my-frontend:v1如果你的项目不想公开,Docker Hub 的私有仓库也能用,或者用云厂商的镜像仓库服务。企业内部通常有私有仓库,地址一般是registry.内网域名/项目名/镜像名,推送前把镜像 tag 成这个完整地址就行:
docker tag my-frontend registry.example.com/team/my-frontend:v1 docker push registry.example.com/team/my-frontend:v1这里有个坑,很多人第一次 push 报denied: requested access to the resource is denied,不是因为仓库不存在,而是没有先登录,或者 tag 里的仓库名和账号不匹配。先docker login,再重新docker tag把仓库路径写对。
5.2 服务器端安装与首次拉取部署
拿到服务器后,先确保 Docker 已经装好,没装就按第 2 节的一行命令装。接下来 SSH 登录服务器,执行拉取和启动:
ssh root@你的服务器IP # 如果服务器还没装过 Docker curl -fsSL https://get.docker.com | sh systemctl enable --now docker # 拉取你在本地 push 的镜像 docker pull yourdockerhub用户名/my-frontend:v1 # 启动容器 docker run -d --name frontend-app \ -p 80:80 \ --restart unless-stopped \ yourdockerhub用户名/my-frontend:v1--restart unless-stopped很重要。服务器重启、Docker 重启、容器意外退出时,有这个参数容器能自动拉起。没有它,服务器一重启前端就下线,你得手动登录重新跑容器,很麻烦。
启动后访问http://服务器IP,应该能看到页面。如果打不开,先检查云平台的安全组和防火墙规则,80 端口必须对外开放。这是最常见的部署失败原因,镜像本身没问题,是端口没放行。
5.3 端口映射、域名与 Nginx 反代
容器里 nginx 监听 80 端口,宿主机上也可能有另一个 nginx 或者需要配域名、SSL 证书。我比较推荐的方案是:宿主机上的 Nginx 做入口,反代到容器端口,好处是可以同时托管多个容器,也能统一处理证书和域名。
假设容器已经映射到宿主机的 8080 端口,宿主机 Nginx 配置如下:
server { listen 80; server_name yourdomain.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/ { proxy_pass http://127.0.0.1:8081/; } }这里location /api/转发到后端的 8081 端口,前端页面里请求/api就相当于请求后端接口。前后端同域部署的好处是不会有跨域问题,前端代码里接口地址直接写相对路径/api就行,迁移服务器也不用改接口地址。
如果你希望容器本身启动时也带域名配置,可以修改项目里的nginx.conf的server_name,但我不建议把域名写死在镜像里。一个镜像可以被多个环境和域名复用,用宿主机的 Nginx 反代更灵活。
5.4 更新迭代:镜像升级与滚动发布
前端经常要发版本,Docker 的更新流程其实很固定:
# 本地重新构建 docker build -t yourdockerhub用户名/my-frontend:v2 . # 推送到仓库 docker push yourdockerhub用户名/my-frontend:v2 # 在服务器上 docker pull yourdockerhub用户名/my-frontend:v2 docker stop frontend-app docker rm frontend-app docker run -d --name frontend-app \ -p 80:80 \ --restart unless-stopped \ yourdockerhub用户名/my-frontend:v2这种先停后起的方式会有几秒到几十秒的中断,对内部系统完全够用。如果要求不中断,可以用宿主机 Nginx 做负载均衡,或者用 Docker Compose 的滚动更新,但前端静态站一般没必要做到那一步,先停旧再起新是性价比最高的方案。
如果你不想自己维护服务器,也可以把相同镜像推到 Railway 这类容器托管平台,它们会自动拉取并部署,原理和本地 Docker 完全一致,只是运维的工作平台帮你承担了一部分。注意,这只是部署方式不同,镜像和容器的基本概念没变。
6. 前端部署最容易踩的坑(附排查手册)
6.1 Docker Desktop 启动失败与虚拟化问题
Windows 用户装 Docker Desktop 后,遇到的第一个报错十有八九是:
Docker Desktop failed to start because virtualisation support wasn't detected.
遇到这个错误,优先做三件事:一是去 BIOS 确认虚拟化技术(Intel VT-x / SVM)已开启;二是在“启用或关闭 Windows 功能”里打开“虚拟机平台”和“适用于 Linux 的 Windows 子系统”;三是执行wsl --update把 WSL 内核更新到最新。做完重启系统。
还有一部分情况是电脑里同时装了 Hyper-V 和 WSL2,两者冲突。在管理员 PowerShell 里执行wsl --set-default-version 2,然后打开 Docker Desktop 的 Settings -> General,勾选 Use the WSL 2 based engine,问题基本就解决了。
6.2 docker 权限与 socket 连接错误
Linux 服务器上,普通用户执行 docker 命令经常看到这么一段:
permission denied while trying to connect to the Docker daemon socket
原因很简单,docker 需要 root 权限,而你的当前用户不在 docker 用户组里。把用户加进 docker 组就能免 sudo 执行:
sudo usermod -aG docker $USER newgrp dockerWindows 上还有一种典型错误:
failed to connect to the docker api at npipe:////./pipe/dockerdesktoplinuxen
这个多半是 Docker Desktop 没启动,或者 WSL 后端还没就绪。去系统托盘找到 Docker 图标,点一下重启,等它显示 Engine running 再试。如果反复失败,退出 Docker Desktop,管理员身份打开 PowerShell 执行wsl --shutdown,再重新打开 Docker Desktop。
6.3 前端 nginx 配置里的经典事故
第一个经典事故是 history 路由刷新 404。症状是:首页能打开,点击跳转正常,但手动刷新或者直接访问二级路径就 404。原因就是 nginx 没配置try_files,解决办法我在第 3 节已经写过了。
第二个是静态资源 404,尤其是 Vite 项目配置了base: '/xxx/',但 nginx 没有对应路径。这时页面能出来但样式全丢,看 Network 面板会发现assets/xxx.js返回 404。解决办法:要么改 Vite 的 base 为相对路径,要么保证 nginx 的 root 和 base 路径匹配。
第三个是缓存导致旧代码残留。Vite 构建出来的文件一般带 hash,配合Cache-Control设置没问题;但如果 index.html 被缓存,用户更新后看到的还是旧 shell。建议 nginx 里对index.html设置no-cache,对带 hash 的资源设置长缓存。下面这段可以直接参考:
location = /index.html { add_header Cache-Control "no-cache"; }6.4 镜像源慢或拉不下来怎么办
服务器 pull 镜像很慢,或者直接超时,先确认是不是镜像源问题。docker pull默认去 Docker Hub,国内网络时好时坏,最稳妥的办法就是配镜像加速器。
mkdir -p /etc/docker cat <<EOF > /etc/docker/daemon.json { "registry-mirrors": ["https://你的加速地址"] } EOF systemctl daemon-reload systemctl restart docker注意,加速地址要填你云服务商提供的专属地址,不要随便找网上那种公共地址。配置完成后执行docker info,能看到 Registry Mirrors 那一栏列出地址,说明生效了。
如果某些新版本的镜像仓库结构比较特殊,pull 时提示 manifest unknown,先检查镜像版本标签是否存在。比如nginx:latest存在,但nginx:1.27.0不一定存在,去 Docker Hub 页面确认一下版本号再 pull。
6.5 容器内时间不对、日志不输出等杂症
部署完成后发现容器里日志时间差了 8 小时,原因很好理解,基础镜像默认使用 UTC 时区,中国是东八区。一劳永逸的办法是在 docker run 时加环境变量:
docker run -d -e TZ=Asia/Shanghai ...如果你用 Docker Compose,也可以在 service 里加:
environment: - TZ=Asia/Shanghai容器日志不输出则要看应用类型。前端静态部署用 nginx,日志默认写到/var/log/nginx,如果容器里没做软链,docker logs看不到 nginx 的访问日志。可以先docker exec进容器查看文件,或者把 nginx 日志重定向到 stdout:
error_log /dev/stdout info; access_log /dev/stdout;最后还有一句经验之谈:部署出问题第一反应永远是先拉日志,再猜原因。docker logs -f frontend-app能告诉你 80% 的问题出在哪,比你反复改代码重推镜像高效得多。
顺便分享一个我自己的习惯:每次上线前,先在本地docker build出一份镜像,docker run起来实测一遍页面和接口,确认没问题再 push 到服务器。这套闭环流程跑顺之后,前端部署就不再是玄学,而是一件可以稳定复制的日常操作。