1. 从一个真实需求说起为什么要在同一台机器上跑两个网站很多人第一次接触 Nginx 都是在一台服务器只放一个站的场景下装完 Nginx把静态文件往/usr/share/nginx/html一丢改改server_name重启收工。这种用法当然没问题但它掩盖了 Nginx 真正值钱的地方——反向代理和请求分发。我见过不少朋友服务器上明明跑着好几个服务却非要再开一台机器或者用不同的端口号硬扛访问的时候还得记:8080、:3000这种后缀体验非常割裂。这次要聊的小实验就是解决这个割裂感的在同一台机器、同一个 Nginx 进程里用两个不同的端口分别代理两个完全不同的网站。比如 8081 端口对外呈现的是一个博客站8082 端口对外呈现的是一个后台管理面板两者互不干扰各自有独立的根目录、独立的日志、独立的配置块。听起来简单但里面涉及 Nginx 的server块匹配逻辑、listen指令的写法、root与proxy_pass的取舍、日志隔离、以及一个特别容易被忽略的坑——默认 server 的兜底行为。这篇文章适合谁看如果你已经能启动 Nginx、能看懂最基本的nginx.conf但还没真正理解一个 Nginx 怎么同时伺候多个站点那这篇就是写给你的。我会从需求拆解开始把配置一块一块写出来解释每一行为什么这么写然后把我自己踩过的几个坑原原本本摆出来最后给一套可以直接抄的完整配置。全程不涉及任何敏感内容就是纯粹的 Web 服务配置实践。先说清楚这个实验的边界我们讨论的是同一台主机上的端口级分流不是基于域名的虚拟主机那需要 DNS 配合也不是基于路径的分流那是location的活儿。端口分流的好处是不依赖域名解析本地测试、内网环境、临时演示都能直接用http://ip:8081和http://ip:8082各走各的非常直观。2. 端口分流背后的匹配逻辑Nginx 到底怎么选 server 块2.1 listen 指令才是第一道门很多人以为 Nginx 选server块是先看server_name其实顺序搞反了。Nginx 处理一个请求时第一步是按listen指令匹配监听端口和地址只有在同一个监听地址端口上存在多个server块时才会进一步用server_name去区分。这个顺序非常关键因为它直接决定了我们这个双端口实验能不能成立。换句话说当你在配置里写server { listen 8081; server_name _; ... } server { listen 8082; server_name _; ... }这两个server块监听的是不同的端口所以 Nginx 在收到 8081 的请求时压根不会去看第二个块直接命中第一个。server_name在这里写什么都无所谓因为端口已经把请求隔离干净了。这就是端口分流比域名分流更傻瓜的原因——它不依赖任何名称匹配纯靠端口这个物理入口。2.2 默认 server 的兜底陷阱这里有个坑我必须提前说因为它坑过我。当你有多个server块监听同一个端口时Nginx 会把其中第一个按配置文件加载顺序作为该端口的默认 server。任何Host头匹配不上任何server_name的请求都会落到这个默认 server 上。那如果两个server块监听的是不同端口呢每个端口各自有自己的默认 server互不影响。这本来是好消息但问题出在你没显式声明默认 server 的时候。假设你只配了 8081 和 8082但服务器上还跑着别的服务占用了 80或者你后来加了个监听 80 的块忘了写default_server访问 80 端口时 Nginx 会返回它内置的默认页面而不是你期望的内容。排查的时候如果只盯着 8081/8082 看很容易懵。我的习惯是每个监听端口都显式指定一个default_server哪怕这个端口只有一个站点。写法是listen 8081 default_server;。这样做的代价几乎为零但能省掉未来加站点时的匹配混乱。2.3 root 和 proxy_pass 的分工这个实验里代理不同的网站有两种实现路径得先想清楚你要哪种方式指令适用场景特点静态托管root/alias网站是纯静态文件Nginx 直接读磁盘返回性能最高反向代理proxy_pass网站是后端应用Node、Python、Java 等Nginx 转发请求给后端可做负载均衡、缓存如果你的两个网站是两套静态页面用root指向不同目录就行如果是两个跑在不同本地端口的应用比如一个 3000 端口的 Node 服务、一个 5000 端口的 Python 服务那就用proxy_pass转发。实际项目里更常见的是混合一个端口托管静态站另一个端口反代后端应用。我下面的配置会两种都覆盖你可以按需取用。提示proxy_pass后面如果带路径比如http://127.0.0.1:3000/Nginx 会做路径替换如果不带路径http://127.0.0.1:3000则原样透传。这个细节在配子路径代理时特别容易出错后面第 4 节会专门讲。3. 动手搭两个端口、两套站点的完整配置3.1 目录结构与文件准备先把两个网站的物料准备好。我用两个静态目录来模拟这样不依赖任何后端语言谁都能复现sudo mkdir -p /var/www/site-a sudo mkdir -p /var/www/site-b sudo mkdir -p /var/log/nginx/site-a sudo mkdir -p /var/log/nginx/site-b echo h1Site A - 博客站/h1 | sudo tee /var/www/site-a/index.html echo h1Site B - 管理面板/h1 | sudo tee /var/www/site-b/index.html目录分开建是有讲究的。我见过有人图省事两个站共用一个root靠location区分结果一个站的 404 页面串到另一个站去了。物理隔离是最省心的隔离日志目录同理——分开之后排查问题时tail -f哪个文件一目了然不用在一堆混杂日志里捞针。3.2 主配置文件的组织方式Nginx 的配置有两种组织习惯全塞进nginx.conf或者用include拆成多个文件。我强烈推荐后者尤其是做多站点的时候。主流发行版的nginx.conf里通常已经有一行include /etc/nginx/conf.d/*.conf;这意味着你只要在conf.d/目录下放独立的.conf文件Nginx 启动时会自动加载。我们建两个文件sudo vim /etc/nginx/conf.d/site-a.conf sudo vim /etc/nginx/conf.d/site-b.conf这样每个站点的配置自包含改一个不影响另一个删站点直接删文件干净利落。比起在一个大文件里翻来翻去这种组织方式在站点数量超过三个之后优势极其明显。3.3 site-a.conf静态托管 独立日志server { listen 8081 default_server; server_name _; root /var/www/site-a; index index.html index.htm; access_log /var/log/nginx/site-a/access.log; error_log /var/log/nginx/site-a/error.log; location / { try_files $uri $uri/ 404; } }逐行拆解一下。listen 8081 default_server声明这个块独占 8081 端口并作为默认 serverserver_name _是个占位符表示不挑域名反正端口已经隔离了。root指向站点 A 的目录index指定默认首页。日志两行把访问日志和错误日志都引到独立目录这是排查问题的命脉。location /里的try_files $uri $uri/ 404是静态站的标准写法先找请求的精确文件找不到就找同名目录再找不到返回 404。不要省略这一行否则 Nginx 在某些情况下会返回目录列表或者行为不符合预期。404是显式兜底比默认行为更可控。3.4 site-b.conf反向代理到本地应用假设站点 B 其实是一个跑在127.0.0.1:3000的后端应用配置就换成代理模式server { listen 8082 default_server; server_name _; access_log /var/log/nginx/site-b/access.log; error_log /var/log/nginx/site-b/error.log; 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; } }这里几个proxy_set_header是反代的标配。Host透传让后端知道用户访问的是哪个域名X-Real-IP和X-Forwarded-For把真实客户端 IP 传给后端否则后端日志里全是127.0.0.1做访问统计或限流时直接抓瞎X-Forwarded-Proto告诉后端原始请求是 http 还是 https后端生成跳转链接时用得上。注意proxy_pass http://127.0.0.1:3000;结尾没有斜杠表示原样透传路径。如果你写成http://127.0.0.1:3000/那么访问/api/user会被改写成/user转发给后端路径前缀被吃掉了。这个斜杠的有无是新手最容易翻车的地方之一。3.5 校验与重载配置写完别急着重启先做语法检查sudo nginx -t输出syntax is ok和test is successful才算过。如果报错它会告诉你具体哪个文件哪一行照着改就行。检查通过后重载sudo nginx -s reload用reload而不是restart是因为 reload 是平滑重载——Nginx 会启动新的 worker 进程处理新配置等旧 worker 把手上请求处理完再退出不会中断正在进行的连接。生产环境里这个区别很重要restart 会瞬断reload 不会。然后浏览器访问http://你的IP:8081和http://你的IP:8082应该分别看到两个站点的内容。如果 8082 是反代记得先把后端应用在 3000 端口跑起来否则会看到 502。4. 那些让我熬夜的坑端口代理的排查实录4.1 502 Bad Gateway 的三种常见成因反代配置里 502 出现频率最高但成因不止一种得会分辨。第一种是后端根本没启动curl 127.0.0.1:3000直接连接拒绝这种情况去把后端拉起来就行。第二种是后端启动了但监听在127.0.0.1之外或之内不匹配——比如后端只监听::1IPv6 本地回环而 Nginx 用127.0.0.1IPv4去连就连不上。解决办法是把后端监听地址改成0.0.0.0或者127.0.0.1明确指定。第三种最隐蔽SELinux 拦截。在某些开启了 SELinux 的系统上Nginx 默认不允许发起网络连接需要执行setsebool -P httpd_can_network_connect 1放行。这个坑我卡了整整一个下午日志里只有一句含糊的权限拒绝最后靠ausearch才定位到。排查 502 的通用顺序是先curl后端确认它活着再nginx -t确认配置没错再看error.log里的具体报错最后才怀疑 SELinux 或防火墙。从近到远、从简到繁别一上来就怀疑系统层。4.2 端口被占用一个低级但高频的错误启动 Nginx 时报bind() to 0.0.0.0:8081 failed (98: Address already in use)说明端口被别的进程占了。用这条命令查是谁sudo ss -tlnp | grep :8081或者老一点的系统用netstat -tlnp | grep :8081。输出里会显示占用进程的 PID 和名字。如果是残留的 Nginx workerpkill nginx清掉再启如果是别的服务要么改它的端口要么改你的listen端口。我个人的习惯是给自建服务选端口时避开 8000、8080、3000 这些大众端口因为开发机上这些端口被各种工具占用的概率极高用 18081、18082 这种偏门号段能省很多事。4.3 日志串台为什么两个站的访问记录混在一起如果你偷懒没给每个server块单独配access_logNginx 会把所有请求都写进主配置里定义的那个默认日志文件。结果就是 8081 和 8082 的访问记录混在一起想统计某个站的 PV 时得靠端口号去 grep非常别扭。更麻烦的是错误日志。默认情况下所有server块共享同一个error_log当某个站出问题时你在一堆无关报错里找线索效率极低。所以我在 3.3 和 3.4 里坚持给每个站配独立的access_log和error_log这不是洁癖是实打实的排查效率。日志目录记得提前mkdir否则 Nginx 启动时会因为目录不存在而报错——这个错误信息还算友好会直接告诉你路径不存在。4.4 一个关于 default_server 的隐蔽冲突前面提过default_server这里说个具体的翻车场景。假设你后来想加第三个站监听 8081但忘了给原来的 site-a 去掉default_server于是两个块都声明自己是 8081 的默认 server。Nginx 在nginx -t时会直接报错nginx: [emerg] a duplicate default server for 0.0.0.0:8081这个报错很明确照着改就行。真正阴险的是不报错但行为诡异的情况比如你只在一个块里写了default_server另一个块没写那么当请求的Host匹配不上任何server_name时会落到那个默认块。如果你期望的是匹配不上就拒绝那就得显式配一个返回 444 的默认块。444 是 Nginx 特有的状态码表示直接关闭连接不返回任何响应用来对付扫描器和垃圾请求特别有效。5. 让这套配置更耐用的几个进阶调整5.1 用 upstream 给反代留扩展余地现在 site-b 是直接proxy_pass到一个固定地址。如果将来后端要扩成多实例直接改proxy_pass会很乱。更好的做法是提前用upstream定义后端池upstream site_b_backend { server 127.0.0.1:3000; # 将来加实例只需在这里加一行 # server 127.0.0.1:3001; } server { listen 8082 default_server; ... location / { proxy_pass http://site_b_backend; ... } }这样后端地址和 Nginx 的server块解耦扩容时只动upstream一处。哪怕现在只有一个后端提前这么写也不亏属于为未来省事的投资。upstream里还能配权重、健康检查、备份节点这些在单实例阶段用不上但结构先搭好后面加功能就是填空。5.2 静态资源的缓存与压缩站点 A 是静态站可以顺手把缓存和压缩打开效果立竿见影location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { expires 30d; add_header Cache-Control public, immutable; } gzip on; gzip_types text/plain text/css application/javascript application/json; gzip_min_length 1024;expires 30d让浏览器把图片、样式、脚本缓存 30 天回头客再访问时这些资源直接走本地缓存服务器压力骤降。gzip对文本类资源压缩比很高一个 100KB 的 JS 压完可能只剩 30KB。gzip_min_length 1024是避免压缩太小的文件——压缩本身也有开销小于 1KB 的文件压了反而得不偿失。注意gzip_types里不要加图片和视频它们本身已经是压缩格式再压一遍纯属浪费 CPU。5.3 访问控制给管理面板加把锁站点 B 如果是管理面板暴露在公网总让人不踏实。最轻量的保护是加 HTTP 基础认证sudo apt install apache2-utils # 或 yum install httpd-tools sudo htpasswd -c /etc/nginx/.htpasswd admin然后在 site-b 的location里加两行auth_basic Restricted; auth_basic_user_file /etc/nginx/.htpasswd;这样访问 8082 时会先弹一个登录框。基础认证的安全性依赖 HTTPS如果只是内网测试够用公网环境务必配合证书。另一种更彻底的做法是限制来源 IPallow 192.168.1.0/24; deny all;只允许内网网段访问其他一律拒绝。这两种方式可以叠加使用形成网络层 应用层的双重门槛。5.4 配置变更后的验证清单每次改完配置我固定走这套流程基本不会出岔子nginx -t检查语法不通过绝不 reload。nginx -s reload平滑重载。curl -I http://127.0.0.1:8081和curl -I http://127.0.0.1:8082看响应头状态码。tail -f两个站的error.log观察有没有新报错。浏览器实际访问一遍确认页面渲染正常、静态资源加载完整。这套流程看着繁琐但比改完直接重启然后线上炸了再回滚要省心得多。尤其是第 3 步的curl -I只取响应头不下载正文速度快能立刻看出状态码是 200、301 还是 502。6. 写在最后的一点个人体会这套双端口代理的配置我从第一次照着文档抄到后来能闭着眼睛写中间踩的坑基本都在这篇文章里了。最大的感受是Nginx 的配置不难难的是理解它的匹配顺序和默认行为。listen先于server_name、default_server的兜底、proxy_pass结尾斜杠的语义、日志的继承关系——这几个点吃透了百分之八十的诡异现象都能自己解释清楚。另外提一句端口分流虽然方便但它有个天然局限用户得记住端口号。如果这是给外部用户用的正式站点最终还是得回到基于域名的虚拟主机用 80/443 加server_name来区分。端口分流更适合内网服务、临时演示、开发调试这些场景。搞清楚工具的适用边界比会用工具本身更重要。如果你照着这篇配置跑通了不妨再往前一步把两个站换成一个静态 一个反代或者给反代加上upstream和健康检查感受一下 Nginx 从静态服务器进化成流量入口的过程。这个演进路径基本就是很多真实项目里 Web 层架构的缩影。