ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Nginx多域名分发配置实战:隐藏端口,轻松部署多个前端项目

Nginx多域名分发配置实战:隐藏端口,轻松部署多个前端项目 你有没有遇到过这种情况同一台服务器上跑着好几个前端项目用户访问的时候地址栏里要么得带一串数字端口号要么就得靠记忆区分哪个端口是哪个项目体验非常糟糕。我踩过几次坑之后慢慢整理出一套用 Nginx 按不同域名分发请求、隐藏端口号的部署方案配合前端项目打包产物基本可以做到“域名一输就能打开对应页面”而且浏览器地址栏始终不带端口号。这套方案在日常多项目部署场景里非常实用适合前端开发、全栈工程师、运维新手参考内容会从原理到配置再到排错完整走一遍。背景需求也很简单同一台服务器上可能同时运行两个、三个甚至更多前端项目每个项目监听不同端口或者使用静态文件目录用户希望访问home.example.com看到 A 项目访问admin.example.com看到 B 项目同时地址栏不要出现:8081、:3000这种端口后缀。下面我把从项目准备到 Nginx 配置再到问题排查的完整过程拆开整理出来。1. 场景与原理为什么域名背后要接 Nginx1.1 多项目部署的端口混乱服务器上项目一多第一个问题就是端口规划。常见做法是项目 A 放在 8080项目 B 放在 8081项目 C 放在 3000。服务本身能跑起来但问题也随之而来。用户不可能记住每个项目对应哪个端口。比如同事发来链接写上http://服务器IP:8081每次都要解释“8081是后台管理端8080是用户端”沟通成本高而且http://IP:8081这种地址看起来非常不专业。企业项目如果要做对外展示或者演示一个清爽的域名入口比乱糟糟的端口组合重要得多。另外还有一个细节浏览器访问默认端口是 80HTTP和 443HTTPS非标准端口必须显式写在地址栏里。你希望用户在浏览器里输入home.example.com就直接到达项目首页而不是输入home.example.com:8081。这说明最终要把入口收敛到 80 或 443由反向代理层完成“域名 → 业务端口/静态目录”的路由分发。我当时接手的那台服务器就是这种情况Nginx 里塞了好几个业务系统端口有 8080、8082、8090还有两个纯静态前端项目直接放在/data/www/下。每次新人问“域名是什么”我都得回复一张端口对照表实在忍不了于是决定统一整改。1.2 Nginx 按域名分发请求的核心逻辑Nginx 之所以能解决这个问题是因为它支持配置多条server规则每条规则针对一个域名或一组域名。当请求到达 80 端口时Nginx 会读取 HTTP 请求头里的Host字段和配置文件里的server_name逐条比对匹配成功就把请求交给对应server块处理。这里的“处理”分两种常见形态静态形态root指向前端构建产物目录Nginx 直接返回index.html、JS、CSS 给浏览器适合纯前端项目Vue、React、静态站点都适用。转发形态proxy_pass把请求转发到本机或局域网内某个端口上的服务适合“后端服务 前端页面”同时存在、或者项目本身运行在固定端口上的场景。这两种方式都可以做到浏览器地址栏里不显示真实业务端口因为转发发生在服务端Nginx 代替浏览器去访问业务端口请求链路在 Nginx 那里就完成了。用生活化的例子理解Nginx 就像一个前台接待员每个访问者进门先报名字域名接待员根据名字带着访问者去不同的办公室办公室具体在几楼几号门牌端口号访问者完全不需要知道。1.3 适用边界哪些场景该用、哪些不该用这个方案不是银弹我先说清楚它的适用边界避免你在不合适的场景里强行套用。适合的场景同一台服务器上部署多个独立前端项目希望不同域名访问不同项目。项目监听在非标准端口但希望用户使用默认端口访问隐藏端口号。多项目共用一张服务器证书或不同域名各自证书需要统一提供 HTTPS 入口。项目前后端分离前端是静态页面后端 API 走另一个域名或路径。不适合的场景项目数量非常多比如几十个微服务这种规模更适合引入网关层Kong、APISIX 等做统一治理。需要根据登录用户、访问来源等动态条件做转发Nginx 的 if 语法能做但维护成本高不是它的强项。业务系统依赖 WebSocket、长连接特别重虽然 Nginx 支持proxy_set_header Upgrade但相对复杂需要额外配置。我的经验值是一台机器上 5 个项目以内用 Nginx 多 server 块管理非常清晰再多的话建议从域名字段上做更细的规划或者考虑容器化方案。项目少的时候这个配置方案能让整个服务器的入口一目了然。2. 动手前的准备工作2.1 域名解析让域名找到服务器Nginx 这边的配置是在服务器上做的但在此之前你得先把域名解析到位。这一步出问题会引起很多无头冤案——Nginx 配置明明没问题测试也通过但外网用户就是访问不到。你需要去域名注册商的管理后台把两个域名的 A 记录都指向这台服务器的公网 IP。解析记录类型一般用 A 记录主机记录分别填home和admin记录值填服务器 IPTTL 可以保持默认。如果希望根域名也能用比如example.com也能打开那么还需要添加一条主机记录为的 A 记录。我在实际项目里遇到过一个问题域名在 Dynadot 等注册商后台设置了“域名转发”但没有添加真正的 A 记录导致 Nginx 的 server_name 始终等不到对应的请求。这里多说一句注册商的域名转发只是一个 URL Redirect跳转发生在 DNS 层面遇到需要把不同域名分发到不同前端项目这种场景你真正要配的是 A 记录 Nginx 虚拟主机而不是域名转发。一个账号下多个域名是可以同时配置各自独立的 A 记录的不会互相干扰。DNS 解析配置完成后建议在本地命令行执行ping 你的域名或nslookup 你的域名做验证确认解析到的 IP 和服务器 IP 一致。如果 DNS 刚修改没过多久不同地区生效时间可能有差异一般几分钟到几小时都属于正常范围。2.2 服务器环境与目录规划服务器环境我默认是 LinuxUbuntu/CentOS 均可Nginx 的安装不复杂。Ubuntu 系统使用 aptCentOS 使用 yum/dnf安装完成后执行nginx -v确认版本。如果之前没装过直接使用系统源安装即可。目录规划建议参考下面这个结构/data/www/ ├── home-project/ # 项目A前端构建产物 ├── admin-project/ # 项目B前端构建产物 └── logs/ # 访问日志、错误日志用/data/www/而不是把文件堆在 root 家目录里主要是方便权限管理、备份和路径记忆。前端项目构建产物通常是dist/目录我每次部署时都是把dist目录里的内容直接同步到/data/www/home-project/下目录内部结构就是index.html、assets/、favicon.ico等。如果项目是直接集成了前后端服务的比如 Node 服务同时渲染页面和提供 API那么不需要静态目录直接把请求转发到服务运行的端口即可目录规划可以更简单只要保证端口可用即可。2.3 前端项目的三个必查项配置 Nginx 之前请先检查前端项目的三个关键点不然后面会因为各种小问题来回折腾。第一打包脚本。确认项目能正常执行构建命令Vue 项目通常是npm run buildReact 项目类似构建完成后本地能跑通。如果构建产物本身有问题Nginx 配置再对也没用。第二路由模式。SPA 项目如果使用了history路由模式刷新非首页路径时会 404因为浏览器直接请求了一个物理上不存在的路径。这个问题需要在 Nginx 里用try_files解决后面配置里会专门提到。第三资源路径。构建产物里的 JS、CSS、图片资源引用是相对路径还是绝对路径。默认情况下 Vue/React 的构建配置常输出绝对路径以/开头如果你的项目最终希望通过子路径访问比如home.example.com/app那就得改基础路径如果项目直接绑定域名根路径保持默认即可。我两台项目都做了根路径访问所以没有额外改 base。这三个点看上去基础但影响面很大。我见过有人把dist传到服务器然后 Nginx 始终显示白屏最后发现是打包配置里 publicPath 用了绝对路径导致资源请求 404。检查这个只需要打开浏览器开发者工具看 Network 面板凡是 404 的资源路径都能直接暴露问题。3. Nginx 配置核心一个域名对应一套规则3.1 配置主体结构http server locationNginx 主配置文件的层级关系一定要先建立概念最外层是http块里面可以包含多个server块每个server块里可以有location。在多数 Linux 发行版里http块内部通常会有include /etc/nginx/conf.d/*.conf;或include /etc/nginx/sites-enabled/*;这样的语法因此你可以在/etc/nginx/conf.d/下新建一个项目专属配置文件比如web.conf而不需要动 nginx.conf 主文件。这种“每个项目一个 conf 文件”的做法好处是隔离清晰A 项目的配置、B 项目的配置互不干扰修改其中一个源文件不会波及另一个。等以后项目多了你甚至可以按项目名规范命名文件比如home.conf、admin.conf方便维护。需要注意的是不要把多个域名对应的 server 块全部堆在 nginx.conf 里尤其是服务器上项目多的时候改主配置的风险和心智负担都会放大。我在初期就是这样所有域名全写在nginx.conf里有一次手滑漏写了一个花括号导致整个 Nginx 起不来后来痛定思痛按目录拆分文件再没有出现过这种问题。3.2 方案 A静态文件直接托管如果前端项目是纯静态文件比如 Vue/React 构建之后的产物那么最简单的配置是直接使用root指到对应目录。下面是一个非常典型的单域名静态托管块server { listen 80; server_name home.example.com; root /data/www/home-project; index index.html; location / { try_files $uri $uri/ /index.html; } }这里有几个关键点。listen 80表示 Nginx 监听 80 端口浏览器访问http://home.example.com时不带端口号默认就是 80所以用户在地址栏看不到端口号。server_name用来匹配请求头里的 Host当用户输入home.example.com时Nginx 会选中这个 server 块处理。root /data/www/home-project;指定了这个域名对应的站点根目录。location /里写try_files $uri $uri/ /index.html;这个非常重要——它尝试直接返回请求的物理文件如果找不到文件或目录就回退到 index.html。这就是解决 SPA history 模式刷新 404 的核心配置。为了更直观我画个对比表配置项作用不写的后果listen 80监听默认HTTP端口用户必须输入端口号server_name匹配访问域名请求可能落到错误站点root指定项目文件根目录找不到页面文件index默认首页文件名访问目录时不显示页面try_files路由回退到index.html刷新二级路由时 4043.3 方案 B反向代理到不同端口服务如果项目不是纯静态而是运行在一个固定端口上的服务比如 Node.js 项目跑在 3000Java 项目跑在 8081那么需要使用反向代理方式。配置长这样server { listen 80; server_name admin.example.com; location / { proxy_pass http://127.0.0.1:8081; 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; } }这里的核心是proxy_pass http://127.0.0.1:8081;Nginx 接收到域名请求后把原始请求转发到本机 8081 端口。这里有几个细节需要特别说明。第一proxy_set_header Host $host;要让后端服务感知到用户访问的域名是admin.example.com而不是127.0.0.1:8081否则一些基于域名做动态链接生成的项目可能会生成错误地址。第二X-Forwarded-For和X-Real-IP是为了让后端能拿到真实用户 IP否则后端看到的全部是127.0.0.1。第三如果后端服务支持 WebSocket还需要额外配置 Upgrade 头。选用方案 A 还是方案 B判断标准很简单如果你的前端项目构建完成后通过静态服务器访问没有任何问题就用方案 A如果项目里有 API 接口必须依赖后端进程处理或者项目是前后端同源部署的就用方案 B。3.4 端口号为什么能隐藏掉这里解释一下“不显示端口号”的原理很多人只知配置不知所以然。浏览器访问一个 URL 时如果不显式写端口号HTTP 协议默认使用 80 端口HTTPS 使用 443 端口。Nginx 监听 80或 443用户输入的域名请求就会直接到达 Nginx。Nginx 根据 server_name 判断应该由哪个业务项目处理。在方案 A 里Nginx 直接读了磁盘上的静态文件把内容返回给浏览器整个过程里用户根本没有直接接触业务端口。在方案 B 里Nginx 代替浏览器访问 8081 端口拿到响应后再原样返回给浏览器业务端口只在 Nginx 和业务服务之间被访问用户端永远看不到。这种“用户 → Nginx → 业务服务”的链路就是反向代理的经典形态。所以隐藏端口号的关键不在于端口本身消失了而在于用户的请求入口被收敛到了 80/443 这两个默认端口上。这也是为什么你不需要在地址栏里写端口号。4. 完整实战两份配置让两个域名互不干扰4.1 第一份配置home.example.com 指向项目 A假设项目 A 是 Vue 构建的纯静态前端构建产物已经同步到/data/www/home-project/。我新建配置文件/etc/nginx/conf.d/home.confserver { listen 80; server_name home.example.com; access_log /data/www/logs/home-access.log; error_log /data/www/logs/home-error.log; root /data/www/home-project; index index.html; location / { try_files $uri $uri/ /index.html; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf)$ { expires 30d; add_header Cache-Control public, max-age2592000; } }这份配置我为静态资源加了一层浏览器缓存策略。前端打包后文件名通常带 hash 指纹比如app.a1b2c3.js这类文件内容变化后文件名也会变化可以放心设置长时间缓存。而未带 hash 的 index.html 不能缓存太久所以 index.html 走try_files规则不会被长时间缓存。配置完成后执行nginx -t校验语法无误后执行nginx -s reload使配置生效。此时本机可以先用 curl 验证curl -I http://home.example.com如果返回 200 或 302视有无强制跳转而定说明配置生效了。如果返回 404检查 root 路径是否正确以及目录内容是否存在。4.2 第二份配置admin.example.com 指向项目 B项目 B 是一个部署在 8081 端口上的前后端一体 Node 服务用户通过浏览器访问页面页面上的 API 也由同一个服务提供。那么配置用反向代理方式新建/etc/nginx/conf.d/admin.confserver { listen 80; server_name admin.example.com; access_log /data/www/logs/admin-access.log; error_log /data/www/logs/admin-error.log; location / { proxy_pass http://127.0.0.1:8081; 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; proxy_connect_timeout 60s; proxy_read_timeout 60s; } }我这里加了proxy_connect_timeout和proxy_read_timeout目的很直观防止后端某个接口处理时间较长时Nginx 过早断开。默认的 60 秒在我的场景里够用如果你有数据导出、报表生成这类慢接口建议单独给对应 location 调大点。两份配置同时存在后只要本机 8081 端口服务正常运行、8080 端口的项目 A 静态文件目录正常那么访问home.example.com和admin.example.com会分别进入各自的项目。即使两个项目原本监听不同端口用户在地址栏看到的也只是这两个干净的域名。4.3 HTTPS 证书接入与 HTTP 跳转现在很多项目都要求 HTTPS尤其是涉及登录、支付等敏感操作的页面。证书接入本身不复杂只需要把证书文件和私钥文件放到服务器然后把 server 块从 80 端口迁到 443并增加证书路径配置。这里给出一个完整示例其中 HTTP 请求通过一个单独的 server 块统一跳转到 HTTPSserver { listen 80; server_name home.example.com admin.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name home.example.com; ssl_certificate /etc/nginx/ssl/home.example.com.pem; ssl_certificate_key /etc/nginx/ssl/home.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; root /data/www/home-project; index index.html; location / { try_files $uri $uri/ /index.html; } }这里有几件事要提醒你。证书文件路径要确认 Nginx 用户通常是nginx或www-data有读取权限。常见权限坑是证书文件放在了 root 的 home 目录下其他用户无法读取导致 Nginx 启动失败。我把证书统一放在/etc/nginx/ssl/下并执行chmod 600设置权限稳妥很多。第二如果两个域名需要各自不同的证书那么 443 的 server 块要分别写两个。如果只有一张通配证书比如*.example.com那么两个域名指到同一张证书server_name 可以分开写证书路径共用。实际申请证书时一般用 Let‘s Encrypt 或云厂商免费证书通配证书比较方便推荐优先考虑。第三HTTP 跳 HTTPS 的返回码一般用301永久重定向。注意 301 的缓存机制浏览器一旦记住了 301 跳转即使你后面改回 HTTP也可能仍然强制 HTTPS所以调试阶段建议先使用 302稳定后再切成 301。5. 高频报错与排查记录这一部分是我在实际操作中反复踩坑后总结出来的排查顺序。出现问题时先别急着翻配置从访问链路上把握整体节奏。5.1 域名访问 404IP 却能打开域名配置了 A 记录浏览器访问http://home.example.com却 404但直接用http://服务器IP访问能看到默认页。这种情况通常有两条排查路径。第一条路径域名解析有问题吗执行nslookup home.example.com看返回的 IP 对不对。如果解析出来是别的 IP去 DNS 服务商改记录等待生效。第二条路径域名解析正确但 Nginx 没有匹配到对应 server 块。用 curl 指定 Host 测试curl -I -H Host: home.example.com http://服务器IP如果这样能访问到正确项目说明 Nginx 侧匹配没问题问题出在解析链路。如果仍然 404到配置文件里检查 server_name 是否写错、是否加上了多余的前缀或空白字符。配置文件里一个小空格的异常都可能导致匹配失败。另外要注意多个 server 块同时监听 80 时如果请求的域名匹配不到任何 server_nameNginx 会落到默认 server 块listen 后未指定 default_server 时一般取第一个。如果默认块没配 root就会显示 404。所以如果你只配置了一个域名访问另一个未配置域名时出现默认页或 404这本身符合预期不需要紧张。5.2 SPA 路由刷新 404Vue/React 使用 history 路由时用户访问home.example.com/about并刷新页面Nginx 找不到about这个物理文件就会 404。解决办法就是我在配置里写过的location / { try_files $uri $uri/ /index.html; }这句话的含义是先尝试按请求路径找物理文件找不到就找该路径对应的目录如果目录也不存在则把请求重写到/index.html。前端路由拿到 index.html 后由 JS 根据当前地址渲染对应页面于是刷新 404 问题解决。这里有一个隐患要提醒你try_files $uri $uri/ /index.html;会让任何不存在的路径都返回 index.html这可能导致后台接口地址写错时返回 200 而不是 404前端不容易察觉接口路径错误。但有得必有失对纯前端路由来说这个取舍是值得的。如果 API 请求也走同一个域名建议为/api路径单独配一个 location直接 proxy 到后端不被 try_files 接管。5.3 静态资源加载不出来页面能打开但样式、图片全是白的打开开发者工具发现资源请求 404。这种情况多半是资源路径问题。常见原因有三种第一构建的 publicPath 设置为绝对路径但项目实际部署在子路径下。例如 vue.config.js 里publicPath: /页面引用了/assets/app.js但 Nginx root 下的assets/app.js不在根目录导致 404。解决方法是把 publicPath 改成相对路径或部署目录对应的路径。第二资源确实存在但文件名大小写对不上。Linux 文件系统区分大小写App.js和app.js是两个文件Windows 上本地开发没问题部署到 Linux 就暴露了。第三Nginx 的 location 匹配把静态资源也引到了 try_files而且 index.html 本身有跳转形成循环或者回退异常。这种比较少见一般从资源响应状态码能看出来。排查这类问题最快的方式浏览器 F12 打开 Network点击报错的 JS/CSS查看 Request URL然后到服务器上检查root路径下对应文件是否存在。要相信一点——Nginx 不会凭空生成文件请求路径和实际文件路径必须严格对应。5.4 改了配置没生效这是我对新手最想强调的一点修改 Nginx 配置后必须执行校验和重载否则改动不会生效。正确的操作顺序是nginx -t nginx -s reloadnginx -t是语法校验如果配置有错Nginx 会给出具体文件位置和错误原因此时不要执行 reload。nginx -s reload是平滑重载配置不中断服务。另外还有一种情况你改了配置文件reload 却提示失败大概率是配置语法错误导致 Nginx 无法重载这时根据nginx -t的报错信息修改即可。如果 reload 成功了但访问行为还是没有变化检查一下你有没有在浏览器缓存或 CDN 缓存。开发调试时建议用无痕窗口或 curl 验证。另外如果使用了 Cloudflare 这类 CDNCDN 层的缓存和边缘节点配置也可能影响访问有时候服务器上明明改好了用户侧因为 CDN 缓存访问的还是旧版本需要针对性地处理 CDN 缓存或等待刷新。5.5 问题速查表现象可能原因排查方向域名访问 404IP 访问正常DNS 解析错误或未生效nslookup 查解析结果配置后未生效未执行 nginx -s reload先 nginx -t 校验再 reloadSPA 刷新二级路由 404缺少 try_files 回退location / 加上 try_files页面白屏JS/CSS 404publicPath 配置错误F12 看请求路径检查打包配置跳转到了错误项目server_name 配置错误检查配置文件是否匹配域名HTTP 访问正常但 HTTPS 不通443 端口未放行或证书路径错误检查安全组、证书权限出现 502 Bad Gateway后端服务挂了或端口不通确认业务端口还在监听接口通但页面资源带端口后端生成 URL 未识别代理确认 proxy_set_header Host 已配置6. 再多聊几句多域名匹配的优先级与扩展玩法6.1 server_name 匹配优先级当一个请求进入 Nginx 时如果多个 server 块都监听同一个端口Nginx 会按固定顺序做匹配。它首先尝试精确匹配 server_name比如server_name home.example.com就是精确匹配其次匹配通配符前缀比如*.example.com再次匹配通配符后缀比如home.*最后才使用正则表达式。如果都没匹配到落到 default_server。这个顺序的意义在于配置里不要同时写模糊匹配的*.example.com和精确匹配的一堆域名否则精确域名会命中高优先级模糊域名可能永远收不到请求。日常做法是多项目用精确匹配单独留一个 default_server 做兜底把无法识别的域名引到一个默认页面或直接 404。6.2 同域名下不同路径转发到不同项目和“不同域名跳不同项目”互补的场景是同一个域名下/a路径想访问项目 A/b路径想访问项目 B。此时不需要多套 server只要在同一个 server 里配置多个 location 即可。server { listen 80; server_name example.com; location /a/ { alias /data/www/project-a/; try_files $uri $uri/ /a/index.html; } location /b/ { proxy_pass http://127.0.0.1:8082/; proxy_set_header Host $host; } }这里的alias和root区别容易混淆root会把完整请求路径拼在 root 目录后面而alias会把 location 前缀部分替换为 alias 路径。比如请求/a/index.htmlroot 模式下找的是/data/www/project-a/a/index.htmlalias 模式下找的是/data/www/project-a/index.html。用错会导致路径重复静态资源 404。不过要注意子路径部署的前端项目在构建时一般要把publicPath设置成/a/否则打包出来的资源引用路径全是/assets/...会向域名根路径请求资源导致 404。这种场景下配置和打包是强耦合的改动任何一方都要同步验证。6.3 用 default_server 做兜底服务器不可能永远只有你配置过的域名有些人会直接拿服务器 IP 来访问或者把别的域名解析过来。这种情况下Nginx 会匹配到默认 server。你可以显式指定一个默认 server给它加一个欢迎页或错误页避免暴露服务器上其他站点的目录结构。server { listen 80 default_server; server_name _; return 404; }server_name _;不是通配符而是表示“不匹配任何已知域名”的写法。这里直接返回 404防止外部用户通过未备案域名或服务器 IP 看到任何内容。这个配置在生产环境很有必要相当于给服务器入口加了一道保护。另外如果请求带了参数Nginx 的 location 匹配和 proxy_pass 默认都会透传 query string。如果项目里需要根据参数跳转不同页面可以直接在 location 里用$arg_参数名读取参数内容做条件跳转。我遇到过一个需求同一个域名下带?fromapp参数的用户要跳到落地页 A不带参数的用户跳到首页。实现并不难但 Nginx 的 if 条件不是银弹过于复杂的条件逻辑建议留给应用层处理Nginx 只做简单判断。目前我这台服务器上两套项目就是通过这种方式稳定跑着的。项目 A 的首页、项目 B 的管理端分别挂在两个域名后面用户访问时地址栏永远只有简单的域名端口号彻底从用户体验里消失了。前端跑构建、同步目录、刷新 Nginx整个发布链路也更加清爽。你在实操时可以先用一份配置、一个域名打通链路确认流程无误后再复制出第二份、第三份这样即使出了问题排查范围也能控制到最小。
返回列表