早上打开终端,vagrant up一个三个月没碰的项目,顺手在 Nginx 里给新站点补了一组server_name。同事路过看了一眼:都到这个年份了,你怎么还在折腾本地虚拟机?
最近圈子里有个预言挺火——到 2028 年,本地开发环境会成为历史遗迹。说实话,我第一次看到这个说法的时候,心里是有点被戳中的。因为这十来年,我确实亲眼看着"本地开发环境"从一个默认前提,慢慢变成了需要被论证、被辩护、甚至被嘲讽的东西。但冷静下来拆这个预言,我又觉得它说得太极端了:本地开发环境不会变成遗迹,它会像一个被重新装修过的老房子——门牌号还在,但功能分区全变了。
这篇文章不打算讲虚的。前半部分聊一聊我对这个预言的真实判断和理由,后半部分把我这两年一直用的、靠得住的一整套"本地虚拟机 + 多端口 Nginx + 多站点自定义域名"配置完整拆出来。这套手艺就算到了 2028 年,依旧会在很多场景里派上用场。而且说实话,越是环境管理工具满天飞的时代,越能理解手动配好一台虚拟机意味着什么。
1. "本地开发环境"到底是什么,为什么总被唱衰
1.1 被重新定义的"本地"
"本地开发环境"这个词,十年前几乎没有歧义:就是指你办公桌上那台电脑里装的一整套工具链。Java 项目配 JDK 和 Maven,PHP 项目配 Nginx 和 PHP-FPM,Python 项目配 virtualenv,前端项目配 Node 和 npm——全部跑在同一台物理机的同一个系统里。
后来这个定义开始膨胀。Docker 出现之后,很多人嘴上说"本地环境",实际指的是宿主机上的一个容器集群;Vagrant 流行的年代,指的是 VirtualBox 里的那台 Ubuntu;再后来,VS Code Remote-SSH 普及了,连"本地的本"都模糊了——你人坐在工位,环境却跑在机房的一台 Linux 上。
这种模糊化本身就是唱衰的土壤。因为当你的"本地环境"已经不再物理存在于眼前这台机器时,"本地"这个词就成了一个历史遗留概念,好像明天就该被丢进博物馆。
1.2 唱衰的三条理由,听着都很有道理
第一,配置成本太高。新员工入职第一天,光是把环境跑起来就要半天,运气不好能折腾两天。"在我机器上明明是好的"这句话,已经被笑话了二十年,但到今天依然是很多团队的日常。环境一致性问题的解法层出不穷——脚本、镜像、配置管理工具、容器编排——但每一种方案都有学习成本和坑位。
第二,云端的算力越来越便宜。远程开发、Web IDE、容器化沙盒,这些东西在今天已经不是玩具了。GitHub Codespaces、各类 Web IDE、JetBrains 的远程开发方案,都能做到"浏览器一打开,开发环境就在那里"。大团队里标准化、集中化、可回收的环境,确实比几百台各自为政的笔记本好管理得多。
第三,AI 编程助手改变了环境的使用方式。以前写代码是人在环境里操作,现在很多场景是 AI 直接读取代码库、生成补丁、跑测试,人只是做审核。当"开发"的主导权从键盘后面的人转移到跑在远端的智能体上,本机要那么重的环境,看起来确实没那么必要了。
这三条理由单独拿出来都成立,拼在一起就构成了一个很像样的预言。但它忽略了一个关键问题:开发者的实际工作流从来不是纯粹理性的工具链堆砌。
2. 我的判断:2028年本地环境不会消失,但会完成三层解构
2.1 第一层:物理机上的"轻薄层生态"会留下来
先亮明我的判断:2028 年,本地开发环境不会消失,但它的形态会变成三层解构,每一层各管一段。
第一层是物理机上的轻薄壳。未来开发者的一台工作电脑,不再需要装全套语言工具链,但一定会装三类东西:一个轻量终端、一个能连远程环境的编辑器、一个本地沙盒运行器。这个沙盒运行器可能是 Dev Container,可能是 Vagrant 加虚拟化软件,也可能是系统级的容器运行时。
为什么这层会留下来?因为开发者本质上需要"一个随时可以动手的院子"。通勤路上、飞机上、断网的时候,你总有一些排查、思考、写小脚本的诉求。2028 年云计算再发达,也不可能保证所有场景都低延迟在线。我自己的体会特别明显:很多难缠的问题,恰恰是在没网或者网络很差的时候,静下心看看日志才找到思路的。
2.2 第二层:虚拟机/容器沙盒继续存在,但会变得更透明
第二层是我最看好的——本地沙盒化。它不会消失,反而会因为"环境即代码"的普及而变得更透明。
过去我们搭一台虚拟机,靠的是手记:装了什么包、改了哪个配置、初始化了什么服务,全在工程师脑子里。到了 2028 年,这些一定是声明式的:一份Vagrantfile、一份Dev Container配置、一组 Nix 表达式,跑出来的虚拟机完全可复现、可销毁、可重建。工具从"人肉运维"变成"配置驱动",本地虚拟机反而会变得更可靠。
而且这一层承担着安全隔离的核心职责。一个项目一套虚拟机、一个项目一组端口,互不污染。公司要交付审计、要隔离客户数据、要测试恶意软件样本,都得靠本地沙盒先兜一层。这些场景别说 2028 年,再往后十年也不会被云化替代。
2.3 第三层:云端执行层的边界到底在哪
云端开发环境当然有大展拳脚的场景。超大仓库的构建、多团队协作的归一化环境、对算力要求离谱的编译任务,这些放云端确实高效。但云端执行层有三个物理边界绕不过去。
一个是网络延迟。哪怕 2028 年网络更好,一个操作从键盘到服务器再渲染回来,物理距离造成的毫秒级延迟,对流畅调试体验的影响是实打实的。另一个是驻留成本。一个常驻的云端开发实例,按小时计费,你随手开着不关,月底账单能吓人一跳。本地虚拟机晚上合上盖子就断电,成本几乎为零。第三个是数据敏感度。很多企业代码、密钥、客户数据,老板根本不想让它们离开内网环境。本地沙盒是"数据不出域"的最低成本方案。
所以我的结论是:2028 年本地开发环境不会是遗迹,而是从一个"默认选择"变成一个"分层匹配的选择"。接下来我要聊的这套配置,正好就处在第二层和第三层之间。
3. 实操:本地虚拟机 + 多端口Nginx + 多站点自定义域名,这套配置2028年还能用
3.1 为什么还要掌握这套手艺
聊完趋势,说点能落地的。
你可能要问了:既然现在 Docker、云 IDE 都这么成熟,为什么还要学"本地虚拟机 + 多端口 Nginx + 多站点自定义域名"这套老手艺?
我的回答很简单:因为它是模拟生产环境的最短路径。Docker 适合跑单个服务,但你要测"一个 Nginx 入口、三个业务站点、各自挂不同域名"这种真实的前后端拓扑,直接上虚拟机反而最顺手。而且很多企业内网环境里,云 IDE 根本推不动,虚拟机方案是少数能绕过重重限制、完全掌握在自己手里的开发方式。
我自己的一个典型场景:手头同时维护三个项目——一个后台管理系统、一个面向用户的 H5、一个对外 API。三个项目跑在同一个本地虚拟机里,需要三个不同的域名来区分。还要让 HTTPS 证书有效,不然浏览器每次打开都红屏警告。这套东西如果靠物理机和 Docker 端口映射来拼,会非常别扭,但用虚拟机加 Nginx 的域名路由,半小时就能搭完,而且逻辑跟线上服务器几乎一模一样。
3.2 方案选型:为什么我优先选私有网络而不是端口转发
先说一个最核心的选型决策:虚拟机网络模式。
很多人知道虚拟机有三种网络:NAT、桥接、仅主机(host-only)。做多站点开发时,最常见的错误是上来就做端口转发——把虚拟机的 8081 映射到宿主机的 8081,然后拿http://localhost:8081访问。这样不是不行,但会有几个麻烦:端口一多,你根本记不住哪个端口对应哪个项目;浏览器地址栏里带着端口号,跟生产环境的 URL 结构不一致;跨域和 Cookie 的处理也会因为 origin 里多了个端口而变得焦躁。
我更推荐的做法是:给虚拟机配一个私有网络固定 IP,然后在宿主机的/etc/hosts里把三个自定义域名全部指向这个 IP,Nginx 里再用不同的server_name区分站点。这样所有站点的访问方式都是http://myapp.local、http://api.local这样的干净地址,跟线上 URL 结构一致。打个比方:端口转发像是打电话翻通讯录一个个找号码,按域名路由则是前台看一眼访客姓名,直接把客人领进对应办公室。
Vagrantfile 里的配置很简单,关键行只有两处:
Vagrant.configure("2") do |config| config.vm.box = "ubuntu/jammy64" # 私有网络固定 IP,宿主机可以通过这个 IP 访问虚拟机 config.vm.network "private_network", ip: "192.168.56.10" config.vm.provider "virtualbox" do |vb| vb.memory = "4096" vb.cpus = 2 end end注意:192.168.56.10这个网段是 VirtualBox 默认 host-only 网段之一,如果你的环境里已经被占用了,可以换成192.168.33.10,或者先跑一下VBoxManage list hostonlyifs看看实际可用网段再定。
3.3 hosts 文件的工作原理与配置细节
自定义域名能不能生效,关键在/etc/hosts。这个文件的本质,就是一台"本地电话簿":系统在发起网络请求时,会先翻这本电话簿,如果找到了对应的名字,就直接拨号到那个 IP,不再去问 DNS 服务器。
我要配三个域名,于是编辑宿主机的 hosts 文件:
192.168.56.10 admin.dev 192.168.56.10 h5.dev 192.168.56.10 api.dev这里有个值得强调的经验:别用.local做自定义域名。.local在 macOS 和很多 Linux 发行版上会被 mDNS/Bonjour 服务抢占,解析结果经常被系统自己的广播应答覆盖,导致你配好的 hosts 不生效。另外,.dev这个后缀已经被纳入 HSTS 预加载列表,浏览器会强制用 HTTPS 访问,在还没有配证书的时候你连 HTTP 都打不开。所以本地开发自定义域名,最稳的是.test,其次是.localhost。我个人习惯用.test,干净且没有历史包袱。
写 hosts 的时候还有个细节:Windows 上要用管理员权限打开记事本,macOS/Linux 上要写/etc/hosts之后可能需要清一下 DNS 缓存。macOS 是sudo dscacheutil -flushcache,Windows 是ipconfig /flushdns,Linux 桌面端通常是重启systemd-resolved或直接用sudo systemd-resolve --flush-caches。配完 hosts 之后,第一时间用ping admin.test验证解析是否生效,别急着开浏览器。
3.4 Nginx 多站点配置:同一入口按域名分流
接下来是重头戏:在虚拟机里配置 Nginx。
虚拟机里先装好 Nginx:
sudo apt update && sudo apt install -y nginxDebian/Ubuntu 系的 Nginx 默认会启用/etc/nginx/sites-enabled/目录,里面每个文件对应一个站点配置。惯例是把站点配置文件写在/etc/nginx/sites-available/,然后做一个软链接到sites-enabled/,这样停用站点只需删软链接,不用动原文件。
我新建一个主配置文件,把所有开发站点都放进去:
# /etc/nginx/sites-available/dev-sites # 站点一:后台管理系统 server { listen 80; server_name admin.test; root /srv/www/admin/dist; index index.html; location / { try_files $uri $uri/ /index.html; } } # 站点二:面向用户的 H5 server { listen 80; server_name h5.test; root /srv/www/h5/dist; index index.html; location / { try_files $uri $uri/ /index.html; } } # 站点三:对外 API,反向代理到 Node/PHP 服务 server { listen 80; server_name api.test; location / { proxy_pass http://127.0.0.1:3000; 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; } }这个配置里最能体现"按域名分流"精神的,是server_name指令。Nginx 收到一个请求后,会先看 HTTP 头里的Host字段,然后用它去匹配所有server块,匹配到了,就把请求交给那个块处理。三个站点,三个名字,三个行为——两个前端站点直接返回静态文件,一个 API 站点做反向代理。
顺带说一句,proxy_pass http://127.0.0.1:3000这里的 3000 端口,是我在本虚拟机里跑的 Node 服务。如果项目多,你完全可以给 API 服务也按端口划分,比如 3001、3002,这样 Nginx 里就能按不同端口往上代理。这就是热词里说的"多端口 Nginx"的核心用法:外部统一走 80 端口按域名分流,内部服务各自监听独立端口。
启用配置并重载:
sudo ln -s /etc/nginx/sites-available/dev-sites /etc/nginx/sites-enabled/dev-sites sudo nginx -t sudo systemctl reload nginxnginx -t这条命令非常重要,它是 Nginx 自带的配置体检工具。如果配置文件里有语法错误——比如少了分号、括号没闭合——它会在重载前直接告诉你,避免你把整个服务搞挂。
3.5 多端口方案的另一种思路:什么时候用端口区分子站点
上面说的是"80 端口 + 域名区分"的方案。但"多端口"这个词既然出现了,我也说一种更老派、但在某些场景依然有效的思路:当一个站点本身就需要按端口区分不同环境时。
比如你有一个服务,同时要跑测试环境和预发布环境,两个环境的入口不能一样。如果你只有一台开发机、一个域名,那就只能靠端口来切:http://app.test:8081是测试,http://app.test:8082是预发布。
实现起来也简单,在同一个 Nginx 配置里写两个listen 8081和listen 8082的 server 块,分别指向不同的 root 或 proxy_pass。像这样:
server { listen 8081; server_name app.test; root /srv/www/app-test; } server { listen 8082; server_name app.test; root /srv/www/app-staging; }不过要提醒一句:这个方案里,如果前端代码用绝对路径引用资源,端口号很容易写死,换环境时分分钟出问题。所以我的建议是——能用域名区分,就别用端口。端口方案适合临时对比、快速验证,域名方案适合长期维护。在本地模拟生产拓扑时,域名方案永远是首选。
3.6 本地自定义域名的 HTTPS 问题:用 mkcert 让浏览器闭嘴
到了这一步,你在浏览器里输入http://admin.test,应该已经能打开站点了。但你可能马上会被另一个问题卡住:现代浏览器对没有 HTTPS 的站点越来越不友好,尤其是你要调试 PWA、Service Worker、摄像头权限这些功能时,没有 HTTPS 寸步难行。
此时你需要一个本地 CA,给这几个自定义域名签名证书。我用的是mkcert,这是一个生成本地信任证书的小工具,思路就是:先在本机装一个"你信任的根证书",然后用这个根证书给任意域名签发子证书,浏览器就会把子证书当作合法证书对待。
在虚拟机上签证书,通用做法是在宿主机上安装 mkcert,然后通过 SSH 把生成的证书传到虚拟机,但我后来发现更顺手的流程是直接在虚拟机内安装:
# 在虚拟机内 sudo apt install -y libnss3-tools # 下载并安装 mkcert,或用 apt 源直接装 mkcert -install mkcert admin.test h5.test api.test "*.test"这几条命令会生成admin.test+2.pem之类的证书文件,里面包含了多个域名的签名。然后把证书放到一个统一目录:
sudo mkdir -p /etc/nginx/certs sudo cp admin.test+2.pem /etc/nginx/certs/dev-sites.pem sudo cp admin.test+2-key.pem /etc/nginx/certs/dev-sites.key最后把 Nginx 配置里的三个 server 块各加一组 SSL 配置。以第一个站点为例:
server { listen 443 ssl; server_name admin.test; ssl_certificate /etc/nginx/certs/dev-sites.pem; ssl_certificate_key /etc/nginx/certs/dev-sites.key; root /srv/www/admin/dist; index index.html; location / { try_files $uri $uri/ /index.html; } } server { listen 80; server_name admin.test; return 301 https://$host$request_uri; }这里我把 HTTP 的 80 端口全部 301 跳转到 HTTPS。浏览器再访问admin.test,就会直接跳到https://admin.test,而且证书被信任,不再出现红色警告。用curl -v https://admin.test验证时,你会看到SSL certificate verify ok.的输出,那时候心里就踏实了。
这套证书方案有个好处:一次安装,长期有效。本地 CA 的根证书信任之后,以后新加的站点域名只要重新签一张证书就行,不需要再装根证书。
4. 配置中的典型坑位与排查思路
4.1 hosts 不生效,到底是谁在捣乱
配置 hosts 后遇到最烦的问题,就是"我明明写了,但浏览器访问还是走了别的地址"。排查顺序很重要。
先确认 hosts 文件真的写对了:有没有把 IP 和域名之间打成空格而不是制表符?末尾有没有不必要的空格?Windows 上保存时是不是被加了.txt后缀?这些低级错误我全踩过。
然后确认是不是有"抢答"的。现在很多操作系统默认开着 DNS over HTTPS 或者系统级的安全防护,会跳过本地 hosts 直接走远程 DNS。如果你发现 ping 域名解析出来的 IP 跟你写的完全不一样,多半是这类机制把 hosts 绕过了。临时关掉这些功能测试一下,能很快定位问题。
最后检查浏览器缓存。现代浏览器会缓存 DNS 解析结果,你改了 hosts 之后,最好把浏览器完全退出再重新打开,或者直接开一个无痕窗口测试。
4.2 端口被杀干净了没有:lsof 和 ss 的妙用
多站点里最恼火的错误,是你配置好了 Nginx,重载也提示 ok,但访问 ip 的 8081 端口就是连不上。
八成是端口被别的进程占了。排查命令:
sudo lsof -i :8081 sudo ss -lntp | grep 8081lsof -i :8081会把占用 8081 端口的进程列出来,ss -lntp则能看到监听状态和对应进程。发现占用之后,要么杀掉冲突进程,要么干脆换一个端口改配置。这里有个经验:虚拟机里的端口和宿主机是隔离的,虚拟机内 8081 被占用,不影响宿主机;但如果是 NAT 端口转发模式,宿主机端口被占用会直接导致转发失败。所以排查之前,先想清楚你的网络模式。
4.3 前端 history 路由刷新就 404
用 Vue Router 或 React Router 的 history 模式开发 SPA 时,你会遇到一个经典问题:在http://admin.test/dashboard页面一刷新,Nginx 就报 404。原因是浏览器向服务器发了一个/dashboard的请求,但你的静态目录里根本不存在dashboard这个文件。
解法就是我在配置里已经写过的这行:
location / { try_files $uri $uri/ /index.html; }它的意思是:先找真实文件,找不到就找目录,再找不到就回退到/index.html,由前端路由接管。这行配置对大部分 SPA 项目都适用。但要注意,如果项目里还有图片、字体这类静态资源,需要单独配location /assets/的缓存策略,否则每次都回退到index.html,资源加载会出大问题。
4.4 虚拟机磁盘快满、文件同步慢
最后一个容易在长期使用中踩到的坑,是虚拟机磁盘和文件同步。
Vagrant 默认会把项目目录同步进虚拟机,但同步性能在目录很大、文件很多时会非常糟糕。实测下来,Vagrant 默认的 rsync 模式在小项目上没问题,但 node_modules 一多,同步一次能让你怀疑人生。目前我比较推荐的方案是:项目代码留在虚拟机内,宿主机只做远程编辑,或者用 NFS 同步(macOS/Linux 上性能不错,Windows 上配置会麻烦一些)。另外定时清一下/var/log和 Docker overlay 目录,别让虚拟机磁盘悄无声息地满了。
5. 写在最后:这套手艺的 2028 年形态
回到开头那个预言。
我的判断是:本地开发环境不会变成历史遗迹,它会变成一个"被重新定位的工具箱"。物理机、虚拟机、云端环境,各管一段,按场景切换。你可能在云端 IDE 里画架构图、跑大仓库构建,但遇到需要细抠网络请求、模拟多站点拓扑、调试系统级依赖的时候,你还是会回到本地虚拟机,配一个 hosts,写几行 Nginx,把自定义域名挂起来。
我个人这两年最大的感受是,工具越自动化,越要理解底层原理。vagrant up一分钟就能拉起一台干净的虚拟机,但你要知道它为什么能在私有网络里被宿主机访问;Nginx 几行配置就能把三个站点分得明明白白,但你要理解server_name的匹配优先级。2028 年,这些原理不会过时,过时的只是"手动敲命令、靠人记步骤"的维护方式。环境即代码,配置可追溯,本地沙盒依然是开发链路里最有掌控感的那一环。
所以我的答案很明确:这不是遗迹,这是我们给自己留的实验室。多站点、多端口、自定义域名这套配置,你以后大概率还会用到。别把它当古董,把它当基本功。