入坑 Nginx,看这一篇就够了

0 阅读12分钟

头图

文章的 TOC 太多了,看起来不方便?PC 端可以看看我的 《两个 TamperMonkey 脚本,解决掘金阅读体验的三点不爽》,可以解决一些掘金阅读体验问题。

我写的「入坑」系列文章:

  1. 《入坑 Mac,看这一篇就够了》
  2. 《入坑 iTerm + OMZ,看这一篇就够了》
  3. 《入坑 Firefox Developer Edition 及 Mobile 版,看这一篇就够了》
  4. 《入坑 WebStorm,看这一篇就够了》
  5. 《入坑 VSCode,看这一篇就够了》
  6. 《入坑 Vim,看这一篇就够了》
  7. 《入坑 Git,看这一篇就够了》
  8. 《入坑 Docusaurus,看这一篇就够了》
  9. 《入坑 Nginx,看这一篇就够了》← 本文

🎙️ 前言

你一定知道 Nginx,那么你「会用」么?说来惭愧,我不太会。这大概是由于之前的公司,此类事情一般都有专门的人负责。

然所谓「技多不压身」,无论你是前端后端,得对 Nginx 有基础的了解,不论是平常开发,还是希望把自己的项目快速落地,都可能用得着。

TL;DR

本文是前端向的 Nginx 入门文章。介绍如何安装、以及如何通过命令行启停、重载、配置检查等。通过不同的实战配置:静态托管、CDN 代理等,帮助开发者快速上手日常开发与部署必备的 Nginx 的基础知识和常用技巧。

主要内容

适合读者

  • 有一定的命令行基础
  • 想初步了解 Nginx 的同学
  • 想知道 Nginx 怎么配置静态托管、CDN 代理等技巧的同学

你将学到

  • Nginx 的正确读音
  • Nginx 的常用命令(以及如何用 systemctlbrew 命令操作 Nginx)
  • 常规的 Nginx 和使用 brew 安装的有什么区别
  • Nginx 常用代理模式,静态托管和 CDN 代理

编辑历史

日期版本说明
2023/08/02V1

😴 认识 Nginx

读音

你或许需要重新认识下 Nginx,先从怎么发音开始吧。估计 10 个里面有 9.5 个人都会把 Nginx 的名字念错。你是不是一直这么读:「恩金克斯」?You say wrong...正确地读法是把「X」念全了:「恩金·埃克斯」。所以,其实应该这么写:NginX。

Nginx 官网 的第一句话就是教你如何正确地发音(然后才是它是什么):

nginx ("engine x") is an HTTP web server, reverse proxy, content cache, load balancer, TCP/UDP proxy server, and mail proxy server.

核心功能

  • Web 服务器:托管网站、静态文件
  • 反向代理:这是 Nginx 最核心的用途(也是你听到最多的关联词)
  • 负载均衡:把流量均匀分给多台后端服务器
  • 网关能力:HTTPS 证书、缓存、限流、URL 重写、Gzip 压缩、长连接优化等

核心优势

  • 轻量,占用资源(内存 / CPU)极低
  • 高并发,单机轻松扛 1 万+ 并发
  • 稳定可靠
  • 配置简单
  • 扩展性强

新手必看

Nginx 的官网…简陋得不像这东西还活着一样,关键是很多对新手有用的链接都藏得很深,放这里以快速参考:

Nginx、Apache、Tomcat、Jetty 这几个兄弟的官网都差不多简陋,难道长得丑,活得久?

📦 安装

一般情况呢,你的 Linux 服务器上一定已经装好了 nginx。线上的机器,不论测试还是生产,都不可亵玩焉,作为练手使用,建议在自己电脑上装一个。

但估计很多人会在这一步就被劝退…官网的 安装文档,不能说并没太多新手友好的东西,只能说跟没写没啥区别。

用 Mac 和 Ubuntu 的同学相对比较简便:

  • Mac - 推荐使用 brew 安装 brew install nginx
  • Linux - Nginx 对大多数耳熟能详的 Linux 发行版 都有支持
  • Windows - 下载 后解压
  • Docker - 我简单玩了一下,结论是「用来练手不合适」

Linux 中以对新手友好的 Ubuntu 为例,执行以下命令(不用 sudo 会报错):

sudo apt update           # 以避免 `Invalid operation install` 的错误
sudo apt install nginx

若你还是想亲手试试 Docker 的方式:

docker pull nginx:latest
docker pull docker.1ms.run/nginx:latest # 上面这个可能因为神秘力量不能拉取,试试这个

以下,我将记录在 Mac 下通过 brew 安装和使用 Nginx。

Brew 安装

安装一如既往的简单,执行 brew install nginx,等一会儿就好了:

注意最后的警告(Caveats)部分:

  1. Docroot 是 /opt/homebrew/var/www,而不是默认的 /var/www
  2. 端口是 8080,而不是默认的 80
  3. 子配置目录是 servers,而不是默认的 /etc/nginx/conf.d/*.conf

以上 2 和 3 可以在 /opt/homebrew/etc/nginx/nginx.conf 找到:

http {
  ...
  
  server {
    listen 8080;
    ...
  }
  
  ...
  include servers/*; # 建议 * 改成 *.conf
}

建议把 include servers/*;改成 include servers/*.conf;,这样,往里面丢 SSL 的 keypem 文件就不会导致出错。

以下是安装后的 /opt/homebrew/etc/nginx 下的文件概览:

启停

你有两种方式启停 Nginx:

启动停止
nginxnginxnginx -s stop
brewbrew services start nginxbrew services stop nginx

无论你用以上哪种方式启动,访问 http://127.0.0.1:8080,你会看到一个很简陋的页面,说明启动成功:

上面这个 HTML 对应的文件在 /opt/homebrew/var/www/index.html

需要注意的是,如果使用 nginx 启动,不能通过 brew 停止(因为 brew services 感知不到);而使用 brew 启动,还是可以通过 nginx 进行停止。但!建议不要混着来,否则就会想以下这样,碰到「Bootstrap failed: 5」的启动错误(当然,解决的办法也简单,就是执行 brew services reload,但我曾经因此困惑而重装过…)。

因此建议只用 brew 启停,就像在服务器上只用 systemctl 启停一样。

路径差异

brew 安装,跟在服务器上(或者在 Ubuntu 是安装)的 Nginx,除了之前提到的配置目录外,还有一些别的路径区别,一般都是在默认的前面拼上 /op/homebrew,以下是常用的路径:

路径常规brew
Bin 文件/usr/sbin/nginx/opt/homebrew/bin/nginx
安装目录/etc/nginx/opt/homebrew/Cellar/nginx/{version}
配置文件/etc/nginx/nginx.conf/opt/homebrew/etc/nginx/nginx.conf
配置目录/etc/nginx/conf.d//opt/homebrew/etc/nginx/servers/
日志目录/var/log/nginx/opt/homebrew/var/log/nginx
静态文件/var/www/opt/homebrew/var/www

常用命令对比

之前我们已经知道,可以通过 brew services 启停 Nginx,而在服务器上你会被告知最好使用 systemctl 进行操作。下表整理了跟 Nginx 有关的常用命令:

操作原生命令(跨系统)brewsystemctl
启动nginxbrew services start nginxsystemctl start nginx
停止nginx -s stop / nginx -s quitbrew services stop nginxsystemctl stop nginx
重启nginx -s stop && nginxbrew services restart nginxsystemctl restart nginx
重载配置nginx -s reload-systemctl reload nginx
检查配置nginx -t / nginx -T--
查看状态无(需用 ps aux | grep nginx 查看进程)brew services info nginxsystemctl status nginx
一些说明:
  • 停止:stop VS quitnginx -s stop 是「暴力停止」,立刻终止所有进程,会中断业务;nginx -s quit 是「优雅停止」,等所有请求处理完再退出,无业务中断;生产环境请用 quit
  • 重启:使用 brew/systemctlnginx -s stop && nginx 多了服务状态校验
  • 检查配置 -t VS -T-t 只讲对错,输出简洁,-T 还会显示详细的配置内容;一般情况下用 -t 即可
  • 虽然 brew services 文档中并没有 reload,但你可以执行 brew services reload nginx,其效果其实跟 restart 是一样的

其他 brew 命令

除了以上常用命令中提到的,使用 brew,跟 Nginx 相关的,还有这些命令:

  • brew services list 查看由 brew 管理的所有服务,可以看 nginx 是否在运行
  • brew info nginx 查看 nginx 版本等基础信息
  • brew upgrade nginx 升级
  • brew outdated nginx 是否需要升级
  • brew uninstall nginx 卸载
  • brew reinstall nginx 重装

🚀 配置实战

我们跳过枯燥晦涩的配置理论知识,直接进入实战。以下我会以在实际服务器上为前提进行操作,而非本地的 brew nginx。

通常情况下,我们很少会去关注或修改 nginx.conf(最主要的配置文件),只需要往子配置目录 conf.d(或 brew 的 servers) 里丢 域名.conf 就行了。

接下来,假设现在我有个前端项目,它可能是一个 Vite 项目,也可能是一个 SSG 项目。拿之前我讲过的 SSG 框架 Docusaurus 的本地项目 documentation 作例子。

我现在将逐步将其部署到某个域名。

关于域名

首先,我们需要一个域名。域名能否被 Nginx 接住,主要看域名解析 DNS 是否能命中 Nginx 所在的机器。

如果你已经有了一个主域名,最简单的就是给它添加一个二级域名,比如 doc.company.com。在阿里云「云解析 DNS」控制台,就可以快速添加一个子域名(不花钱),只需要让子域名指向 Nginx 服务器所在的 IP 即可:

现在我们只是本地部署看看效果,只需要增加一条 Host 记录,比如:

127.0.0.1		doc.test

静态托管

静态托管的方式,需要把项目构建产物上传到服务器对应的目录 /var/www/doc,然后添加 conf.d/doc.<company-domain>.conf

本地试验,新增 /opt/homebrew/etc/nginx/servers/doc.test.conf

server {
  listen 80;
  server_name doc.test;
  root /....../documentation/build; # 这里直接指导项目的构建目录,省去拷贝

  # 处理根路径请求
  location / {
    try_files $uri $uri/ /index.html; # 支持单页面应用路由
  }

  # 静态资源缓存配置(可选但推荐)
  location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|txt|map|json)$ {
    expires 30d;
    add_header Cache-Control "public";
    access_log off;
  }

  # 错误页面配置
  error_page 404 /404.html;
  location = /404.html {
    internal;
  }
}

如果是以这种方式直接应用于现实中的服务器的话,需要注意 root 并不是相对于 /var/www 的相对路径,需要写绝对路径,Nginx 并没有此类约定。你或许看到 nginx.confroot html 而产生此类想法,但这是 Nginx 内置的约定,html 目录固定在配置前缀下(如 /usr/share/nginx/html),属于标准化的相对路径用法。

另外就是,这种部署方式会很麻烦——你得会往服务器上传目录的资格和技巧。幸运的是,你可以利用类似 Transmit 的 SFTP 工具进行操作,这里推荐你使用 rsync,一个原生的 Linux 命令,Mac 下也支持。

rsync -rvz --delete 本地目录/ 用户@服务器:/var/www/doc/
  • -r 保留目录结构
  • -v 详细输出
  • -z 传输时压缩,加快上传速度
  • --delete rsync 默认增量更新,虽然不成问题,但会生产垃圾文件,此参数可以先删除再上传
  • 本地目录/ 末尾带 / 表示同步目录内的所有内容,而非目录本身
  • /var/www/doc/ 末尾带 / 确保内容同步到目标目录下,不存在时自动创建

CDN 代理

以上说到直接去服务器上部署文件,虽然简单粗暴,但却相当麻烦,还将占用服务器的存储资源。

假设现在,我已经把前端资源(包括一个 index.html)发布到了 CDN https://somecdn.com/documentation/1.0.0/,可以把 Nginx 的配置也改成 CDN 代理的方式:

server {
  listen 80;
  server_name doc.test;
  
  location / {
    proxy_ssl_server_name on;
    proxy_ssl_protocols TLSv1.2 TLSv1.3;
    proxy_pass https://somecdn.com/documentation/1.0.0/; # 注意必须以 / 结尾,否则 404
    proxy_set_header Host somecdn.com;
    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 Accept-Encoding "";
    proxy_redirect off;
    proxy_intercept_errors on;
    
    proxy_buffering on;
    proxy_buffer_size 4k;
    proxy_buffers 8 4k;
    proxy_busy_buffers_size 8k;
    
    gzip off; # gzip,避免重复压缩
    
    error_page 404 = /index.html;
    # if ($request_uri = /) {
    #   rewrite ^ /index.html;
    # }
  }
  
  # 静态资源缓存配置(可选)
  # location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|txt|map|json)$ {
  #   expires 30d;
  #   add_header Cache-Control "public";
  #   access_log off;
  # }
}

这样,每次发布 CDN,只要版本号不变(比如测试环境)就没有必要做部署;而如果进行了生产发布,则只需要更新生产机器上的版本号即可。

通过查看 index.html 请求的响应头,可以看到两个方案的差别:

另外,静态托管和 CDN 代理还有一个显著的区别,静态托管的核心配置是 root,有没有 / 结尾都可以,但 CDN 代理的核心 proxy_pass 却必须以 / 结束,否则将 404。

HTTPS

接下来,经验丰富的你会想到 HTTPS 的问题。

有些时候,本地开发、联调、小程序 / 本地H5调试时,可能会被强制要求 HTTPS,如小程序请求、Safari 跨域、PWA、安全接口校验等。本地 Nginx 默认仅支持 HTTP,需要手动生成本地自签名 SSL 证书并配置 Nginx,实现本地可信 HTTPS 访问。

本地 SSL 证书

以下,我们将在 brew Nginx 配置目录下新建一个 ssl 目录专门存放 SSL 文件:

# 进入 nginx 目录(根据自己环境修改)
cd /opt/homebrew/etc/nginx
mkdir ssl && cd ssl

# 生成 RSA 私钥 + 自签名证书(有效期10年)
openssl req -x509 -newkey rsa:4096 -nodes -keyout local.key -out local.crt -days 3650

它会问几个问题,随便回答就行:

这样就在 ssl 目录下生出了两个文件,公钥 local.crt 和私钥 local.key

配置 SSL

然后,我们基于之前的配置,添加 SSL 的相关配置,同时让 80 端口 301(永久重定向)到 HTTPS:

server {
  listen 80;
  server_name doc.test;
	return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  server_name doc.test;
  
  # 核心 ssl 证书配置
  ssl_certificate /opt/homebrew/etc/nginx/ssl/local.crt;
  ssl_certificate_key /opt/homebrew/etc/nginx/ssl/local.key;
  	
  # 基础 ssl 优化配置
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;
  
  location / {
    proxy_ssl_server_name on;
    proxy_ssl_protocols TLSv1.2 TLSv1.3;
    proxy_pass https://somecdn.com/documentation/1.0.0/;
    proxy_set_header Host somecdn.com;
    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 Accept-Encoding "";
    proxy_redirect off;
    proxy_intercept_errors on;
    
    proxy_buffering on;
    proxy_buffer_size 4k;
    proxy_buffers 8 4k;
    proxy_busy_buffers_size 8k;
    
    gzip off; # gzip,避免重复压缩
    
    error_page 404 = /index.html;
    # if ($request_uri = /) {
    #   rewrite ^ /index.html;
    # }
  }
  
  # 静态资源缓存配置(可选)
  # location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|txt|map|json)$ {
  #   expires 30d;
  #   add_header Cache-Control "public";
  #   access_log off;
  # }
}

绕过不安全提示

重启 Nginx 后,你会发现,浏览器并不认可此证书:

这是正常的,毕竟这个证书血统不正宗,但同时也恭喜你,配置 SSL 成功了,可以通过人工干预的方式绕过:

  • Firefox / Chrome:高级 → 继续访问(不安全)
  • Safari:信任证书
  • 小程序开发工具:勾选 不校验合法域名、HTTPS、TLS

以下是配置了 SSL 后的访问效果:

关于生产环境

本地 SSL 签名只能我们开发者自己玩玩,生产环境下就必须使用 CA 机构签发的正式可信 SSL 证书。

而且,证书是有时效的,阿里云 / 腾讯云默认免费证书一般都是时效 90 天的单域名证书(一个证书只能用于特定域名);更方便的长时效的范域名证书(推荐一个证书用于多个子域名),就得舍得花点银子了。

日志

查看日志是必备的技能,记住 tail 这一个命令即可:

默认brew 安装
访问日志tail -100f /var/log/nginx/access.logtail -100f /opt/homebrew/var/log/nginx/access.log
错误日志tail -100f /var/log/nginx/error.logtail -100f /opt/homebrew/var/log/nginx/access.log

你已经知道 Nginx 可以带多个 Server,会思考的你也一定会想到上面的 access.logerror.log 如果混杂了所有 Server 的日志将会带来不必要的麻烦。你可以在对应的服务下配置日志路径到专属的文件,比如你有个 verynb.com 的 Server:

server {
  listen 80;
  server_name verynb.com;
  
  access_log /var/log/nginx/verynb.com.access.log;
  error_log /var/log/nginx/verynb.com.error.log;
  
  ...
}

📕 配置理论知识

变量

Nginx 内置了很多 变量,也就是你在配置文件中看到的 $xx。以下是一些核心的变量(AI 整理):

变量名含义典型使用场景
$remote_addr客户端的 IP 地址(公网 / 内网)日志记录、限流、防盗链
$remote_port客户端与 Nginx 建立连接的端口号日志排查、连接数统计
$remote_user客户端通过 HTTP 认证的用户名权限控制、日志审计
$http_user_agent客户端的 User-Agent(浏览器 / 设备信息)适配不同设备、反爬虫
$http_referer客户端请求的来源页面(Referer 头)防盗链、统计来源
$http_cookie客户端携带的 Cookie 信息鉴权、个性化配置
$request_methodHTTP 请求方法限制请求方法、日志
$request_uri客户端请求的完整 URI(含参数)日志记录、重定向
$request_filename对应请求的本地文件路径(静态资源场景)静态资源缓存、文件访问控制
$uri请求的 URI(不含参数,已解码)反向代理、路径匹配
$args请求的 URL 参数参数透传、日志
$host请求的主机名(优先 Host 头,无则用服务器域名)多域名配置、反向代理
$server_nameNginx 配置中当前 serverserver_name(固定值)多域名区分、日志
$server_portNginx 接收请求的端口号端口适配、日志
$scheme请求的协议 http/https强制 HTTPS 跳转、日志
$statusNginx 返回给客户端的 HTTP 状态码日志统计、错误监控
$bytes_sentNginx 发送给客户端的字节数流量统计、限流
$request_time请求的总处理时间(秒,含小数)性能监控、慢请求排查
$proxy_add_x_forwarded_for拼接客户端真实 IP(X-Forwarded-For 头),传递给后端后端获取客户端真实 IP
$upstream_addr反向代理转发的后端服务器地址(IP: 端口)后端集群排查、日志
$upstream_status后端服务器返回的 HTTP 状态码后端错误监控
$upstream_response_time后端服务器的响应时间(反向代理场景)后端性能监控

Nginx 核心配置项

Nginx 的默认核心配置文件 nginx.conf,可分成 6 个模块:

  • 全局段:全局配置,对全局生效
  • events:配置影响 Nginx 服务器与用户的网络连接
  • http:最常用的配置,配置代理、缓存、日志等功能和第三方模块的配置
  • server:配置虚拟主机的相关参数,一个 http 块中可以有多个 server 块
  • location:用于配置匹配的 URI
  • upstream:配置后端服务器具体地址,负载均衡配置

🙋 FAQ

❓ 如何查看 Nginx 正在代理的所有域名?

这是一个算常见的需求,我就想看看这台机器承接了哪些业务。但可恨的是,Nginx 并不提供类似的便利。幸运的是,有曲线救国的办法:

nginx -T 2>/dev/null | grep -E "\sserver_name\s+" | grep -v "#" | awk '{print $2}' | tr ';' ' ' | tr ',' ' '

以上命令,有几点需要澄清:

  1. 不要问我诸如 如 2>/dev/null 的细节
  2. 其实它代表不了「正在」二字,因为它只是从 nginx -T 的输出中利用正则提取所有 server_name 后面的字段,可能包括注释掉的部分,也可能 Nginx 甚至都没有起来

❓ 访问到了真实的 CDN 文件怎么回事?

理论上,使用 CDN 代理,是不会看到 CDN 的地址的,尤其不可能在浏览器地址栏看到。但偏偏碰到了,是怎么回事?

原因其实也很简单,就是你很可能把 CDN 的 https 误写成了 http,导致触发了 CDN 的 302 跳转,导致页面地址被强行 Forward 到了 https