1. 前端为什么要碰 Docker:先搞清楚它到底解决了什么问题
1.1 "在我电脑上是好的"——环境差异的锅
做前端几年,你一定听过或者说过这句话:"代码在我本地跑得好好的,怎么到你那儿就不行了?" 这背后往往是环境不一致的问题:Node 版本不同、npm 依赖版本对不上、系统里缺某个底层库、后端接口联调时本地起的服务跟服务器上的配置不一样……排查起来特别费劲,而且很多时候复现不了别人的问题。
我第一次正经接触 Docker,就是因为一个项目在本地开发没问题,但部署到测试服务器后,页面白屏、接口 502、静态资源 404,各种怪问题轮着来。后来发现是服务器上 Node 版本太老,构建出来的产物跟本地不一致,加上 nginx 配置踩了 SPA 路由的坑。折腾了一整天,当时就想:如果能像搬箱子一样,把整个环境一起搬过去该多好。Docker 干的就是这件事,它把"代码 + 运行环境 + 配置"打包成一个标准化的箱子,搬到哪都能跑,不再依赖目标机器上装了什么东西。
这篇文章我打算从前端开发者的视角,把 Docker 从零开始讲明白,一路讲到把前端项目真正部署到服务器上,形成一个完整的闭环。内容不涉及太深的后端知识,所有概念我都会用前端熟悉的东西做类比,保证你学完能自己上手操作。
1.2 Docker 到底是个啥:用前端的话说清楚
你可以把 Docker 理解成一个"快递打包系统"。我们平时发 npm 包,是把代码和依赖打包,别人下载后能不能跑,取决于他的 Node 环境;而 Docker 更进一步,它把"代码 + 依赖 + 操作系统层面对应的运行环境"整个打包成一个镜像。镜像到了任何装了 Docker 的机器上,都能用一模一样的方式启动,环境差异被彻底抹平。
更直白的类比是这样:你写了一个 Vue 项目,本机能跑是因为你装好了 Node、npm、可能还有各种全局工具。服务器上要跑同一个项目,理论上也得装一模一样的 Node 版本、一模一样的全局依赖,还得配好 nginx、设置好环境变量。这一步一步手工操作,只要漏一个细节,项目就起不来。Docker 的做法是:把这些安装和配置过程全部写进一个 Dockerfile 文件,然后构建成一个镜像。以后不管在哪台服务器上,一条 docker run 命令就能把整套环境启动起来,而且保证和你本地构建时完全一致。
Docker 的核心优势,对前端来说主要有三个:一是环境一致性,本地怎么跑,服务器就怎么跑;二是部署简单,一条命令搞定,不用每次上线前写一堆操作文档;三是隔离干净,一个容器一个项目,互不干扰,就算服务器上同时跑十几个前端项目,也不会因为依赖版本不同而打架。
1.3 前端学 Docker 的收益边界
我不建议前端把 Docker 学成运维专家,但有几个场景是实实在在需要用到的:第一种,项目要部署到云服务器,无论是阿里云、腾讯云还是其他平台,用 Docker 部署会比手动装环境省心太多;第二种,公司内部有多个前端项目共用一个服务器,用容器隔离后互不影响;第三种,本地开发时需要依赖一些中间件,比如 Redis、MySQL、Nginx,用 Docker 起一个容器比在本机装原生服务干净得多,用完删掉不会污染系统。
这篇文章就是围绕"前端项目容器化 + 服务器部署"这条主线来写的。我会先把概念讲透,再带你写一个真实的 Dockerfile,把 Vue 或 React 项目构建成镜像,最后推送到服务器上跑起来,并且把更新、日志、数据持久化这些实际部署中一定会遇到的问题也讲清楚。学完这套,你不仅能应付面试里常见的 Docker 问题,更能真正独立完成一个项目的上线。
2. 核心概念:镜像、容器、仓库,别被术语吓住
2.1 镜像(Image):相当于一个"环境的 npm 包"
镜像这个概念,用前端的话说,就是一个包含了完整运行环境的只读模板。它不只是你的代码,还包括了操作系统的基础层、Node 运行时、nginx、项目依赖、配置文件等等。你可以把镜像理解成 npm 包,只不过这个包不是只有 JS 代码,而是连运行它的"宿主环境"一起打包了。
拿 npm 包类比:你用 npm install 装了一个包,这个包有版本号(比如 vue@3.4.0),有依赖关系,有固定的文件结构;Docker 镜像同样有 tag 做版本管理(比如 node:20-alpine、nginx:1.25-alpine),有继承关系(一个镜像可以基于另一个镜像扩展),有固定的分层结构。镜像本身是只读的,你不能直接改一个镜像里的文件,只能基于它创建容器后再改,或者重新构建一个新镜像。
看一个最简单的镜像示例,这就是前端项目最常用的 node 镜像:
FROM node:20-alpine这行代码的意思是:基于 node 20 的 alpine 版本镜像来构建。alpine 是一个特别精简的 Linux 发行版,镜像体积小,只有几 MB 到几十 MB,比完整的 Ubuntu 镜像小很多,用来做前端构建非常合适。这个基础镜像里已经装好了 Node、npm,你不需要再手动去安装。
2.2 容器(Container):跑起来的实例
如果说镜像是类(Class),那容器就是实例(Instance)。这个类比前端同学应该秒懂:类定义了属性和方法,实例才是真正在内存里跑起来的对象。镜像是静态的模板文件,容器是镜像运行后的进程,它有独立的文件系统、网络、进程空间,里面可以跑命令、写文件、起服务。
你可以在同一个镜像基础上启动多个容器,它们相互隔离,互不影响。比如同一个 nginx 镜像,可以启动两个容器,一个跑你的前端项目 A,一个跑项目 B,端口映射不同就行。容器可以被启动、停止、删除,删除后容器里产生的数据(如果没有挂载数据卷)会跟着丢失,但镜像不受影响,随时可以再创建新容器。
我习惯把容器理解为"一个轻量级的虚拟机",但比虚拟机轻得多。虚拟机要跑一整个操作系统,启动可能要几十秒到几分钟;容器直接共享宿主机的操作系统内核,启动只需要几百毫秒到一两秒。这也是为什么 Docker 能在一台服务器上同时跑几十个容器而不太吃力。
2.3 仓库(Registry):镜像的 npm registry
镜像构建好之后,要让人家能用,就得放到一个"镜像仓库"里,类似 npm registry。Docker 官方的仓库叫 Docker Hub,里面有很多官方维护的基础镜像,比如 node、nginx、mysql、redis,这些镜像质量高、更新及时,基本能满足绝大多数需求。
我们自己构建的前端项目镜像,可以推到 Docker Hub 的公开仓库,也可以推到私有仓库。国内常见的私有仓库有阿里云容器镜像服务(ACR)、腾讯云 TCR,也可以自己在服务器上用 Registry 镜像搭建一个简单的私有仓库。生产环境我强烈建议用私有仓库,因为前端项目镜像里可能包含一些不太适合公开的配置信息。
拉取镜像的命令和 npm install 很像:
# npm install vue docker pull node:20-alpine # npm install 指定版本 docker pull nginx:1.25-alpine2.4 数据卷与网络:两个绕不开的概念
数据卷(Volume)是 Docker 里实现数据持久化和宿主机与容器间数据共享的机制。容器被删除后,容器内的文件就没了,但如果你把某个目录挂载成数据卷,数据就会存到宿主机上,容器删了数据还在。比如 nginx 容器里的日志目录,或者前端项目运行时产生的上传文件,都应该挂载到宿主机上。
网络这块,前端只需要理解两件事:端口映射和容器间通信。端口映射是把容器的某个端口映射到宿主机的某个端口,比如把容器里的 nginx 80 端口映射到宿主机的 8080 端口,这样外部访问http://服务器IP:8080就能打到容器里的 nginx。容器间通信则是让多个容器可以通过内部网络互相访问,比如前端容器要访问后端容器,可以不用暴露端口到公网,直接在同一个 Docker 网络里通过容器名访问,这样更安全。
3. 环境准备:本地开发机和服务器都要装好 Docker
3.1 本地安装 Docker Desktop:Windows 和 macOS 的注意事项
本地开发环境推荐直接用 Docker Desktop,它自带图形界面,能直观地看到镜像、容器、数据卷的状态,对新手特别友好。Windows 用户要注意,Docker Desktop 依赖 WSL 2(Windows Subsystem for Linux)或者 Hyper-V,安装前最好先确认 Windows 的虚拟化功能已经开启。
最容易踩的坑是启动时报错 "virtualization support not detected" 或者 "Virtualization support wasn't detected"。遇到这个报错,先去 BIOS 里把 Intel VT-x 或者 AMD-V 打开,然后在 Windows 功能里确认"虚拟机平台"和"适用于 Linux 的 Windows 子系统"这两个选项已勾选,再执行wsl --update更新一下 WSL 内核,重启后一般就能解决。
macOS 用户装 Docker Desktop 相对简单,但要注意芯片类型:Apple Silicon(M1/M2/M3)和 Intel 芯片需要下载不同版本的安装包。装完之后在终端跑一下:
docker version docker compose version如果两个命令都能正常输出版本信息,说明 Docker 已经装好了。Docker Desktop 默认会自动启动,你也可以手动点开它的面板查看当前运行的容器和镜像。
3.2 Linux 服务器安装:用包管理器还是官方脚本
服务器的安装方式跟本地不一样。国内云服务器大多是 CentOS、Ubuntu 或者 Debian,我建议直接跟着 Docker 官方文档用包管理器安装,这样版本比较新,维护也方便。以 Ubuntu 为例:
# 更新 apt 包索引 sudo apt-get update # 安装依赖包,让 apt 可以通过 HTTPS 访问 Docker 官方仓库 sudo apt-get install -y ca-certificates curl # 添加 Docker 官方 GPG 密钥 sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # 添加 Docker 仓库到 apt 源 echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 正式安装 Docker sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin装完之后记得把当前用户加入 docker 组,这样就不用每次敲 sudo 了:
sudo usermod -aG docker $USER newgrp dockerCentOS 的安装步骤类似,只是包管理器从 apt 换成了 yum/dnf。如果你不想折腾这些,也可以直接用官方的一键安装脚本,但我不推荐在生产服务器上这么干,因为脚本不够透明,你没法确认它到底执行了哪些操作。只有一条命令,这个说白了就是两条路的事,看你自己习惯。
3.3 配置镜像加速:在国内拉镜像不被卡住的正确姿势
服务器和本地装好 Docker 后,第一个要解决的问题就是镜像拉取速度。Docker Hub 官方源在国内访问速度很慢,有时候一个 node 镜像能拉十几分钟甚至超时。解决办法是配置国内镜像加速器,把 registry 地址指向国内服务商提供的加速节点。
常见加速器地址有好几个,具体用哪个看你的网络环境。我用的是阿里云的容器镜像加速服务,登录容器镜像服务控制台能看到每个人专属的加速地址,安全性比较高。配置方法是编辑/etc/docker/daemon.json:
{ "registry-mirrors": ["https://你的专属加速地址.mirror.aliyuncs.com"] }修改完后重启 Docker:
sudo systemctl daemon-reload sudo systemctl restart docker然后拉一个镜像测试速度:
docker pull nginx:alpine如果速度明显变快,说明配置生效了。这里有个小细节:daemon.json 这个文件如果不存在,需要手动创建,注意 JSON 格式一定要正确,否则 Docker 会启动失败。配置完成后可以用docker info命令确认镜像加速器是否生效,在输出里能看到 Registry Mirrors 列表。
4. 写 Dockerfile:把 Vue/React 项目打包成前端镜像
4.1 从"前端构建"到"镜像构建"的思路转变
本地跑一个前端项目,流程通常是这样:先npm install装依赖,再npm run build构建出 dist 静态文件,然后用 nginx 指向 dist 目录,或者用 Node 起一个静态服务器。镜像构建的逻辑其实一模一样,只是把这些步骤"固化"到 Dockerfile 里,让 Docker 自动执行。
我第一次写 Dockerfile 时犯过的错,是把npm install和npm run build放在同一个阶段,而且没有利用好镜像缓存,每次改一行代码都要重新npm install,构建一次要等好几分钟。后来才意识到,Docker 的每个 RUN 指令都会生成一个新的镜像层,如果前面的层没变,Docker 会直接复用缓存。所以正确做法是:先把 package.json 和 package-lock.json 复制进去,单独跑一次npm install,再复制源代码。这样只要依赖没有变化,以后构建都会直接命中缓存,速度会快很多。
来看一个基础的 Dockerfile:
# 第一阶段:构建前端静态文件 FROM node:20-alpine AS builder WORKDIR /app # 先复制依赖清单,利用 Docker 缓存 COPY package.json package-lock.json ./ RUN npm install # 再复制源码并构建 COPY . . RUN npm run build # 第二阶段:用 nginx 托管静态文件 FROM nginx:1.25-alpine COPY --from=builder /app/dist /usr/share/nginx/html4.2 多阶段构建:一个 Dockerfile 搞定构建和运行
上面这个 Dockerfile 用了多阶段构建,这是前端镜像推荐的标准姿势。核心思路是:第一阶段用 node 镜像安装依赖并构建,第二阶段用 nginx 镜像只拷贝构建产物。这样做的好处是,最终生成的镜像体积非常小——node 镜像动辄几百 MB,而 nginx:alpine 只有几十 MB,而且镜像是精简的系统,不会带上 npm 缓存和 node_modules 这些构建过程中的"垃圾文件"。
你可以用docker build命令构建镜像:
# 在项目根目录执行 docker build -t my-vue-app:1.0.0 .注意结尾有一个点,表示构建上下文是当前目录。Docker 会把当前目录下的所有文件都发送给 Docker 引擎来做构建,所以一定要在项目根目录加一个.dockerignore文件,把 node_modules、dist、.git 这些不需要的文件排除掉,不然镜像构建会非常慢,甚至会把本地的 node_modules 一起打包进去。
.dockerignore文件内容大致这样:
node_modules dist .git *.log .DS_Store4.3 nginx 配置:解决 SPA 路由刷新 404 的问题
直接用默认 nginx 镜像托管 dist 有一个很常见的问题:Vue 或 React 的 history 路由模式,在页面内跳转没问题,但一刷新或者直接访问某个子路由,nginx 会返回 404。原因很简单,nginx 在磁盘上找不到/detail这个文件,就返回 404 了。
解决办法是配置 nginx 的 try_files 指令,把前端路由的请求全部回退到 index.html。新建一个nginx.conf文件:
server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # 所有请求都回退到 index.html,交给前端路由处理 location / { try_files $uri $uri/ /index.html; } # 静态资源缓存,提升性能 location /assets/ { expires 30d; add_header Cache-Control "public, no-transform"; } # 前端项目里的 favicon 或者其他静态文件 location = /favicon.ico { log_not_found off; access_log off; } }修改 Dockerfile,把自定义的 nginx 配置拷贝进去:
FROM nginx:1.25-alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf4.4 构建镜像并本地验证:跑起来看效果
Dockerfile 写好后,在本地先构建并运行容器验证一下:
# 构建镜像 docker build -t my-vue-app:1.0.0 . # 运行容器,把 80 端口映射到本地 8080 docker run -d -p 8080:80 --name my-app my-vue-app:1.0.0打开浏览器访问http://localhost:8080,如果能看到你的前端页面,而且刷新子路由不会 404,说明镜像构建成功。验证完可以停掉容器:
docker stop my-app docker rm my-app一个小提醒:docker run的-d参数表示后台运行,这样终端不会有日志刷屏;--name给容器起个名字,方便后续管理。如果你想看实时日志,可以docker logs -f my-app。
5. docker run 实战:端口映射、日志、进入容器,一条条讲透
5.1 前端最常用的 Docker 命令,没有之一
很多人觉得 Docker 命令多,记不住,其实前端开发真正常用的就那么几条。先明确一下命令体系:docker ps看当前运行的容器,docker images看本机所有的镜像,docker pull拉镜像,docker run运行容器,docker build构建镜像,docker logs看容器日志,docker exec进入容器执行命令。记住这七条就够起步了。
新手最容易混淆的是run、start和restart。docker run是根据镜像创建一个全新的容器并启动;docker start是启动一个已经存在但处于停止状态的容器;docker restart是重启一个正在运行的容器。你可以用docker ps -a看到所有容器,包括已经停止的,docker ps默认只显示正在运行的。
5.2 端口映射到底怎么理解
端口映射是部署中最关键的一环,理解了它,你就理解了一大半的网络配置。容器内部的服务监听某个端口,比如 nginx 默认监听 80,但这个 80 是容器内部的 80,宿主机并不知道。为了让外部能访问到,必须在docker run时指定映射关系:
docker run -d -p 8080:80 my-vue-app:1.0.0-p 8080:80的意思是把宿主机的 8080 端口映射到容器的 80 端口。外部访问http://服务器IP:8080时,流量会转发到容器内的 80 端口,由 nginx 接收并响应。宿主机端口和容器端口可以是同一个数字,比如-p 80:80,但要注意宿主机端口不能冲突,两台容器不能映射同一个宿主机端口。
有一次我在服务器上同时跑两个前端项目,先跑了一个映射到 8080,第二个也想用 8080,结果报错 "port is already allocated"。这就是端口冲突。解决办法是给第二个项目换一个宿主机端口,比如 8081,或者是把容器放在 Docker 网络里,通过容器名互相访问,不暴露到宿主机端口,这种方式更适合前后端联调场景。
5.3 查看日志和进入容器:排查问题的基本动作
容器跑起来后,问题排查主要靠两条路:看日志和进容器里看现场。看日志很简单:
# 查看最近 100 行日志 docker logs --tail 100 my-app # 实时跟踪日志输出,类似 tail -f docker logs -f my-app如果容器能正常运行但页面有异常,可以进入容器内检查:
# 进入容器,起一个交互式 shell docker exec -it my-app sh # 进入后可以看目录结构、检查配置文件 ls /usr/share/nginx/html cat /etc/nginx/conf.d/default.conf注意,alpine 系统的镜像没有 bash,只有 sh,所以命令是docker exec -it my-app sh。如果你想看正在运行的前端页面有没有被打包进去,看/usr/share/nginx/html目录下是否有 index.html 和 assets 文件夹就行了。这些操作在 Docker Desktop 的图形界面里也能做,但命令行更高效,服务器上也只能用命令行,所以还是尽早习惯比较好。
6. 服务器部署闭环:从本地镜像到线上服务
6.1 方案选型:镜像怎么"搬"到服务器上
本地构建好的镜像,要部署到服务器,主要有三种思路。第一种,把镜像推送到镜像仓库,服务器再从仓库拉取,这是生产环境最推荐的方式;第二种,把项目代码复制到服务器,在服务器上直接构建镜像,适合个人开发小项目;第三种,用docker save把镜像导出成 tar 文件,拷贝到服务器后docker load导入,适合内网环境或者离线部署。
我实际用得最多的是第一种:本地构建并推送到阿里云容器镜像服务,然后在服务器上拉取并运行。这个流程看起来步骤多,但好处是版本管理清晰、回滚方便,而且只要服务器能访问到镜像仓库,不管有多少台服务器,都能拉同一份镜像,保证所有环境一致。
如果你不想用云厂商的镜像仓库,也可以自己搭一个私有的 Registry。在服务器上运行:
docker run -d -p 5000:5000 --name registry registry:2这个命令会启动一个极简的私有镜像仓库,监听 5000 端口。本地构建镜像后,把镜像 tag 改成服务器IP:5000/项目名:版本号,然后推送到这个私有仓库。这种方式适合对数据安全要求高的内网场景,但要注意默认的 Registry 没有鉴权,暴露在公网有风险,所以如果要公网访问,需要加 TLS 和密码认证。
6.2 推送镜像到远程仓库:打 tag 和 push 的完整流程
以阿里云容器镜像服务为例(其他平台类似),流程是这样的:先在控制台创建命名空间和镜像仓库,然后在本地给镜像打 tag,最后推送。
# 登录阿里云 Docker Registry,替换成你自己的地址 docker login --username=你的账号 registry.cn-hangzhou.aliyuncs.com # 给本地镜像打 tag,格式是 仓库地址/命名空间/镜像名:版本号 docker tag my-vue-app:1.0.0 registry.cn-hangzhou.aliyuncs.com/my-namespace/my-vue-app:1.0.0 # 推送镜像 docker push registry.cn-hangzhou.aliyuncs.com/my-namespace/my-vue-app:1.0.0推送的时候有一点要注意,镜像的 tag 就相当于版本号,我用的是语义化版本规则:1.0.0是大版本,1.0.1是小修复,latest则是最新快照。在实际项目里,我倾向于把一个稳定版本同时打上具体版本号和latest,这样服务器拉取时既可以选择固定版本保证稳定,也可以用latest跟上最新改动。
6.3 服务器拉取并运行:从零到线上服务
镜像推送到仓库后,在服务器上只需要三步:登录、拉取、运行。
# 登录镜像仓库 docker login --username=你的账号 registry.cn-hangzhou.aliyuncs.com # 拉取镜像 docker pull registry.cn-hangzhou.aliyuncs.com/my-namespace/my-vue-app:1.0.0 # 运行容器 docker run -d \ --name my-vue-app \ -p 80:80 \ --restart always \ registry.cn-hangzhou.aliyuncs.com/my-namespace/my-vue-app:1.0.0--restart always这个参数特别关键,它的意思是容器意外退出时自动重启。服务器重启后,Docker 也会自动把这个容器拉起来,不用你手动干预。如果不加这个参数,服务器一断电重启,你的服务就再也起不来了,得手动docker start,这在生产环境是不可接受的。
运行之后,用docker ps确认容器状态,然后在浏览器访问http://服务器IP,如果能看到页面,恭喜你,闭环已经完成了。
6.4 用 docker-compose 编排多服务:再加一个容器也不乱
当项目变多,或者你需要同时跑前端、后端、数据库三个容器时,一条条写docker run命令就会很痛苦。这时候用docker compose把你需要启动的所有容器定义在一个 YAML 文件里,一条命令全部启动。
以"前端 + 后端"为例,创建一个docker-compose.yml:
version: "3.8" services: frontend: image: registry.cn-hangzhou.aliyuncs.com/my-namespace/my-vue-app:1.0.0 ports: - "80:80" restart: always backend: image: registry.cn-hangzhou.aliyuncs.com/my-namespace/my-api:1.0.0 ports: - "3000:3000" restart: always用法也很简单:
# 启动所有服务 docker compose up -d # 查看服务状态 docker compose ps # 停止并移除所有服务 docker compose down前端容器要访问后端容器时,不用通过localhost:3000,直接通过服务名backend:3000访问即可。Docker 会为 compose 项目创建一个内部网络,服务名就是容器在网络中的域名。这个特性在前后端联调时非常实用,不用再关心 IP 地址,网络配置也安全得多。注意backend的服务名要尽量只用字母和数字,避免下划线导致 DNS 解析问题。
7. 部署之后的日常工作:日志、持久化、版本更新
7.1 发布新版本的正确姿势:零停机更新
项目上线后,你肯定要持续迭代。最常见的问题是:怎么发布新版本不影响用户访问?如果直接停掉旧容器再启动新容器,中间会有几秒到十几秒的空窗期,页面打不开。对于前端项目来说,简单方案是用docker compose完成滚动更新:
docker compose up -d --build如果用了 compose,它会自动检测到镜像版本变化,先启动新容器,再停掉旧容器。如果新容器启动失败,它会回滚到旧版本,这个过程对用户基本无感。如果不用 compose,就得手动操作:先docker pull新镜像,再docker stop旧容器,docker rm移除旧容器,最后docker run新容器。顺序错了或漏掉一步,服务就可能挂掉。
我踩过的坑是,旧容器还没删掉,新容器因为同名冲突启动失败。所以如果不用 compose,务必把"停止、删除、启动"三步按顺序执行完。实际发布流程中,我建议先在本地构建镜像并本地起容器验证一下,再推送到仓库,最后在服务器上拉取更新。避免把一个有问题的版本直接推到生产环境。
7.2 数据卷挂载:日志和上传文件不能丢
容器是"临时"的,删除容器后里面的文件就没了。但生产环境里,很多数据是不能丢的。对前端项目来说最常见的是两类:nginx 的访问日志和应用产生的上传文件。解决办法是挂载数据卷(Volume),把容器里的目录映射到宿主机上。
用-v参数启动容器:
docker run -d \ --name my-vue-app \ -p 80:80 \ -v /data/my-vue-app/logs:/var/log/nginx \ -v /data/my-vue-app/uploads:/usr/share/nginx/html/uploads \ --restart always \ registry.cn-hangzhou.aliyuncs.com/my-namespace/my-vue-app:1.0.0这里的两个-v参数,分别把 nginx 日志目录和上传目录挂载到宿主机的/data/my-vue-app下。这样即使容器被删掉重建,日志还在、上传文件还在,不会丢数据。数据卷目录的权限也需要注意,宿主机目录的属主和容器内的用户 id 可能需要保持一致,否则容器写文件时可能报 Permission denied。
7.3 容器重启策略:让服务自己恢复
生产环境还有一个常见场景:服务器内存不足导致某个容器被杀,或者代码里有内存泄漏导致进程崩溃。如果没有设置重启策略,容器挂了就挂了,直到你发现用户访问不了才去处理。有了--restart always,Docker 会自动把容器拉回来。
Docker 支持多种重启策略:no是不自动重启,always是任何情况都重启(包括 Docker 服务重启后也自动拉起容器),on-failure是在容器非正常退出时才重启,还可以加次数限制on-failure:3。
在docker-compose.yml里对应配置是:
services: frontend: image: registry.cn-hangzhou.aliyuncs.com/my-namespace/my-vue-app:1.0.0 ports: - "80:80" restart: always我个人的建议:核心服务用always,辅助服务像日志收集这种用unless-stopped。unless-stopped和always的区别是,如果管理员手动停止了容器,unless-stopped不会在重启 Docker 时自动拉起来,always会。这个细节面试也常考,记下来没坏处。
8. 常见问题与排查技巧实录
8.1 容器一直重启(Restarting):从日志里找突破口
部署后最常见的故障,就是容器反复处于 Restarting 状态。你用docker ps会看到容器 STATUS 列显示 "Restarting (1) 5 seconds ago",蒙了。原因五花八门,但排查方向很固定:直接看日志,绝大多数问题一眼就能定位。
docker logs my-app常见的原因有这么几类:一是 nginx 配置写错了,比如缩进问题、少写分号,nginx 启动失败,容器反复重启;二是端口被宿主机上其他进程占了,容器启动时绑定端口失败;三是镜像本身有问题,比如复制进去的静态文件不完整,nginx 找不到 index.html。每种问题日志里都会有明显提示,比如 "address already in use" 说明端口冲突,"nginx: configuration failed" 说明配置写错了。日志看完还解决不了,可以docker inspect my-app查看容器详情,重点看 State、ExitCode、Error 这几个字段。
8.2 镜像拉不下来:加速器失效和 DNS 问题
国内服务器上经常遇到镜像拉不下来的情况,特别是重启了一台新服务器,忘了配置加速器。症状是 pull 的时候卡在 "Waiting" 或者超时。先检查一下/etc/docker/daemon.json是否存在、格式对不对,然后docker info确认加速器列表。
还有一种情况是配置好了加速器但依然慢,可能是 Daemon 没有重启成功,配置文件 JSON 语法错误会导致 Docker 直接拒绝启动。验证方法很简单:改完配置后执行sudo systemctl restart docker,然后立刻docker ps,如果命令报错,大概率是配置文件有问题。把 daemon.json 临时移走再重启,就能确认。
8.3 端口映射访问不了:防火墙、安全组和监听地址
镜像拉下来、容器也起来了、日志正常,但浏览器就是访问不了。这种问题九成不是 Docker 的问题, 而是云服务器的安全组策略。很多云厂商默认只开放少数端口,你要在控制台的安全组规则里放行宿主机端口,比如 8080。Ubuntu 服务器上还有本机防火墙 ufw,用sudo ufw status查看,如果防火墙在启用状态,需要sudo ufw allow 80/tcp。
另一个容易忽略的点是容器内的监听地址。如果容器内的服务监听的是127.0.0.1,就只能容器内部访问;要对外提供服务,得监听0.0.0.0或者::。nginx 默认监听所有地址,一般不会踩这个坑,但如果你用 Node 直接起服务,app.listen(3000)默认监听所有地址,换成app.listen(3000, '127.0.0.1')就只能在容器内访问,映射到宿主机也访问不了。排查这种问题可以在宿主机上curl http://localhost:8080,如果宿主机能通而外网不能通,那就是防火墙或安全组的问题。
8.4 容器时区不对:日志时间慢了 8 小时
容器默认使用 UTC 时区,而我们习惯的是北京时间(UTC+8)。如果你看容器日志,发现时间总是比实际慢 8 小时,这是正常现象,不是 Bug。解决方法是启动容器时挂载宿主机的时区文件:
docker run -d \ --name my-vue-app \ -p 80:80 \ -v /etc/localtime:/etc/localtime:ro \ -v /etc/timezone:/etc/timezone:ro \ my-vue-app:1.0.0或者更规范的做法是,在 Dockerfile 里设置时区环境变量:
ENV TZ=Asia/Shanghai8.5 镜像体积太大:构建产物臃肿的优化思路
前端项目的镜像体积如果超过 200MB,基本可以断定构建阶段有问题。我见过有的同事npm install之后没删掉缓存,构建 mirror 里包含 devDependencies,还有的人直接把整个项目目录 COPY 进去再在容器里 install,导致镜像里塞进了 node_modules、源码、构建配置一大堆东西。
优化思路就三招:一是用多阶段构建,最终镜像只保留构建产物;二是.dockerignore一定要写对,排除 node_modules 和 dist;三是基础镜像用 alpine 版本,能省一半体积。举个实际数据,我用node:20-alpine构建一个 Vue 项目,最终镜像大约 40MB 左右,如果用完整版 node 镜像做最终阶段,动辄 1GB。这个优化对磁盘和拉取速度的影响非常明显。
9. 写在最后:前端把容器思维带进日常开发
写这篇内容的时候,我一直在想一个问题:前端为什么要学 Docker?不是为了卷,而是因为工程化发展到今天,前端不再只是写页面,还承担了部署、联调、环境管理这些事情。Docker 提供了一个很好的抽象,让你把"环境"也当成代码来管理。这种思维方式的转变,比记住几十条命令重要得多。
我自己从"能用 docker run 跑通 demo"到"能独立上线生产项目",中间踩了不少坑,最大的体会是:先在低风险项目里多练,把流程跑顺了再去碰生产环境。例如接手一个简单的活动页或者内部后台,从 Dockerfile 到服务器部署完整走一遍,你会发现自己对前端工程化的理解会深一个层次。踩过的坑越多,后面越稳,遇到 "Exited (1)" 这种报错也不用慌,一条docker logs就能让问题水落石出。
最后再分享一个小技巧:如果你在公司本地开发环境配了 Docker Desktop,试着把 Redis、MySQL 这些中间件全部用 Docker 跑起来,不用的时候一键停掉,系统干净不少。从这些小场景开始用起,你自然会感受到容器化的便利,后面再碰到部署问题时,就不会觉得 Docker 是个陌生工具了。