Max
搜索
返回故事会

前端为什么也要学 Docker、Compose 和 k3d

62 分钟阅读0Max ZhangDevOps
DockerKubernetesFrontend Engineering

"我本地是好的"——这句话你说了多少次?

星期二下午四点十七分。

你正准备合上电脑去接孩子放学。或者去楼下买杯咖啡。都行。反正今天的代码已经推了,MR也过了,就等CI绿了自动上线。

PM在群里发了一条消息,带三个感叹号:"线上崩了!!!用户反馈页面白屏,赶紧看一下!!"

你叹了口气,重新打开电脑。打开浏览器,线上页面一切正常。刷新三遍,还是正常。清了CDN缓存再试——也没用。你问PM能不能复现,她说"用户截了图,就是白屏"。截图发过来,确实一片白花花的。

你有点烦躁了。打开监控面板,某个Pod的CPU在三分钟前飙到100%,然后自己掉下来了。你翻日志,翻到一行你见过无数次的东西:

Error: libvips not found

卧槽。

你本地明明能跑。Mac上pnpm install一切顺利,pnpm dev启动速度飞快,改了图片处理逻辑之后你还在浏览器里验证了好几遍。但线上那个Linux容器里,sharp模块找不到libvips这个系统级的C库。

你下意识在群里回了一句:"我本地是好的。"

发完这句话,你盯着屏幕看了三秒钟。

这句话你在职业生涯里说过多少次了?二十次?五十次?从入行第一年开始,这句话就长在你嘴里了。每次项目出问题,第一反应永远是"我本地没问题啊"——好像这句话能帮线上页面恢复一样。

说实话,每次说完你都觉得自己像个傻逼。

因为你心里很清楚,这句话一毛钱的价值都没有。用户不在乎你本地好不好。PM不在乎。老板更不在乎。他们只关心一件事:屏幕上能不能正常看到页面。而你的任务不是证明自己的清白,是让线上恢复正常。

你SSH连上服务器,开始手动装libvips。装好之后图片处理能跑了,但你发现Node版本不对——你本地是Node.js 20,CI流水线配的是Node.js 18,这台生产服务器不知道被谁装的是Node.js 16。项目里有个包用node-gyp编译原生模块,在Node 20下正常工作,在18下偶发性挂,在16下直接编不过去。

你盯着终端,开始怀疑人生。

代码没写错。框架配置没问题。npm包版本全都锁死了。但换了一台机器,操作系统不一样,系统库少了几个,Node版本比你低了四个版本——它就炸了。

这锅到底该谁背?

不是代码的问题。不是什么"玄学"。

是你的代码运行在一个从来没有被明确定义过的环境里。你的团队靠的是"老王离职前跟我说过要装这个"、"那个Confluence文档里写了但两年没更新了"、"你去找运维在机器上手动装一下就行"。

这些就是"环境问题"的全部真相。它不是一个技术难题,它是一个管理真空。

Docker、Compose、k3d要解决的,就是这个真空。


容器化这件事,前端到底该关心什么?

先做一次深呼吸。

暂时忘掉Pod、Service、Ingress、ConfigMap这些词。忘掉Kubernetes那一大张五颜六色的架构图。忘掉那些让你看一眼就觉得"我这辈子肯定学不会"的技术名词。

回到一个更根本的问题。

你说"我本地能跑",这句话到底是什么意思?

拆开来看,它实际的意思是:在macOS 15.4操作系统上,安装了Node.js 20.11.0,用pnpm 9.1.0,在某个时间点通过Homebrew安装了libvipsopensslpython3,设置了一堆你可能早就不记得的环境变量,然后依次执行了pnpm installpnpm dev——在这些条件全部满足的情况下,你的代码运行正常。

问题在于,这堆条件从来没有被"写下来"。

它在你的脑子里。在你的MacBook的某个隐藏配置文件里。在你队友的脑子里。在某个可能已经离职的前端的脑子里。在各种"诶你装一下这个就好了"、"那个错误不用管正常的"、"我也不知道为什么反正这样就能跑"的口头传承里。

Docker做的事情很简单:

把这段话从"脑子里"搬到"文件里"。

不是"口头约定",是"工程事实"。

你不再需要跟新同事说:"你先装个Node 20,版本号是20.11.0别装错了,然后装个python3.11,用brew装libvips记得加--build-from-source,哦对了可能还需要装个make和g++..."。

你把Dockerfile给他就行。一条docker build .,环境就搭好了。跟你本地一模一样——不是"差不多",是真的一模一样。因为大家跑的是同一个基础镜像,执行同一条安装命令,用的是同一套构建流程。

读到这儿你可能会想:"这不就是个打包工具吗?跟Webpack有什么区别?"

区别大了。

Webpack打包的是你写的JavaScript代码——把一堆.ts.tsx.css.scss文件编译、压缩、合并成几个能在浏览器里跑的JS文件。

Docker打包的是你代码的整个生存环境——操作系统、Node运行时、系统C库、字体文件、编译工具链、npm依赖、环境变量、启动命令。全部装在一个标准化的包裹里。

你把这个包裹扔到任何一台装了Docker的机器上,解开,就能跑。Mac、Linux、Windows、云服务器、你朋友的树莓派——只要它能跑Docker,你的代码就能跑。

打个比方。

你去一个陌生的城市出差,晚上到了酒店饥肠辘辘。你需要一碗面。

你有两个选择:

  • 你妈打电话跟你说"先去超市买一袋富强粉,和面的时候水要少量多次加,醒面二十分钟,然后切点五花肉炒个浇头..."。你听了十分钟,最后挂了电话点了个外卖。
  • 你妈在你出门前给你塞了一个保温饭盒,你到了酒店打开盖子,热乎的红烧牛肉面就在里面。

Docker就是这个保温饭盒。

它不关心酒店有没有厨房、灶台是电磁炉还是燃气灶、有没有合适的锅、调味料全不全。这些东西已经被封装好了。你只需要一个能打开饭盒的地方(Docker引擎),就能吃上。


这东西到底解决什么问题?——四个让你头疼的真实场景

现在不聊概念,聊点你一定碰见过的事。

场景一:Node版本像过山车

你本地是Node.js 20。你用到了几个Node 20才有的API,比如.env文件的原生支持,或者某个新版的fetch行为。

团队里有人用的是Node 18——他没升级,因为公司另一个老项目还在Node 16上跑。CI流水线里配的也是Node 18,当初是谁配的、为什么是18,已经没人说得清了。生产服务器上更离谱,装的还是Node 16,因为运维说"上次升Node把那台机器搞挂了不敢再动了"。

三个版本,三种行为。

更卧槽的是,项目里刚好有个包叫sharp,它依赖node-gyp编译原生C++模块。这个包在Node 20下编译一切正常,在Node 18下偶尔编译失败(取决于操作系统的glibc版本),在Node 16下直接报错退出了。

结果就是:

  • 你本地开发:从来没出过问题,一切顺利。
  • CI流水线:有时绿有时红,看运气。你每次push之前都要祈祷一下。
  • 上线那天:崩了。三台生产机器,两台正常,一台挂了,用户访问随机504。

这不是"玄学",这是环境漂移。你的代码是三周前写的,那时候你的Node版本、CI的Node版本、生产的Node版本可能还一致。三周后,它们各自漂向了不同的方向。

场景二:系统依赖像一个看不见的坑

这个场景做前端的太熟了。

你用的是一台MacBook Pro,M3芯片,macOS 15。项目里需要处理用户上传的头像——裁剪、压缩、加滤镜。你用sharp做图片处理,pnpm install顺利通过,本地开发一切正常。

代码推到GitLab,CI流水线启动了。然后你看到日志里一行红字:

Error: Cannot find module './build/Release/sharp-linux-x64.node'

你懵了。什么叫sharp-linux-x64?你刚才装的那个sharp不是在Mac上跑得好好的吗?

答案很简单:sharp是一个包含原生C++代码的npm包。它需要在目标操作系统上编译。你在macOS (arm64)上安装的版本跟Linux (x64)上需要的版本是两回事。CI里的Linux环境没有你Mac上那些东西:

  • 没有libvipssharp的底层依赖)
  • 没有glibc的正确版本
  • 没有字体文件(海报渲染一用就全是方块)
  • 没有python3(某个依赖的编译脚本需要)
  • 没有makeg++(编译原生模块需要)

你在CI日志里看了十分钟,发现错误信息里有一半的名词你根本不认识。最后你找运维,运维在CI的Docker镜像里手动装了一堆东西,终于跑通了。

三个月后公司换了CI工具——从GitLab CI换到了GitHub Actions。一切重来。

前端项目早就不只是"装个npm包"了。它的依赖图里有一大块藏在操作系统层。而这些东西,在大部分团队里从来没有被管理过。

场景三:本地联调是人类能力的极限考验

假设你现在做的东西长这样:

  • 一个前端主应用(Next.js,端口3000)
  • 一个BFF中间层(Node/Express,端口8080)
  • 一个账号服务(Go写的,端口9001)
  • 一个内容服务(Python/FastAPI写的,端口9002)
  • 一个Redis(缓存,端口6379)
  • 一个Nginx(网关转发,端口80)

六个服务。你想在本地跑起来,得开六个终端。

你的日常:

  • 终端一:cd apps/web && pnpm dev
  • 终端二:cd apps/bff && node server.js
  • 终端三:cd services/account && go run main.go
  • 终端四:cd services/content && python main.py
  • 终端五:redis-server
  • 终端六:nginx -c nginx.conf

然后你发现BFF连不上Redis,因为你没设REDIS_HOST。设好之后,Nginx转发规则不对,前端所有API请求404。调了十分钟端口,内容服务又挂了——因为它要连的PostgreSQL数据库你根本没起。

而且这些服务的启动顺序是有依赖的——Redis要先起,BFF要等Redis准备好才能连上,内容服务要等数据库初始化完才能跑,前端要等BFF和内容服务都准备好了才能正常渲染。

你拿着对讲机指挥自己六个手指头跳舞。

"你先起A,等A准备好再起B,B起来之后去改C的.env文件,然后在D里重新加载配置,最后F可能需要重启一次因为缓存没刷新..."

这不是工作。这是行为艺术。

场景四:本地和线上是两套完全不同的网络

你在本地访问localhost:3000,前端页面里的fetch请求直接打到localhost:8080的BFF。BFF再去调localhost:9001的账号服务和localhost:9002的内容服务。一切都在本地环回地址上,全都是明文HTTP,没有任何中间环节。

线上呢?

用户的请求进来之后:

  1. 经过CDN
  2. DNS解析到某个域名
  3. 打到负载均衡器
  4. 负载均衡把请求分给某个网关Pod
  5. 网关做HTTPS卸载
  6. 网关根据路径规则转发:/api/auth/* 到账号服务,/api/content/* 到内容服务,/api/* 到BFF,剩下的到前端应用
  7. 每个服务之间通过Service发现互相调用

这七步里的每一步都可能在某个时刻出问题。而你在本地一行都没有模拟过。

你上线那天,网关的路径重写规则配错了一行,前端所有API请求都打到了错误的服务。用户看到的页面要么白屏,要么数据错乱,要么按钮点了没反应。

你在本地为什么从来没见过这个Bug?因为你在本地压根就没有网关。你的请求是直接从前端打到BFF的,中间什么都没有。

所以结论很直白:很多被叫做"环境问题"的东西,根本就不是什么深奥的技术难题。它只是因为运行环境从来没有被工程化地定义和管理过。

代码质量重要还是环境一致性重要? 答案是:代码再好,环境不对也跑不起来。


三个东西,三个层次

Docker、Compose、k3d。它们不是一个维度的东西,不是"You Only Need One"的选择题。它们是三个不同的答案,对应三个不同的问题。你从最底层的问题开始解决,解决完了发现又有新问题,再往上走一层。


第一层:Docker —— 让单个服务能被稳定打包

先讲个故事

你们公司有一个图片海报渲染服务,叫render-service。三年前老李写的,跑在Node 14上,用canvas库画海报。老李去年离职了。

这个服务的"运行环境知识"分散在以下地方:

  • 老李的脑子里(99%)
  • README.md里的一句话:"需要node14和canvas"(1%)
  • 生产服务器上某个手动apt-get install的历史记录(没人知道)
  • Slack的某条三年前的聊天记录(已经搜不到了)

现在海报渲染出bug了,需要你修。

你从GitLab上把代码拉下来。Readme上说"需要node14和canvas"。你装了Node 14,npm install了一下,报错——node-gyp编译canvas的原生模块失败,因为你的macOS上缺少cairo这个C库。你去装cairo,用brew装了一堆依赖。终于npm install过了,node server.js一跑——又挂了,因为canvas还需要一个叫pango的库,brew没带。

你花了一整个下午,在StackOverflow和GitHub issues之间反复横跳,最后终于在本机跑起来了。

这个下午用来改海报bug的时间,其实只有十五分钟。剩下的时间全花在让代码能跑起来。

Docker解决的就是这个"剩下时间"。

Docker到底做了什么

它让你写一个叫Dockerfile的文件,把老李脑子里的那99%全部写进去:

FROM node:20-alpine

RUN apk add --no-cache \
  cairo \
  pango \
  libjpeg-turbo \
  giflib \
  librsvg \
  pixman

WORKDIR /app

COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

EXPOSE 3000
CMD ["pnpm", "start"]

这份文件就是一份"运行环境搭建声明"。它说清楚了:

  1. 这个服务需要一个Alpine Linux操作系统
  2. 操作系统上需要有Node.js 20
  3. 还需要cairo、pango这些系统C库
  4. 依赖安装用pnpm,构建用pnpm build
  5. 启动用pnpm start,端口3000

任何一个有Docker的机器,不管是Mac、Linux、Windows、云服务器,执行docker build -t render-service . && docker run -p 3000:3000 render-service,都能得到完全一致的运行结果。

老李脑子里的知识,现在变成了一个可以执行、可以版本管理、可以code review的工程文件。团队里任何人修改render-service的环境依赖时,都在Dockerfile里改,而不是在Slack里喊一句"大家记得装一下xxx"。

这就是Docker的核心价值:把"运行环境"从口头约定变成可复制的工程资产。

你得明白的几个核心概念

我不列定义了。那套"镜像是模板容器是实例"的话术你在任何地方都能看到。我说人话。

镜像和容器

镜像:你妈给你塞的那个保温饭盒,还没打开。它包含了这碗面需要的一切。你可以把它放在冰箱里存着,也可以寄给你在外地的朋友,也可以复制十份分给十个人。

容器:你把饭盒打开,正在吃的那碗面。饭盒里的面不会因为你吃了就消失——你吃完一份,还能从同一个饭盒里再倒出一份一样的。镜像也是一样,你可以从一个镜像启动十个容器,每个都是独立运行的实例。

所以"docker run"做的事情就是:把饭盒打开,把面倒进锅里加热,端上桌开始吃。"docker stop"就是吃完了收碗,擦桌子。

Dockerfile —— 运行说明书

Dockerfile不是什么高深的技术规范。它就是一本"怎么做这碗面"的菜谱。

Dockerfile指令对应菜谱里的意思
FROM"基于这个底料开始做"——就像你用了现成的面条底子
RUN"现在做这一步"——比如炒浇头
COPY"把某个东西放进去"——比如切好的葱花
CMD"端上桌的时候最后怎么呈现"——比如撒芝麻、淋香油

你不需要背这些语法。你需要理解的是:写一个好的Dockerfile,等于你替全队人把"这个服务的运行环境应该是什么样"这个问题回答了一遍。而且是可执行、可验证的回答。

构建缓存 —— 为什么先COPY package.json是个好习惯

你注意过没有,Dockerfile里常见的一个模式是:

  1. COPY package.json pnpm-lock.yaml ./
  2. 然后RUN pnpm install
  3. 最后才COPY . .

不是代码洁癖。是正经的工程优化。

Docker构建是分层的。每一行命令就会新增一个"层"。如果某一个层的内容没变化,Docker就跳过它,直接用上次构建时缓存的结果。

如果你一上来就COPY . .(把所有源码拷进去)再装依赖,那么只要你改了一个字——哪怕只是改了某个组件里的一个文案——整个这一层就变了,Docker就得重新安装所有npm依赖。每次构建都全量重装,慢得要死。

但如果你先把package.json和lock文件拷进去,只在这一层装依赖,再拷源码——那大多数情况下你改的只是源码层,依赖层直接用缓存。构建速度可能从五分钟变成五秒。

这不是什么黑科技,就是常识。懂这一点,你写Dockerfile的质量就已经超过了只会上网抄模板的大多数人。

多阶段构建 —— 把"编译时"和"运行时"分开

前端项目有个让人哭笑不得的特点:编译时需要的东西多到你怀疑人生——TypeScript、Webpack/Vite/Turbopack、Babel、Sass、PostCSS、各种plugin和loader。但运行时只需要编译结果——一堆JS文件、CSS文件、.next目录里的静态产物。

如果你把编译时用的所有东西都打进最终镜像,这个镜像会非常大——可能好几G。线上跑的容器里装着一堆永远用不到的工具,浪费磁盘、拉长启动时间、还增加了攻击面。

多阶段构建就把这个过程拆开了:

# 第一阶段:编译 —— 装了一堆工具负责打包
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

# 第二阶段:运行 —— 只拿编译结果,轻装上阵
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["pnpm", "start"]

第一阶段(builder)里什么都有——所有devDependencies、TypeScript编译器、打包工具。第二阶段(runner)只从builder里拿了四个东西:package.json.next目录、public目录、node_modules

最终镜像大小可能从2G降到500M。CI构建更快,线上部署更快,能被打的漏洞也更少。

启动时和构建时不是一回事

还有一个容易踩的坑:你Dockerfile里写的CMD ["pnpm", "start"],是在容器启动时才执行的。而RUN pnpm build是在构建镜像时执行的。这两件事之间可能隔了一个星期——你在星期一构建了镜像,周五运维用它部署了容器。

所以不要在RUN里做启动时才应该做的事(比如RUN pnpm start,这会让你在构建的时候就把服务真的跑起来了一次),也不要在CMD里做构建时才应该做的事(比如CMD pnpm build,这会导致每次容器重启就重新构建一遍)。

分清"做便当的时候"和"吃便当的时候"。

Docker不解决什么

说完了它能干的,必须说明白它干不了的。不然你会产生不切实际的期待,然后失望。

**Docker不解决多服务怎么配合。**它只管把一个服务打包好。你有五个服务,就写五个Dockerfile。但这五个Dockerfile之间没有任何关联,它们不知道彼此的存在。让它们一起协同跑起来——是下一层的事。

**Docker不解决生产部署和扩容。**你用docker run启动一个容器,这跟在生产环境里管理几十个副本、滚动更新、负载均衡、自动扩容是两回事。前者是"我用手扶了一辆车",后者是"我管理一个车队"。

**Docker不帮你写对代码。**容器化只是让你的错误更容易被复现——在所有人的机器上以相同的方式出错。但代码本身的bug、逻辑漏洞、性能问题,它一个都解决不了。

**Docker不自动保证安全。**默认配置下,容器里跑的进程可能是root权限,端口可能暴露到了公网,镜像可能来自一个你不知道的第三方。安全问题需要额外关注——USER nodeEXPOSE只暴露需要暴露的、镜像用官方认证的。

**Docker不解决本地热更新。**你在容器里改了代码,页面不会自动刷新——除非你额外配置了卷挂载和文件监听。本地开发体验这件事,Docker原生并不擅长。

总结:Docker负责的是"单服务运行环境可复制"。这个边界画清楚了,你才不会对它有不切实际的期待。


第二层:Compose —— 让多个服务能一起跑起来

你学会了Docker,给每个前端服务都写好了Dockerfile。你的render-service打包好了,web打包好了,bff打包好了。每个单独看都很完美。

但把它们一起跑起来试试。

打开四个终端,分别docker run四个容器。你要手动指定端口映射(不能冲突),手动建一个Docker网络把它们连起来,手动注入环境变量让它们能找到彼此,手动控制启动顺序——还要等Redis完全就绪了才能启动依赖它的服务。

你能做到吗?能。你想这么做吗?不想。

Compose就是来解决"不想"的。

打个更准确的比方

如果Docker是单个保温饭盒,Compose就是一份便当套餐

  • 一份红烧牛肉面
  • 一碟凉拌黄瓜
  • 一碗紫菜蛋花汤
  • 一小碟泡菜

每样东西是独立打包的(放饭盒里是面、放另一个小格子里是凉菜),但它们作为一个整体一起送到你手里。而且套餐里有"吃法"——先喝汤暖胃,再吃面,凉菜和泡菜是配着面吃的。

docker-compose.yml就是这个套餐的说明书。

它不管每道菜怎么做(那是Dockerfile的事)。它只管这桌饭的整体编排:

services:
  web:
    build: ./apps/web
    ports:
      - '3000:3000'
    depends_on:
      - bff
    volumes:
      - ./apps/web:/app
      - node_modules:/app/node_modules
    environment:
      BFF_URL: http://bff:8080

  bff:
    build: ./apps/bff
    ports:
      - '8080:8080'
    environment:
      REDIS_HOST: redis
    depends_on:
      - redis

  redis:
    image: redis:7-alpine
    ports:
      - '6379:6379'
    volumes:
      - redis-data:/data

volumes:
  redis-data:
  node_modules:

这份文件写清楚了:

  • 这桌饭有一个web、一个bff、一个redis
  • web依赖bff(必须先等bff启动好),bff依赖redis
  • web的源代码目录挂载到容器里(改了代码立即生效)
  • redis的数据目录也挂载出来(容器删了数据还在)
  • bff访问redis的地址是redis:6379——不是localhost,是服务名

注意那个redis。Docker Compose会自动创建一个内部网络,把docker-compose.yml里定义的所有服务连在一起。在这个网络里,服务名就是域名。你不需要记IP地址,写redis就能连上Redis,写bff就能连上BFF。

然后,整个套餐的启动只需要一句话:

docker compose up

所有的容器一起起来了。网络自动建好。环境变量自动注入。启动顺序按depends_on编排好。你不需要开六个终端,不需要手动协调,不需要在README里写"先起A等A好了再起B然后改C的env配置重新加载D的..."

这是Compose的核心能力:把"多个容器如何一起跑"描述清楚。

Compose对前端最有价值的四个场景

第一:微前端或多应用联合调试

你们公司的主产品是一个壳应用,里面嵌了三个子应用——每个子应用有独立的仓库、独立的Dockerfile、独立的dev server。你要在本地把所有应用跑通做联调。

没有Compose之前:分别去四个仓库开四个终端,手动配代理转发路径,搞半小时还不一定跑通。中间任何一个应用改了端口,全局配置全得跟着改。

有了Compose之后:一个docker-compose.yml声明所有应用及其依赖关系。任何同事入职第一天——拉代码,docker compose up,泡杯咖啡回来,所有服务就绪。从"看文档配环境三天"变成"五分钟跑起来"。

第二:SSR/Node服务的完整本地调试

做Next.js项目时,你的服务不只渲染HTML。你还需要Redis做缓存,需要PostgreSQL存数据,需要MinIO模拟S3对象存储。本地装这些不是一个简单的事——安装方式Mac和Linux不一样,版本要跟线上一致,卸载还可能留一堆残留文件。

用Compose,一行配置全搞定:

services:
  web:
    build: .
    ports:
      - '3000:3000'
    environment:
      REDIS_URL: redis://redis:6379
      DATABASE_URL: postgresql://user:password@postgres:5432/mydb
      S3_ENDPOINT: http://minio:9000
    depends_on:
      - redis
      - postgres
      - minio

  redis:
    image: redis:7-alpine

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb

  minio:
    image: minio/minio
    command: server /data --console-address ":9001"

你用的时候docker compose up,用完了docker compose down。来去干净,电脑不会残留任何后台服务。LocalStack、MailHog、Elasticsearch——任何你需要在本地模拟的中间件,用同样的方式加进去。

第三:新人入职的"一键启动"

你们团队的Onboarding文档有多长? "第一步,安装Homebrew / Chocolatey。第二步,用包管理器装Node 20.11.0。第三步,装pnpm。第四步,克隆五个仓库。第五步,分别进每个仓库安装依赖。第六步,安装并启动PostgreSQL。第七步,安装并启动Redis。第八步,配环境变量..."

新人坐在那里,光看文档就要一个上午。中间任何一个环节出问题,就得找人帮忙。如果新人用的是Windows,很多Mac专属的指令还需要额外查替代方案。

不如把所有这些服务写成docker-compose.yml。新人只需要装好Docker Desktop,git clone项目,docker compose up。十分钟内就开始写业务代码了。

第四:CI中的集成测试

跑E2E测试、接口联调测试、性能回归测试时,你需要一个真实的后端环境。以前的做法是:搭建一个专门的测试环境——申请服务器、配置域名、部署服务、维护数据。费时费钱,而且测试之间可能因为数据污染互相影响。

用Compose,CI流水线里直接:

  1. docker compose up -d 启动完整环境
  2. 等健康检查通过
  3. 跑测试
  4. docker compose down --volumes 销毁一切

每次测试环境都是干净的。成本低,速度可控,不会互相干扰。

Compose不解决什么

Compose的定位极其清晰:**它是本地开发和CI测试阶段的文档化和自动化方案。**它不是生产环境工具。

**Compose不负责生产环境编排。**你在docker-compose.yml里跑三个副本,它们还是在同一台机器上。生产环境里你的服务可能跑在二十台机器上,需要跨主机调度、滚动更新、自动扩缩容、健康检查自动替换——这些东西Compose不管。Compose连"把服务部署到多台机器"这个概念都没有。

**Compose不解决单服务打包。**那是Dockerfile的活。你在Compose里写了build: ./apps/web,它只是告诉Compose去哪里找Dockerfile来构建。Dockerfile怎么写得对,还是得你亲自搞。

**Compose不能替代Kubernetes。**很多人在docker-compose.yml里玩得风生水起,就觉得自己"会编排"了。然后运维给他看K8s的Deployment+Service+Ingress三件套,直接懵了。Compose的编排模型跟K8s完全不同——它在本地好用,不代表你懂K8s的那套逻辑。

Compose不会自动帮你重启挂了的东西。restart: always只是重试启动同一个容器,不是"这个容器挂了就自动用新副本替换"。所以Compose环境里偶尔会出现"某个服务起不来,整个环境卡住"的情况——你得手动排查。

**Compose的网络很简单。**没有TLS、没有mTLS、没有外部负载均衡、没有DNS策略、没有网络隔离。这在本地方便,但在生产环境远远不够。

总结:Compose是本地联调和集成测试的性价比之王。别把它当生产工具用。


第三层:k3d —— 在本地搞一个接近线上的环境

现在你用了Docker把每个服务打包好了,用了Compose把本地联调环境也搞定了。新同事入职十分钟就能跑起来,CI里的E2E测试稳如老狗。你觉得挺满意的。

直到上了预发环境,又翻车了。

生产环境不是Compose。是Kubernetes。你们的服务是通过Ingress暴露的,有TLS证书管理。部署通过Deployment做滚动更新,有PodDisruptionBudget保证更新时不断流。多副本间通过Service做负载均衡和服务发现。配置通过ConfigMap和Secret注入。

你在Compose里验证过的东西,到了K8s里仍然出问题:

  • 滚动更新时502——旧Pod已经销毁但连接还没断,新Pod还没就绪
  • 服务发现延迟——DNS缓存导致请求还在往已销毁的Pod上打
  • Ingress路径重写规则跟Compose里配的不一样,API请求全404
  • ConfigMap热更新逻辑跟.env文件完全不同

这些Bug的共性是:它们在Compose环境下根本不会出现,因为Compose没有滚动更新、没有服务发现、没有Ingress Controller。

这就是k3d要解决的问题。

它是什么?怎么理解?

先拆开看:

K3s:一个轻量级的Kubernetes发行版。可以理解成"Kubernetes的迷你版"——核心组件都在(API Server、Scheduler、Controller Manager、etcd),但去掉了很多重型组件,打包成一个不到100M的二进制文件。由Rancher Labs维护,专为边缘计算和开发环境设计。

k3d:一个命令行工具,让你用Docker的方式在本地运行K3s集群。它的核心思路很粗暴——把K3s的控制面节点和工作节点分别跑在Docker容器里,然后把这些容器连成一个集群。你不需要虚拟机、不需要复杂的网络配置。

打个类比:

  • Docker → 驾校场地练起步和倒车
  • Compose → 驾校的模拟街区(有红绿灯但没真车)
  • k3d → **飞行模拟器。**方向盘、油门、仪表盘、路况、天气,跟真上路几乎一样。你在这儿撞了就撞了,但你能提前把上路可能碰到的路况全体验一遍。

对前端来说,k3d的意义不是"我要学K8s运维"。

而是"我要在本地验一下,我的服务在K8s环境下是不是真的能正常工作。"

什么时候你真的需要它?

大部分前端项目不需要k3d。我说真的。如果你们公司线上跑的是Docker Swarm、或者直接裸机部署、或者用的Serverless平台——你完全不用碰k3d。

但如果你在这些情况里,就值得搞:

**一,生产环境就是K8s。**这是最直接的理由。你的部署配置(Deployment、Service、Ingress)已经在代码仓库里了。你本地跑个k3d,直接把这些配置apply进去——在你自己的机器上就能看到跟线上几乎一样的行为。

**二,你需要验证Ingress和路由规则。**前端应用最常见的线上问题之一就是路由不对——/api打到了错误的服务、静态资源路径被错误重写、WebSocket升级被Ingress挡掉了。这些问题在Compose环境下你根本感受不到,因为Compose就是粗暴的端口映射,没有中间层。

**三,你需要验证多副本行为。**SSR服务扩容到三个Pod——每个Pod的状态是独立的吗?缓存是共享的还是本地的?session能不能跨Pod保持?滚动更新时用户会不会掉登录态?这些行为不在K8s里跑一圈,光看代码你是看不出来的。

**四,你需要和平台/运维共享交付标准。**你用的是Compose,运维用的是K8s YAML。你们之间永远隔着一层翻译——"我在本地是这么跑的,你帮我翻译成K8s配置"。如果出了问题,这层翻译就是推诿的源头。用k3d,你自己就能跑K8s配置,在交给运维之前自己先验一遍。

怎么用?三十秒搞定

创建一个单节点本地集群:

k3d cluster create frontend-dev -p "8088:80@loadbalancer" --agents 1

这条命令做了什么?在Docker里起了一个K3s集群:一个Server节点(控制面)、一个Agent节点(工作节点),并且把本地的8088端口映射到集群内置的负载均衡器上。三十秒就位。

把你刚才在本机构建好的镜像导入集群:

k3d image import my-web:latest -c frontend-dev

注意这一步很重要。你本机的Docker镜像默认是k3d集群不可见的——K3s跑在自己的Docker容器里,它有自己的镜像存储。k3d image import做的就是把你本机的镜像拷贝一份到集群里。

然后应用你的K8s部署文件:

kubectl apply -f deploy/k8s/

现在你可以用标准的kubectl命令操作了:

kubectl get pods           # 看Pod状态
kubectl logs <pod-name>    # 看Pod日志
kubectl exec -it <pod-name> -- sh  # 进Pod调试
kubectl describe pod <pod-name>    # 看Pod详细信息
kubectl get svc            # 看Service
kubectl get ingress        # 看Ingress

用完了?一键清理:

k3d cluster delete frontend-dev

干干净净。没有残留的虚拟机,没有奇怪的网络配置。

k3d不解决什么

**k3d不替代Docker。**你得先能把服务打包成镜像,k3d才能用。Dockerfile写不好,k3d就是无米之炊。

**k3d不替代Compose。**如果你只需要本地联调,Compose比k3d简单一百倍。k3d解决的是"接近生产验证",不是"本地开发便利"。你天天用k3d做开发的话,每次改代码要重建镜像、重新导入、重新apply——累死你。

**k3d不是生产环境的100%克隆。**你们公司线上可能用了自定义的CRD(Custom Resource Definition)、Istio或Linkerd之类的Service Mesh、特定的存储插件(CSI Driver)、特殊的网络策略、准入控制器(Admission Controller)。这些东西k3d默认不带,你需要自己额外装。你只能验证K8s核心的编排行为(Deployment、Service、Ingress、ConfigMap),深度的定制组件需要另外配置。

**k3d不等于你懂运维。**能在本地起一个K3s集群,和能设计、维护一个生产级的K8s集群是两件事。前者你花一小时学一下就会了。后者需要你对网络、存储、安全、监控、日志、备份恢复有系统性的理解。别搞混了。

总结:k3d是为了让你在本地验证"我的服务在K8s环境下能不能正常工作"。它不是本地开发的主力工具,Compose才是。


这三者的关系 —— 不是选择,是台阶

聊到这儿你可能有点晕:我现在有三个东西,它们看起来都能"管理容器",我该怎么选?

答案是:不用选。它们不是互相替代的。它们是三个台阶。

第一个台阶:Docker。你做任何容器化的事情之前,先得能把你一个服务打包成镜像。没有镜像,后面什么都跑不了。这就像你先得把菜炒出来,才能谈摆盘。

第二个台阶:Compose。你有了多个服务的镜像之后,需要让它们在本地方便地一起跑起来。Compose解决的是"开发效率"——让你和你的队友不用在终端之间反复横跳。

第三个台阶:k3d。当你发现本地跑得好好的东西到K8s环境就翻车,你就需要k3d在本地补上"接近生产验证"这一层。

它们的关系不是选择题,是做菜的不同阶段:

  • Docker = 把每道菜单独做好
  • Compose = 把一桌菜摆好、确定上菜顺序
  • k3d = 在模拟的餐厅环境里预演一遍服务流程

你不可能跳过第一步直接做第二步——你连菜都没炒出来,摆什么盘?你也不可能跳前两步直接第三步——你连菜是什么样都不知道,在模拟餐厅里预演啥?

所以如果有人跟你说"别学Docker了直接上K8s吧",你可以在心里骂他一句傻逼。饭要一口一口吃。


前端到底该学到什么程度?

这是整篇文章最重要的一节。前面的知识你可以忘,这一节请记住。

学容器化有两条死路,每一条我都见过很多人往里走。

死路一:完全不学。

"我是写前端页面的,Docker跟我有什么关系?"

结果是什么?你越来越被动。环境问题别人都在用Docker解决,你还在手动排查。团队开始用Compose做联调了,你每次起环境要花别人三倍的时间。后端和运维在聊镜像、容器、K8s配置,你听不懂他们在说什么——你成了团队里信息链最短的那个人。

死路二:上来就狂啃K8s全家桶。

买了一本八百页的《Kubernetes权威指南》,从第一章开始看。Pod、ReplicaSet、Deployment、StatefulSet、DaemonSet、Job、CronJob、ConfigMap、Secret、PV、PVC、StorageClass、Ingress、Ingress Controller、Service、Endpoints、NetworkPolicy、RBAC、ServiceAccount、HPA、VPA、PodDisruptionBudget...

学了三个月,名词背得滚瓜烂熟,别人问你怎么给前端项目写个Dockerfile——你说"我先查一下"。

这就是本末倒置。你学K8s是为了在本地验证前端服务的线上行为,不是要转行做运维。你对K8s的掌握程度应该止于"够用",而不是"精通"。

那"够用"的标准是什么?

第一阶:Docker —— 能独立把前端服务容器化

这个东西你必须会。没有商量余地。因为它解决的问题太基础了——单个服务的运行环境可复制。

达标标准:

  • 能给Next.js/Nuxt/Express等常见前端项目类型写出能跑的Dockerfile
  • 会用多阶段构建做镜像瘦身
  • 理解镜像和容器的区别(不需要背术语,能说人话就行)
  • docker builddocker rundocker psdocker logsdocker exec
  • 会调试容器为什么起不来:看日志、检查环境变量、验证端口映射、进容器里手动跑命令排查
  • 理解构建缓存,知道为什么先COPY package.json能大幅加速构建
  • 理解卷挂载,知道怎么让改了代码之后容器里立即生效(不用重新构建)

这个阶段的目标不是"会Docker"这三个字。而是:你团队里任何一个前端服务的运行环境,都可以被一份Dockerfile复现。

做到了,你就已经解决了一半以上的环境问题。

第二阶:Compose —— 把联调环境工程化

这一层也强烈建议学。因为只要你的项目超过两个服务,Compose就能给你省下大量的时间。

达标标准:

  • 能用docker compose同时跑前端、BFF、Redis、数据库、Mock Server等所有开发依赖
  • 理解depends_onportsvolumesenvironmentnetworks
  • 理解Compose自动创建的网络里服务名可以直接当域名用
  • 能给团队维护一份公共的docker-compose.yml,而不是每个人自己写一份
  • 新同事来,拉代码+docker compose up就能跑起全部服务

这一阶的收益非常直接:团队联调时间减少一半以上,新人上手时间从三天变半小时,CI测试稳定性大幅提升。

第三阶:k3d / Kubernetes基础 —— 在确实需要时才学

这一层是可选的。不是所有前端都需要。但如果你的团队生产环境跑在K8s上,你至少应该做到:

达标标准:

  • 能用k3d在本地起一个K8s集群
  • 能把镜像导入并部署(k3d image import + kubectl apply
  • 能看Pod状态、查日志、进容器排查
  • 能看懂Deployment、Service、Ingress的基本YAML结构——不需要会写,但需要能读
  • 知道怎么暴露服务(Ingress vs NodePort vs LoadBalancer的区别)——至少知道概念
  • 出问题时知道去看哪个控制器的日志,而不是盲目panic

对你来说,K8s就是前端的"第四层运行环境":

  1. 你的代码(JS/TS)
  2. Node.js运行时
  3. Docker容器(操作系统+系统依赖)
  4. K8s编排(网络、调度、扩缩容)

你不需要精通第四层。但你能看懂第四层的配置、能在第四层里找出你服务挂了的原因——这就已经是非常有价值的能力了。


在一个真实的大前端项目里,这些东西长什么样

如果你在做中大型前端项目,目录结构最终大概会变成这样:

my-project/
├── apps/
│   ├── web/                   # 前端主应用(Next.js)
│   │   ├── Dockerfile
│   │   └── ...
│   ├── admin/                 # 后台管理(Vite + React)
│   │   ├── Dockerfile
│   │   └── ...
│   ├── bff/                   # BFF中间层(Express)
│   │   ├── Dockerfile
│   │   └── ...
│   └── render-service/        # 海报渲染服务(Node + canvas)
│       ├── Dockerfile
│       └── ...
├── packages/
│   ├── ui/                    # 公共UI组件
│   ├── shared/                # 公共工具函数
│   └── config/                # 公共配置(ESLint、TSConfig等)
├── deploy/
│   ├── docker-compose.yml     # 本地联调编排
│   └── k8s/
│       ├── web/
│       │   ├── deployment.yaml
│       │   └── service.yaml
│       ├── bff/
│       │   ├── deployment.yaml
│       │   └── service.yaml
│       └── ingress.yaml
└── .github/
    └── workflows/
        ├── ci.yml             # CI构建+测试
        └── deploy.yml         # 部署流水线

对应的日常开发流程:

第一步:本地开发。docker compose up把所有服务拉起来。代码通过卷挂载实时同步到容器里,热更新生效。

**第二步:提交代码。**CI流水线触发,用Dockerfile构建镜像,跑测试,把镜像推送到容器仓库。

**第三步:预发部署。**运维团队用deploy/k8s/下的YAML文件把新镜像部署到预发环境。

**第四步:本地验证K8s(可选)。**如果你想在本地验证一下K8s的行为,k3d cluster create起一个集群,k3d image import导入新镜像,kubectl apply -f deploy/k8s/应用部署配置。在浏览器里打开Ingress暴露的地址,排查线上可能出现的编排问题。

这才是Docker、Compose、k3d在真实工作流里的样子——不是三选一的替代品,而是同一条链路上的三个节点。


高频命令其实就这么几条

很多人被容器化劝退是因为一上来就看到几百条命令。Docker有docker一百多个子命令,kubectl更多。但你日常前端工作中真正常用的,两只手完全数得过来。

Docker:六条

docker build -t my-web:latest .       # 构建镜像
docker run -p 3000:3000 my-web:latest # 启动容器
docker ps                              # 看哪些容器在跑
docker logs -f <container-id>          # 实时看日志
docker exec -it <container-id> sh      # 进容器里调试
docker rm -f <container-id>            # 强制删掉某个容器

你80%的Docker操作就这六条。

Compose:六条

docker compose up          # 前台启动所有服务(开发时用)
docker compose up -d       # 后台启动所有服务
docker compose down        # 停掉所有服务
docker compose logs -f     # 看所有服务日志
docker compose logs web    # 只看某个服务的日志
docker compose restart web # 重启某个服务

六条,搞定了本地所有联调需求。

k3d / kubectl:七条

k3d cluster create frontend-dev         # 创建本地K8s集群
k3d cluster delete frontend-dev         # 删除集群
k3d image import my-web:latest -c frontend-dev  # 导入镜像
kubectl apply -f deploy/k8s/            # 应用K8s配置
kubectl get pods                        # 看Pod状态
kubectl logs -f <pod-name>              # 看Pod日志
kubectl exec -it <pod-name> -- sh       # 进Pod里调试

七条。加起来不到二十条命令。不是什么不可逾越的大山。别被"容器化很难"的说法唬住了——那是运维视角,不是前端视角。


给你的判断框架 —— 而不是总结

文章读完了。但以后你还会遇到新的工具和概念。总不能每次都等人写文章帮你捋。

所以我不会给你总结。"总结"这种东西读完了就忘了。

我给你三个问题。以后你碰到任何像"Docker/Compose/K8s"这种"听上去很庞大不知道要不要学"的东西时,先问自己这三句话:

第一问:这个东西管的是"单个服务"还是"多个服务协作"?

如果是单服务(把一个服务打包好),方向就是Docker级别的工具。 如果是多服务(让一堆服务一起跑),才需要看Compose或K8s级别的东西。 不要一上来就看最复杂的。从单服务开始。

第二问:这个东西的目标场景是"开发阶段"还是"生产环境"?

开发阶段的工具:快、轻、容易改、重启快。Compose属于这一类。 生产环境的工具:稳、可扩容、有容错、有监控。K8s属于这一类。

把开发工具用到生产环境(比如Compose跑线上)是自杀。 把生产工具用到开发环境(比如每天用K8s做本地开发)是自虐。

第三问:我的项目里,真的存在它要解决的问题吗?

你们团队三个人,项目就是一个纯静态官网,部署在Vercel上。 那你大概率不需要Compose,100%不需要k3d。Docker都用不上。别因为看了某篇文章就焦虑,觉得不学就落后了。学你真正需要的东西。

反过来,你们的项目有五个服务需要联调,有CI/CD,有SSR,有Node中间层,环境问题已经明显在吃掉团队效率——那就该上什么上什么,不要拖。


这套框架的价值不在于给你答案,而在于让你自己有能力判断。以后没有Max Zhang写文章帮你分析了,你也能自己掂量。


回到最初那个问题

前端为什么要学Docker、Compose和k3d?

不是为了简历上多一行"熟练使用Docker/K8s",不是为了换工作时多要两千块钱,不是为了社交场合跟后端同事有共同话题。

是因为你的代码早就不只在浏览器里跑了。

它在Linux服务器上渲染HTML。它在Node层做身份认证和数据聚合。它在某个Alpine容器的沙箱里处理用户上传的图片。它依赖的Node版本、它链接的系统C库、它所在的网络拓扑——跟你本地MacBook上那个pnpm dev --port 3000的环境,根本不是一个东西。

Docker让你能把"这个环境"精确打包。 Compose让你能描述多个服务在这个环境里怎么配合。 k3d让你能在本地验证这套东西上了K8s会不会炸。

这就是它们的全部价值。

"我本地是好的"不能保你上线不翻。但如果你能用这三样东西把本地环境管理得跟线上一样严格,你上线之前就有底气说一句比"我本地是好的"更靠谱的话:

"我验证过了,能跑。"

读者来信

0/1000

暂无来信,期待你的分享。