1. 多端口访问到底在解决什么问题
1.1 什么时候需要给 nginx 开多端口
先聊一个非常典型的场景:你手头只有一台服务器,上面跑着好几个项目——一个公司官网、一个后台管理系统、一个对外提供数据的接口服务。三个东西技术栈不一样,部署目录也不一样,更要命的是,它们各自需要独立的更新节奏。
以前偷懒的做法是什么?把三个项目全塞到同一个目录下,靠路径去区分,比如/website、/admin、/api。刚开始还行,但项目一旦开始频繁迭代,问题就来了:后台管理系统改个接口,不小心把官网的静态资源路径写错了,两个项目互相踩踏,排查起来极其痛苦。而且不同项目用的框架不同,有的需要 PHP 解析,有的只需要静态托管,有的要走 Tomcat,挤在同一个 server 块里配置会越来越乱。
这时候就该考虑多端口方案:给每个项目分配一个独立端口,nginx 根据请求进来的端口,把流量分发到对应的项目目录或后端服务上。比如:
8080端口 → 官网静态站8081端口 → 后台管理系统8082端口 → 接口服务
这样做的直接好处是物理隔离。每个 server 块都是独立的配置空间,互不干扰,谁挂了也不影响别人。想更新后台,直接替换后台的目录或重启后台的服务就行,官网那边完全无感知。
1.2 多端口不是唯一的方案,但大部分场景下最优
有人可能会问:用同一个端口、不同域名(server_name)不也能区分吗?确实可以,比如a.example.com指向项目 A,b.example.com指向项目 B。但域名方案有个前提:你得有域名,而且还要做 DNS 解析。
那用同一个端口、不同路径(location)呢?比如/a走项目 A,/b走项目 B。这个方案也能用,但缺点前面说了:项目之间共享同一个 server 上下文,配置一旦复杂起来,location 之间的优先级冲突会让人头大。特别是不同项目的 URL 前缀如果有重叠,匹配规则会让你怀疑人生。
多端口方案最大的优势在于配置简单、隔离彻底、不依赖额外资源。只要服务器防火墙放行对应端口,外部就能直接通过IP:端口访问,内网调试、临时给客户看效果、压测分流都非常方便。所以我个人的习惯是:没有域名约束、项目间完全独立、需要互不影响的场景,优先选多端口;有域名且需要统一 80/443 入口的,再考虑多 server_name 或多 location。本文后面所有配置,都围绕多端口这个核心来展开。
2. 核心配置拆解:端口和路径是怎么关联起来的
2.1 端口的载体:server 块和 listen 指令
nginx 里每个server块就相当于一个虚拟主机,它定义了一套独立的监听规则和处理逻辑。而listen指令就是这套规则的入口——告诉 nginx 这个 server 块负责处理哪个端口进来的请求。
一个最简的多端口配置长这样:
server { listen 8080; server_name localhost; root /opt/www/site1; index index.html; } server { listen 8081; server_name localhost; root /opt/www/site2; index index.html; }这里有几个细节值得展开讲。
listen后面可以直接跟端口,也可以跟IP:端口的组合,比如listen 192.168.1.100:8080;。如果你这台服务器绑定了多个 IP,想限定某个端口只能通过特定 IP 访问,就这么写。不写 IP 的话,默认监听服务器上的所有 IP 地址。
同一台服务器上可以同时存在多个 server 块监听同一个端口,这时候 nginx 会通过server_name来做进一步区分。但如果我们做多端口方案,每个端口只对应一个 server 块,server_name其实写不写都行,养成写上localhost的习惯就好,一方面格式完整,另一方面万一以后加了域名方案,迁移成本低。
还有一个非常实用的参数:listen 8080 default_server;。default_server表示这个 server 块作为该端口的默认处理者。当某个端口的请求在多个 server 块之间匹配不到合适的server_name时,就会落到这个默认块上。如果只有一个 server 块监听该端口,加不加这个参数效果一样,但加上之后语义更明确,也方便以后扩展。
2.2 location 匹配规则:路径分流的基础
配置完端口,接下来就是路径的处理。nginx 的路径匹配是靠location指令实现的。很多人在这里栽跟头,是因为没搞明白 location 的匹配优先级。
nginx 的 location 匹配规则按优先级从高到低排列如下:
| 匹配方式 | 写法示例 | 优先级 | 说明 |
|---|---|---|---|
| 精确匹配 | location = /api | 最高 | 只有请求路径完全等于/api时才命中 |
前缀匹配(带^~) | location ^~ /static/ | 高 | 一旦匹配成功,不再检查正则 |
| 正则匹配(区分大小写) | location ~ \.php$ | 中 | 按书写顺序从上到下匹配,第一个命中的生效 |
| 正则匹配(不区分大小写) | location ~* \.jpg$ | 中 | 同上,但不区分大小写 |
| 普通前缀匹配 | location /api/ | 低 | 最长匹配原则,匹配到最长的那个前缀 |
| 兜底匹配 | location / | 最低 | 以上都没命中时使用 |
举个例子。如果配置了:
location = /api { return 200 "exact"; } location /api/ { return 200 "prefix"; } location ~ ^/api/v[0-9]+ { return 200 "regex"; }请求/api会命中第一条;请求/api/users会先看正则,如果正则不匹配,再落到前缀匹配/api/;如果请求是/api/v2/users,正则命中,返回regex。
理解这个优先级非常重要,因为多端口方案里经常会发生这样的情况:明明配置了一个 location 想处理某个路径,结果请求进来后命中的却是另一个 location,导致返回结果完全不对。后面排查部分我会专门讲这个问题。
2.3 root、alias、proxy_pass:三种路径指向方式
location 匹配到之后,真正把请求交给谁、指向哪个路径,取决于root、alias和proxy_pass这三个指令。它们的区别如果不搞清楚,配置起来就是玄学。
root是最常用的,它的语义是:把 location 匹配到的完整路径拼接在 root 指定的目录后面。比如:
location /blog/ { root /opt/www; }请求/blog/post.html时,nginx 会去/opt/www/blog/post.html找文件。注意,root 是不舍弃 location 前缀的,路径是直接拼上去的。
alias的语义则是:用 alias 指定的路径替换掉 location 匹配到的前缀。比如:
location /blog/ { alias /opt/www/static/; }请求/blog/post.html时,nginx 会去/opt/www/static/post.html找文件。/blog/这个前缀被完整替换掉了。
proxy_pass则完全不同,它不涉及本地文件系统,而是把请求转发给后端服务。比如:
location /api/ { proxy_pass http://127.0.0.1:8082/; }最关键的区别在于proxy_pass后面带不带斜杠。带斜杠(http://127.0.0.1:8082/)表示转发时把 location 匹配到的前缀去掉;不带斜杠(http://127.0.0.1:8082)则表示保留完整路径。这个细节是项目中最常见的"路径变长、404"的元凶,后面我会用案例详细说明。
简而言之,三者的选择逻辑是:
- 托管静态文件:优先用
root,直观、好理解 - 想把请求映射到另一段完全不同的目录:用
alias - 把请求转给后端服务(Java、Python、Node):用
proxy_pass
3. 从零搭建多端口多站点:实操全过程
3.1 项目规划与目录结构设计
理论说了不少,现在来点实际的。我以一个非常常见的服务器场景为例:一台 Linux 服务器(CentOS 系),IP 假设是192.168.1.100,上面跑了三个项目,规划如下:
| 项目 | 端口 | 类型 | 访问入口 |
|---|---|---|---|
| 公司官网 | 8080 | 纯静态站 | http://192.168.1.100:8080 |
| 后台管理系统 | 8081 | 静态页面 + 调用后端接口 | http://192.168.1.100:8081 |
| 用户接口服务 | 8082 | Spring Boot 应用 | http://192.168.1.100:8082 |
目录设计为:
/opt/www/ ├── site1/ # 官网静态文件 │ ├── index.html │ └── assets/ ├── site2/ # 后台管理系统 │ ├── index.html │ └── static/ └── app/ # 存放 Spring Boot 包和运行脚本 └── user-api.jar这里有个建议:把所有站点都归拢到一个统一目录下,比如/opt/www,或者/data/www,按站点名分子目录。这样做的好处是备份、迁移、权限管理都方便。不要东一个目录西一个目录,用的时候找半天。
然后启动后端服务。Spring Boot 默认监听8080端口,但我们要让 nginx 的 8082 转发过去,所以这里后端服务改成监听127.0.0.1:18082,这样既不影响 nginx 对外提供 8082 端口,又避免了直接暴露后端端口,意会一下这个"内部服务不对外"的设计:
java -jar /opt/app/user-api.jar --server.port=18082当然也可以让后端直接监听 8082,nginx 用 8082 转发到本机 8082,有点多此一举;更稳妥的方式是后端只监听内网地址,外部流量统一走 nginx 这个口子。这个设计在后续维护中会省很多事。
3.2 编写多端口 nginx 配置
nginx 的配置目录结构一般是/etc/nginx/,主配置文件是nginx.conf。在nginx.conf的http块里,默认会有一行:
include /etc/nginx/conf.d/*.conf;这个include指令非常关键——它把conf.d目录下所有以.conf结尾的文件都并入了主配置。我们管理多站点时,最好不要把全部东西堆在nginx.conf里,而是按站点拆分配置文件,维护起来清爽得多。比如我为上述三个项目建一个/etc/nginx/conf.d/multi-site.conf,内容如下:
server { listen 8080; server_name localhost; root /opt/www/site1; index index.html; location / { try_files $uri $uri/ =404; } } server { listen 8081; server_name localhost; root /opt/www/site2; index index.html; location / { try_files $uri $uri/ =404; } # 后台系统需要调接口,统一转发到后端服务 location /api/ { proxy_pass http://127.0.0.1:18082; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } server { listen 8082; server_name localhost; location / { proxy_pass http://127.0.0.1:18082; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }逐个拆解一下。
官网(8080)就是一个标准的静态站点配置,try_files $uri $uri/ =404这句的作用是:先尝试找对应文件,找不到再尝试找目录,都找不到就返回 404。这个写法比直接index index.html更严谨,能避免一些目录穿越或空白页面的问题。
后台管理系统(8081)除了托管静态文件,还要处理接口请求。假设后台页面的 JS 里所有请求都发到/api/xxx,那么 nginx 就把这个路径转发给后端18082。这里proxy_pass后面没有带斜杠,所以转发时保留了/api/前缀,后端接口如果定义的是/api/user/list,就能正确拿到路径。
接口服务(8082)就是把所有请求全盘转发给后端。这个端口一般只对内网开放,或者只给特定的调用方使用,配置上就一个location /+proxy_pass,非常纯粹。
另外,如果你发现自己的场景里 8081 后台系统并不需要单独对外暴露接口端口(也就是第三个 server 块),反而希望后台所有接口都走 8081 的/api/,那第三个 server 块可以完全删掉,只保留 8080 和 8081 两个 server 块就行。这里我保留 8082 是为了展示"一个端口对应一个 server 块"的完整形态,方便读者做变形。
3.3 语法检查与重载:改动配置后的必备操作
配置文件写完,第一步不是重启,而是检查语法。nginx 提供了非常贴心的检查命令:
nginx -t如果配置正确,输出是:
nginx: configuration file /etc/nginx/nginx.conf test is successful如果配置有问题,它会明确告诉你哪个文件哪一行出错。我见过太多人直接nginx -s reload,结果配置写错了,reload 失败,服务没挂但新配置没生效,一脸懵。凡是改配置,先nginx -t,再 reload,这个习惯值万金。
检查通过后,执行平滑重载:
nginx -s reloadreload是平滑重载,不会中断正在处理的请求,nginx 会先把新的配置解析好、加载好,老的工作进程处理完手头的请求后自动退出。所以这个操作是安全的,不用怕影响线上业务。
重载完成后,验证一下端口监听状态:
netstat -tlnp | grep nginx或者用新版一点的工具:
ss -tlnp | grep nginx正常应该看到三个端口都在 LISTEN:
tcp LISTEN 0 511 0.0.0.0:8080 0.0.0.0:* users:(("nginx",pid=1234,fd=6)) tcp LISTEN 0 511 0.0.0.0:8081 0.0.0.0:* users:(("nginx",pid=1234,fd=7)) tcp LISTEN 0 511 0.0.0.0:8082 0.0.0.0:* users:(("nginx",pid=1234,fd=8))然后本地先验证一下静态站能不能访问:
curl -I http://127.0.0.1:8080/ curl -I http://127.0.0.1:8081/返回200 OK说明静态站没问题。再测接口转发:
curl http://127.0.0.1:8082/api/user/list如果后端服务正常,就能看到 JSON 数据。本地测通了,再去外部访问,这样能把问题限定在"nginx 配置层面"还是"防火墙网络层面",排查效率会高很多。
3.4 别忘了防火墙和云平台安全组
这一步是很多人忽略的。本地 curl 通了,但外部浏览器访问不了,十有八九是防火墙没放行端口。
CentOS 7/8 上默认用的是 firewalld,添加端口的命令是:
firewall-cmd --permanent --add-port=8080/tcp firewall-cmd --permanent --add-port=8081/tcp firewall-cmd --permanent --add-port=8082/tcp firewall-cmd --reload如果服务器用的 iptables,则类似:
iptables -A INPUT -p tcp --dport 8080 -j ACCEPT iptables -A INPUT -p tcp --dport 8081 -j ACCEPT iptables -A INPUT -p tcp --dport 8082 -j ACCEPT service iptables save另外,如果服务器买的是云厂商的 ECS(比如阿里云、腾讯云、华为云),你还要去云控制台的安全组里放行对应端口。这个非常重要,因为云安全组的优先级高于操作系统防火墙,安全组不放行,系统防火墙开了也没用。
我踩过一次坑:在腾讯云的一台服务器上折腾了半天防火墙规则,外部就是访问不了 8081 端口,最后发现是安全组只放行了 80/443/22,压根没加 8081。所以遇到"本地通、外网不通"的问题,先检查安全组。
4. 多端口环境下的路径与访问排查手册
4.1 端口不通的三个排查层级
多端口方案上线后,最常见的故障就是"某个端口访问不了"。这类问题的排查思路,我习惯分成三个层级,按顺序来。
第一层:进程层面。先确认 nginx 是否真的监听了这个端口。
ss -tlnp | grep 8080如果没有任何输出,说明 nginx 根本没监听这个端口。可能原因:配置没生效(忘了 reload)、配置里 listen 写错了、或者这个端口被别的进程占用了。nginx -t加nginx -s reload先走一遍。
第二层:防火墙层面。进程在监听,但本机 curl 都通不过?
curl -v http://127.0.0.1:8080/如果 127.0.0.1 能通,但服务器公网 IP 访问不通,不是云安全组就是系统防火墙。查看系统防火墙规则:
firewall-cmd --list-ports云安全组的检查就只能登录云控制台去看了。
第三层:网络层面。本地通、服务器上通、但其他机器就是访问不了,那就要查路由和云厂商的网络策略。这种情况相对少见,真遇到了,结合telnet或nc探测一下:
telnet 你的服务器IP 8080如果连接超时,基本可以确定是网络策略拦截;如果连接被拒绝,说明端口没在监听或被防火墙挡了。按照这个层级一步步查,绝大多数问题 10 分钟内能定位。
4.2 403 与 404:静态站最常遇到的两个状态码
端口通了,但打开页面报 403 或者 404,这是多端口静态站点最常遇到的问题。
403 Forbidden 的常见原因有三类:
第一类是目录权限不够。nginx 默认以nginx用户运行,如果站点目录的属主不是你手动改过的某个用户,而nginx用户对该目录没有读权限,就会 403。检查目录权限:
ls -ld /opt/www/site1如果目录权限是drwxr-xr-x,属主是root,那nginx用户(属于nginx组)是可以读的。但如果你用了chmod 700,那就只有属主能访问,nginx用户直接 403。解决办法很简单,把目录属主改成nginx或调整权限为 755:
chown -R nginx:nginx /opt/www/site1第二类是目录下没有 index 文件。index index.html;配置了首页文件,但目录里实际没有index.html,nginx 又找不到可列目录的权限(默认禁止目录列表),于是返回 403。这个很好排查,去目录里ls一下就知道了。
第三类是SELinux 拦截。CentOS 上 SELinux 默认开启,nginx 对某些目录的访问可能被 SELinux 策略拦截。如果上面两个原因都排除了,试试临时关闭 SELinux:
setenforce 0再访问一次,如果好了说明就是 SELinux 的问题。要么永久关闭(/etc/selinux/config里将SELINUX=enforcing改为disabled),要么给目录打正确的 SELinux 标签。生产环境我建议打标签而不是静默关闭,毕竟安全机制还是有用的:
chcon -R -t httpd_sys_content_t /opt/www/site1404 的问题则多半是root路径配置不对。举个例子,你的文件实际在/opt/www/site1/index.html,配置里却写成了:
server { listen 8080; location / { root /opt/www/site1/static; # 路径多了一层 } }那 nginx 会去/opt/www/site1/static/index.html找,找不到自然返回 404。这种问题通过nginx -t是查不出来的,因为语法没问题,纯粹是路径写错了。最快的排查方法:
nginx -V 2>&1 | grep prefix # 查看 nginx 安装前缀以及打开错误日志:
tail -f /var/log/nginx/error.log错误日志里会明确写出open() "/opt/www/site1/static/index.html" failed (2: No such file or directory),这就是 404 的直接原因。看日志永远是排查路径问题的最优解。
4.3 proxy_pass 斜杠陷阱:一个斜杠引发的路径丢失
在多端口方案里,我们经常用反向代理把请求转发给后端。这里有一个高频坑:proxy_pass后面到底加不加斜杠,效果完全不同。
记住一个简化版的规则:
proxy_pass http://127.0.0.1:18082;(不带斜杠):请求路径原样转发,前端访问/api/user,后端收到的也是/api/user。proxy_pass http://127.0.0.1:18082/;(带斜杠):请求路径中匹配 location 的那一段被去掉,前端访问/api/user,后端收到的是/user。
举个例子,如果你后端接口定义的是@GetMapping("/user"),但你 nginx 配置的是:
location /api/ { proxy_pass http://127.0.0.1:18082/; }而前端发的请求是/api/user,那么 nginx 转发给后端的路径是/user,这刚好和后端接口定义一致——看起来"歪打正着"能用。但如果后端接口定义的是/api/user,这个配置就会 404,因为后端收到的路径变成了/user。
我见过最魔幻的连续踩坑是:运维觉得带斜杠是"官方推荐",于是把所有proxy_pass都加了斜杠。结果一个后端接口路径写的是/api/user,location 也是/api/,加斜杠后转发到后端的路径成了/user,后端返回 404,运维排查了半天,最后发现是斜杠的问题。
所以在实际工作中,我的建议是:
- 如果后端接口本身就带
/api这个前缀,就用不带斜杠的写法,原样透传。 - 如果后端接口的路径和前端路径不一致,比如前端发
/api/user,后端接口是/user,则用带斜杠的写法,去掉前缀再转发。
务必跟后端开发对齐接口路径的定义,不然这个斜杠问题就会周期性地折磨你。
4.4 多说几个隐蔽的坑
除了上面三类典型问题,多端口配置中还有几个比较隐蔽的坑,值得单独拿出来说。
第一个:include的文件名后缀问题。默认配置里写的是include /etc/nginx/conf.d/*.conf;,如果你新建的文件叫multi-site.conf.bak或者111,它不会被加载。很多人把文件从 Windows 传上来,后缀变成.conf.txt,结果 nginx 半天没反应,检查/etc/nginx/conf.d/目录下文件后缀才发现问题。一个朴素又有效的习惯:建配置文件名一律小写,后缀严格用.conf。
第二个:备份文件被 include 进去。与上面相反,如果你在conf.d目录下用cp multi-site.conf multi-site.conf.bak做备份,而这个目录的 include 是*通配(有些人改成include /etc/nginx/conf.d/*;,不带.conf后缀),那么.bak文件也会被加载。如果这个备份里监听了同样的端口,就会和正式配置冲突,reload 时直接报"duplicate listen port"。所以尽量把备份文件放到conf.d目录外,或者用vhost目录单独管理,避免误加载。
第三个:多 server 块同端口时,server_name匹配导致串站。如果你的配置中有两个 server 块都listen 8080,只是server_name不同,当通过 IP 访问时,nginx 会把请求交给default_server或第一个匹配的 server 块。这时候如果两个 server 块之间根路径一样但目录不同,就可能出现"打开 A 站点,显示的是 B 站点的内容"这种诡异现象。排查方法是在每个 server 块里加一个唯一的响应头标签:
add_header X-Server-Tag "site1" always;然后curl -I http://127.0.0.1:8080/看返回的头,就能确认请求到底落到哪个 server 块了。这个方法在多 server 环境排错时极其好用。
第四个:location 里不写结尾斜杠导致路径拼接错误。比如:
location /blog { root /opt/www; }访问/blog时,nginx 会拼出/opt/www/blog;但如果访问/blogabc,也会匹配这个 location,拼出/opt/www/blogabc。所以如果你的本意是只匹配/blog这一个目录,记得写成location /blog/,带上结尾斜杠,避免把其他路径也吞进来。这是一个很多人不会注意但实际影响很大的细节。
5. 多端口方案还能怎么扩展
多端口访问的配置骨架搭好之后,扩展性是非常强的。我在这里补充几个进阶用法,让这套方案能适应更多业务场景。
场景一:HTTPS 多端口。现在网站基本都要上 HTTPS。如果你的多端口方案要兼容 HTTPS,只需要在对应 server 块里加上证书配置:
server { listen 8443 ssl; server_name localhost; ssl_certificate /etc/nginx/ssl/example.crt; ssl_certificate_key /etc/nginx/ssl/example.key; root /opt/www/site1; index index.html; }配置和 80 端口几乎一样,只是listen加上了ssl参数,然后指定证书路径。注意:每个想启用 HTTPS 的端口,都需要单独指定证书;如果多个端口共用一套证书也是允许的。完备的证书配置还涉及ssl_protocols、ssl_ciphers等调优项,但基础配置就是上面这两行。
场景二:同端口多域名区分服务。如果你手头有域名,更推荐的方案是同一端口(比如 80 或 443)上通过server_name区分不同站点。这样外部访问更友好,不需要记端口号。配置非常直观:
server { listen 80; server_name site1.example.com; root /opt/www/site1; } server { listen 80; server_name site2.example.com; root /opt/www/site2; }这种方案和本文的多端口方案可以混用。比如我用 80 端口跑正式站点,用 8081 端口跑内部管理系统,就是"域名 + 端口"两手抓。
场景三:可视化配置工具。如果你觉得手写 nginx 配置还是麻烦,现在有一些开源的可视化工具可以辅助,比如 Nginx Proxy Manager、nginxWebUI 之类的。它们通常提供网页界面,图形化添加站点、反向代理、SSL 证书,底层自动生成配置并 reload。对新手非常友好,也适合管理站点数量较多的场景。不过我的建议是:先手写配置搞懂原理,再用可视化工具提效。因为工具生成的东西出了问题,你还是得回到配置文件去排查,不懂原理会非常被动。
这个方案后续扩展的思路也很多:比如给 8080 和 8081 分别配置访问日志,access_log各写各的文件;比如用upstream给 8082 这个端口背后挂多个后端实例做负载均衡;再比如通过limit_req对某个端口做请求速率限制,防止接口被刷。多端口的架构本质上就是"一个 server 块一个服务",在这个框架内你想做什么都很自然。
说到底,nginx 多端口配置本身并不难,难的是把它放进一个清晰、可维护的结构里。我这几年的实际体会就是:配置越规整,问题越少;每条配置都清楚为什么这么写,线上排障的时间就能缩短一大半。希望这篇文章能帮你在遇到多端口需求的时候,少走一些我当年走过的弯路。