上周帮一个做前端的朋友排查线上问题时,他发来一张截图让我崩溃了半小时——页面能正常打开,但所有接口全部报404,静态资源却加载得飞起。他用的就是从某云服务器上直接解压前端包、然后跑http-server这种临时方案。这已经不是第一次有人问“我网站代码明明没问题,为什么部署上去就不对”这件事了。
每次看到这种问题,我都想说:真正决定一个网站“能不能正式上线”的,往往不是那几百行业务代码,而是你懂不懂 Web 技术里那些关于请求、响应、缓存、代理的基本规则,以及你有没有一套像 nginx 这样成熟、可控、可排查的部署载体。这篇文章就围绕“Web 技术与 nginx 网站部署”这条线,把从本地开发到公网可访问这一路该掌握的东西彻底捋一遍。适合刚接触部署的开发者、单干的前端同学,也适合那些想搞懂“为什么我的网站又打不开”的项目负责人。
1. 先搞清楚一件事:nginx 到底在你的网站里扮演什么角色
很多人在本地开发时很顺,一上线就懵,根子在于本地环境和线上环境的“入口逻辑”完全不同。本地你运行的是开发服务器,它知道你的项目结构,能给你热更新、代理接口、打日志;线上则是一个对外的、常驻的、要面对无数不确定请求的服务器进程在接客。nginx 就是这个“接客”的入口。
1.1 一个网站请求的完整旅程
假设你在浏览器输入了一个域名,回车之后发生了什么,我们按最小模型拆一下:
- 浏览器向 DNS 服务器询问:这个域名对应的服务器 IP 是什么。
- 拿到 IP 后,浏览器向该 IP 的 80 或 443 端口发起 HTTP 请求(HTTPS 就是 443,HTTP 是 80)。
- 请求到达服务器网卡,内核把数据包交给监听该端口的进程——这个进程就是 nginx 或者别的 Web 服务器。
- nginx 根据请求头里的 Host、URI、以及自己的配置文件,决定如何处理这次请求:是返回磁盘上的某个静态文件,还是转发给后端的业务服务(比如 Java、Go、Node 进程)。
- 提供响应内容后,nginx 再把结果返回给浏览器,完成一次闭环。
从这个流程能看出一个关键点:nginx 是请求的“路由器”和“分发员”,它本身不执行业务逻辑。它更像写字楼大堂的前台,访客来了先问去哪层、找谁,再由前台引导;而真正干活的人是各个办公室里的员工,对应你运行在后端的业务进程。
1.2 为什么偏要用 nginx,而不是让 Node 或 Java 直接监听 80 端口
很多人会问:我的应用本身就能监听端口返回页面,为什么还要在它前面放一个 nginx?这个问题每被问一次,我就觉得一些开发者对 Web 基础设施的理解还是有断层。
第一,静态资源处理效率差距巨大。纯静态文件(HTML、CSS、JS、图片、字体)的返回,nginx 用了高效的事件驱动模型,配合 sendfile、epoll 这些系统级优化,几万并发连接下依然能保持很低的内存占用。如果你让 Node 进程自己返回静态文件,也不是不行,但每来一次请求都要走一遍应用逻辑、业务框架、日志记录,资源的丈量完全不在一个量级。就好比让公司研发总监去前台给访客登记身份证,能干活,但浪费。
第二,多站点复用与端口隔离。一台服务器上可以同时跑多个网站、多个后端服务,nginx 可以按域名把流量分给不同站点,而它们互不干扰。业务进程只需监听内网端口(比如 127.0.0.1:8080),对外统一走 nginx 的 80/443。这样降低了攻击暴露面,也会让证书管理、日志管理收敛到一处。
第三,成熟的兜底能力。请求超时重试、缓存、压缩、限流、访问控制、灰度分流,这些都是 nginx 自带的基础能力。你在业务代码里造轮子处理这些,等于在重新发明一套并不比它做得更好的基础设施。
1.3 部署环节最常见的“崩盘点”为什么是配置
实话说,nginx 本身在绝大多数发行版里都能稳定跑几个月甚至几年不出事。真正让网站挂掉的,几乎都是配置问题:root 路径拼错、location 匹配顺序没搞明白、代理目标端口写错、缓存策略让用户拿到旧文件、HTTPS 证书没配全导致网页出现混合内容警告。部署是一门“经验学”,不是你会不会启动 nginx,而是你能不能控制它按你预想的方式处理每类请求。本文后面给的每套配置,我都会讲清它每个字段的含义和适用范围,拒绝“抄了能用但改了就挂”的黑盒式复制。
2. 环境准备:把 nginx 从安装到“能自我检查”彻底搞定
在写配置之前,先把底子打牢。很多网上教程上来就丢配置,最后你连 nginx 装在哪、日志在哪、怎么查错都不知道,那部署就是“碰运气”。
2.1 安装方式与版本选择
我常用的系统是 Debian/Ubuntu 系列,CentOS/RHEL 系列下面差异点我会专门标出来。安装命今如下,二选一:
# Debian / Ubuntu sudo apt update sudo apt install -y nginx # CentOS / Rocky / AlmaLinux sudo yum install -y nginx装完之后先不要急着改配置,先验证它是否正常工作:
sudo systemctl status nginx sudo systemctl enable --now nginx sudo ss -tlnp | grep -E ':(80|443)'能看到 80 端口有 nginx 在监听,基本就成功了。浏览器访问服务器公网 IP,看到欢迎页说明 Web 服务已经通。
关于版本,我建议不要追求太老或太奇特的版本。多数发行版仓库自带的 nginx 就能用,如果你的业务需要 HTTP/3、更精细的流控,再考虑维护官方源获取新版本。新版本对 HTTP/2、gzip、Brotli 的支持更完整,配置语法兼容性也稳定。
提示:如果
ss命令找不到,先安装iproute2。这一步会帮你快速确认端口占用情况,是线上排错的基础技能。
2.2 目录结构、配置入口与进程模型
nginx 的核心目录结构,无论哪个发行版,心智模型都是一致的:
/etc/nginx/nginx.conf:主配置文件,定义全局参数和 HTTP 块。/etc/nginx/conf.d/:主配置里自动加载的子配置目录,常用来放站点级配置。/etc/nginx/sites-available/与/etc/nginx/sites-enabled/:Debian 系列的站点管理惯例,前者放可用配置,后者做软链启用。/usr/share/nginx/html/:默认静态站点目录。/var/log/nginx/access.log与error.log:访问日志与错误日志,这两个是你日后排错的第一现场。
nginx 的常见进程模型是:一个 master 主进程负责读取配置、管理工作进程;多个 worker 进程负责真正处理连接。配置里你经常看到这两行:
worker_processes auto; worker_connections 1024;worker_processes auto表示按 CPU 核心数自动创建 worker,一般可直接用 auto。worker_connections是单个 worker 能同时打开的最大连接数,1024 是保守值,高并发场景调高到 4096 或更高之前,记得同步调整系统级文件描述符上限ulimit -n,否则配置再高也受限。
2.3 每次改配置必做的一步:语法自检与优雅重载
在 nginx 里有一个被我视为“保命键”的命令:
sudo nginx -t它会对配置做语法检查,若输出syntax is ok和test is successful,再执行:
sudo systemctl reload nginxreload和restart的区别很重要:reload 不会中断现有连接,master 进程先检查新配置,确认无误后启动新的 worker,再平滑地切换过去。生产环境一律用 reload,不要随意 restart。这个习惯养好了,你基本告别“改个配置网站就闪断一下”的尴尬。
2.4 新手上路时最容易忽略的默认站点干扰
Debian/Ubuntu 装完 nginx 之后,默认会有一个default站点配置监听 80 端口。如果你自己新建的 server 块和它撞了端口、或者 Host 匹配规则不明确,访问时仍可能落到默认页上。我建议所有环境在开始自定义前,先明确这个默认站点的存在,要么编辑它,要么在写完自己的配置后用nginx -t检查端口冲突。从长期维护角度看,我习惯把默认配置里的 server 块注释掉,或者直接把listen 80 default_server从它身上摘掉,让流量完全按自己写的站点规则走。
3. 三种典型部署方案:从静态站到前后端分离逐个拆开
你的网站到底是什么形态,决定了 nginx 配置的核心长什么样。我把最常见的三种场景按从简单到复杂的顺序拆一遍,每个方案都会给出完整配置和关键字段的解释。
3.1 方案一:纯静态网站部署
如果一个网站就是一堆 HTML、CSS、JS 文件,没有任何后端接口,那部署精神就是“让 nginx 把这些文件干净快速地送出去”。
第一步,把项目文件放到服务器某个固定目录下,我常用/var/www/作为父目录。假设项目名是demo-site,上传后完整路径就是/var/www/demo-site/。
第二步,准备站点配置。在/etc/nginx/conf.d/下新建demo-site.conf:
server { listen 80; server_name demo.example.com; root /var/www/demo-site; index index.html; location / { try_files $uri $uri/ =404; } access_log /var/log/nginx/demo-site.access.log; error_log /var/log/nginx/demo-site.error.log; }这段配置里,最重要的联动是root和location。try_files $uri $uri/ =404的含义是:收到请求后,先用请求路径去找对应的文件,找不到就试着把路径当目录找目录下的默认索引文件,还找不到就返回 404。
这里要特别提醒:root 和 alias 千万别搞混,这在网上是被问烂了的点。root 是“根目录 + 完整路径拼接”,比如上面配置访问/css/app.css,nginx 实际找的是/var/www/demo-site/css/app.css;如果用的是alias /var/www/demo-site/assets/,那访问/css/app.css时会找/var/www/demo-site/assets/app.css。每个 location 使用 root 时都会重新拼接,初学者最容易在这里踩到路径 404。
第三步,校验并启用:
sudo nginx -t sudo systemctl reload nginx把demo.example.com替换成你自己的域名,并在 DNS 解析里把该域名指向服务器 IP,即可访问。
纯静态场景的优化点集中在两个方向:
- 开启 gzip 压缩:文本类资源传输体积能小 60% 以上。
- 给带哈希的文件设置长缓存:比如
app.d3a2f1.css这种文件名里带版本指纹的资源可以缓存一年,而index.html永远不缓存或只缓存极短时间,这样发布新版本时浏览器能第一时间拿到新的入口文件,再去拉新的哈希资源。
location /assets/ { expires 30d; add_header Cache-Control "public, immutable"; }3.2 方案二:SPA 单页应用的 history 路由与缓存坑
现在很多项目是 Vue、React 或其它框架做的前后端分离应用。前端打包出来同样是静态文件,但多了一个核心问题:前端路由是 history 模式(比如/user/123这种无#的路径)时,nginx 怎么处理刷新和直接访问。
如果 nginx 配置成和方案一一模一样,用户访问/user/123时,nginx 会去找/var/www/demo-site/user/123这个文件,结果根本不存在,于是返回 404。这就是大量单页应用部署后“从首页跳转没事,一刷新就白屏或 404”的根源。
解决办法是把所有路径都 fallback 到index.html:
server { listen 80; server_name spa.example.com; root /var/www/spa-site/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /assets/ { expires 30d; add_header Cache-Control "public, immutable"; } location = /index.html { add_header Cache-Control "no-cache"; } }location = /index.html这种精确匹配很关键:只有入口文件不缓存,其它哈希文件长缓存。否则会出现一种经典事故:你上午发版,用户下午还在跑旧代码,因为浏览器缓存了旧的 index.html,永远拉不到新的 JS CSS。
另外再补一句:如果是前后端分离且和后端接口同域部署,你的 location 还需要按/api/前缀做区分,这个放到方案三里展开。
3.3 方案三:反向代理到后端服务,并顺手解决跨域
当你的网站有 Java、Go、Node 等后端进程时,nginx 的核心角色变成了反向代理。后端服务通常监听内网端口(例如127.0.0.1:8080),对外由 nginx 统一入口。
一个标准的代理配置:
upstream backend { server 127.0.0.1:8080; keepalive 32; } server { listen 80; server_name app.example.com; root /var/www/app-front/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend; 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 末尾要不要带斜杠。
# 情形 A:不带斜杠,转发后路径保持不变 # 请求 /api/user/1 # 转发到后端 http://backend/api/user/1 # 情形 B:带斜杠,/api/ 前缀会被剥掉 # 请求 /api/user/1 # 转发到后端 http://backend/user/1我见过无数配置因为这两个差异导致后端一直 404。绝大多数情况下,如果你的后端接口设计是统一/api前缀的,用情形 A(不带斜杠);如果你的后端本身不认/api前缀,需要在网关层去掉它,就用情形 B(带斜杠)。选择哪种,取决于你后端拆分的服务是否统一带了这个前缀。这是前后端协作时最容易出现“本地能调通、线上全 404”的原因之一。
再往下,是代理时常见的粘性参数:
proxy_set_header Host $host:让后端知道原始请求的域名,否则它会误以为访问它的域名是127.0.0.1。X-Real-IP与X-Forwarded-For:让后端能从代理后面拿到用户真实 IP,否则日志里的 IP 全是 nginx 这台机器的 IP,做限流和审计时会很被动。X-Forwarded-Proto $scheme:告诉后端用户当时是 HTTP 还是 HTTPS 访问,不然后端生成的跳转链接会错误地变成 HTTP。
如果你的后端是 Node 的 WebSocket 服务,代理 server 块的 HTTP/1.1 升级也要显式写出:
location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 60s; }忘记配Upgrade头是 WebSocket 连接一建立就断开的一大原因。这条被问到的概率非常高。
4. 上线前的最后一步:域名、HTTPS 与那些躲不开的经典报错
配置能跑通、页面能打开,这只是“能访问”。真正作为正式站点对外提供服务,你还需要把基础设施补齐,并且知道线上出问题时该怎么定位。
4.1 用 HTTPS 证书把站点变成可信任状态
现在的主流浏览器对非 HTTPS 站点的态度越来越严格,凡是涉及登录、支付、表单的页面,不配 HTTPS 基本等于劝退用户。获取证书这件事现在非常成熟,主流云平台都能提供免费证书,也可以用自动化方式申请并自动续期。这里我不展开某个具体平台的操作,只讲 nginx 侧的配置逻辑。
拿到证书文件之后(一般包含证书公钥文件和私钥文件),HTTPS 的 server 块长这样:
server { listen 443 ssl; http2 on; server_name app.example.com; ssl_certificate /etc/nginx/ssl/app.example.com.pem; ssl_certificate_key /etc/nginx/ssl/app.example.com.key; root /var/www/app-front/dist; index index.html; location / { try_files $uri $uri/ /index.html; } } server { listen 80; server_name app.example.com; return 301 https://$host$request_uri; }第二个 server 块的作用是把所有 HTTP 流量强制 301 跳到 HTTPS,避免用户走明文入口。
关于证书,我想强调四件琐碎但致命的事:
- 证书到期不是一个“过几天再说”的事,它会直接造成大面积访问故障,浏览器会直接拦截。一定要做自动续期和到期提醒。
- 证书文件权限要收严,私钥文件建议设为
600,属主为 root,不要给 nginx worker 之外的人留可读权限。 - 证书链不完整会导致部分浏览器报错,只放叶子证书是不够的,要把中间证书一并合并进去,运营商提供的下载包里通常有对应文件。
- HTTP 301 跳转时不要过度放大招,例如把根域名和子域名规则混在一起,导致用户访问
www子域时跳到别的站,这类情况常出现在从一台服务器迁移到另一台时。
4.2 三类高频报错:404、403、502 的现场定位思路
线上报错并不可怕,可怕的是没有一套定位链路。我列一下这三类高频错误背后最常见的 nginx 侧原因。
404 Not Found:这个最常见的场景是路径拼接问题。我接到过最多的排查电话,最后都指向同一件事——前端资源请求路径和 nginx root 目录的层级对不上。排查顺序是:先curl -I http://127.0.0.1看返回头,再从error.log里看 nginx 实际去找的完整文件路径,它会写得很明白,例如/var/www/demo-site/user/123。看到实际路径,对比项目目录,就基本明白是哪一层多拼了或写漏了。
403 Forbidden:一般是权限或索引权限问题。nginx worker 进程用户(Debian/Ubuntu 下通常是www-data)对站点目录没有执行权限时会 403。目录权限至少要755(拥有者可写可执行,组和其他用户可读可执行),文件至少644。另一个场景是autoindex off时访问了一个没有 index 文件的目录,这时候把index index.html;配好即可。
502 Bad Gateway:这是反向代理场景最高频的报错。nginx 本质上不是出错方,它是在告诉你:“我按配置把请求转给了上游,但上游没有正常响应。”上游进程崩了、端口监听错了、防火墙拦截了内网端口、或者 upstream 配置的 IP 和端口对不上,都会导致 502。排查链路是:
# 1. 确认后端进程是否还活着 ps aux | grep java # 或者 systemctl status your-service # 2. 确认端口是否在监听 ss -tlnp | grep 8080 # 3. 绕过 nginx 直接请求后端 curl http://127.0.0.1:8080/health # 4. 再看 nginx error.log sudo tail -50 /var/log/nginx/error.log如果这四步走完,问题基本能锁定在“后端没起来”、“端口写错”或“后端自身启动失败”三个范围内。很多新手一遇到 502 就疯狂改 nginx 配置,其实只要先curl 127.0.0.1:8080一下,半分钟就知道方向了。
4.3 日志切割与最基础的存活检查
生产环境跑一段时间后,访问日志会涨得非常快。默认的 access.log 如果不做切割,几个月就能把磁盘怼满。Linux 的logrotate是常用的轮转方案,以下配置可以放在/etc/logrotate.d/nginx:
/var/log/nginx/*.log { daily missingok rotate 14 compress delaycompress notifempty create 640 www-data adm sharedscripts postrotate if [ -f /var/run/nginx.pid ]; then kill -USR1 `cat /var/run/nginx.pid` fi endscript }这个配置的核心点是:切完日志后要给 nginx 发USR1信号,让它重新打开新的日志文件,否则 nginx 还会继续往已经被改名的旧文件里写,磁盘空间照样被吃满。
基础存活检查我建议用一条命令搞定:
curl -s -o /dev/null -w "%{http_code}" https://app.example.com/health返回200就说明入口通。也可以配到监控脚本里,每几分钟打一次,异常时再人工介入。这些看着琐碎,但就是这些琐碎的动作,决定了你的网站在无人值守时能不能一直“稳着”。
5. 复盘几次真实踩坑:那些报错日志背后的真相
部署这件事,真正涨经验的地方在“踩坑—定位—修复”的循环里。最后分享几个我反复遇到过、而且特别有代表性的复盘,希望能给你省掉几晚上熬夜。
5.1 一次 502 的全链路定位:日志里那句 connect() failed 的含金量
有一次帮某团队排查线上服务间歇性 502,error.log 里刷满了类似下面的内容:
connect() failed (111: Connection refused) while connecting to upstream第一反应是后端挂了,但在服务器上curl 127.0.0.1:8080却是正常的。再看 upstream 配置,发现代理目标写的是127.0.0.1:8090,而后端实际监听 8080。为什么会这样?因为配置文件是从旧的部署文档复制的,端口早就改过但文档没同步。这个教训告诉我一件事:nginx 报错日志是在替你说出真相,connect() failed后面括号里的 IP 和端口,就是 nginx 实际去连接的目标。遇到类似报错,第一眼不要看业务代码,先仔细读日志里解析出来的 upstream 地址,然后对比当前真实监听端口。这种低级但极具迷惑性的错误,靠肉眼检查配置往往找不出来,必须靠日志里的实打实信息去定位。
5.2 权限问题的排查链路:“目录明明存在为什么 403”
还有一次,静态文件全部就位,路径也没问题,但访问任何页面都 403。我登录服务器一看,/var/www/demo-site的所有者是 root,权限是700。问题就在这里:nginx worker 进程是以www-data用户运行的,它没有权限进入700的目录。
解决办法也很简单:
sudo chown -R www-data:www-data /var/www/demo-site sudo chmod -R 755 /var/www/demo-site但这里我想说的是排查顺序。遇到 403,先别急着改权限,应该先确认:
- 访问的目标文件在不在?是不是 index 文件缺失?
- nginx worker 用户是谁?可以用
ps aux | grep nginx看到。 - 走到目录的每一级,是否有执行权限?
namei -l /var/www/demo-site/index.html会列出每一层的权限,一步到位。
namei这个命令在权限排查时几乎是无敌的,它会告诉你从根目录到目标文件的每一级路径权限状态。很多时候你以为目录权限没问题,其实中间某一层是 750,导致用户进不去,namei能把这些隐藏问题暴露得干干净净。
5.3 配置改完不生效的锅,最后都是自己买单
最后一种“神坑”恐怕每个人都遇到过:明明改了配置、reload 了,但行为没变化。这时候先别怀疑 nginx 没生效,按这个顺序自查:
- 改的是不是正在被加载的配置。Debian 系里
sites-enabled里软链失效是一件很安静的事,配置看起来改了,实际加载的还是另一份。 - reload 是否真的成功。
systemctl reload nginx如果失败,systemd 会直接把失败原因打到 stderr 里;如果只是nginx -s reload,失败时不会有明显提示,务必看nginx -t先行验证。 - 有没有多个 server 块在打架。如果两个 server 块
server_name同时匹配了同一请求,nginx 会按匹配优先级处理,这时候你改的可能根本不是生效的那个块。用nginx -T可以把当前生效的全部配置打印出来,逐个确认。 - 浏览器缓存。你改了 nginx 缓存头,但浏览器还在用本地缓存。测试时开无痕窗口或者加个时间戳参数,能排除掉“人类错觉”因素。
我后来养成的习惯是:每次改配置都执行三个命令连招——nginx -t验证语法、nginx -T | grep server_name确认真实加载的 server 块、systemctl reload nginx平滑重载。三连招走完,基本可以把“改不生效”的概率压到最低。
还有一个小技巧是调试期把错误日志级别临时调到debug,定位完再调回error。error_log /var/log/nginx/error.log debug;会输出每个请求的详细处理过程,包括它最终匹配到哪个 server、哪个 location、try_files 走了哪些路径。很多棘手的匹配问题,在 debug 日志面前都是透明的。但要注意生产环境不要长期开 debug,日志量会大到你怀疑人生。
部署网站这件事,说到底就是把 Web 技术里的“请求与响应模型”、DNS、端口、进程、权限这些基础概念串起来,再交给 nginx 这个可靠的载体去执行。你觉得它玄乎,是因为还没见过它把请求一步步掰开的样子;等你把错误日志当成第一信源,把 root、alias、proxy_pass 的差异刻进脑子,再遇到任何部署问题,都会变成一次有迹可循的推理题。