news 2026/10/27 21:07:10

CentOS 9 单机 Web 栈部署与端口排错指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CentOS 9 单机 Web 栈部署与端口排错指南

1. 先把目标定清楚:CentOS 9 这套栈的整体设计与选型

CentOS 9 安装配置 MySQL、Redis、Node.js、pm2、Nginx 加防火墙,这套组合我前后搭过几十次,它基本是一台“单机跑 Web 服务”的最小可用栈:Nginx 在最前面接流量、做 SSL 和静态资源,Node.js 跑业务逻辑并由 pm2 守着进程,MySQL 存关系数据,Redis 扛会话和热数据,firewalld 管对外暴露的那几个口子。听起来五件事,实际上一台两核四G的小机器上,装完配完往往要耗掉一整个下午,而且九成的时间不是花在“装”上,而是花在“装完之后跑不起来”——端口不通、权限不够、开机不自启、日志把磁盘写满。

这篇文章写给两类人:一是第一次在 CentOS 9 上从零搭这套环境,希望少走弯路的;二是已经装过几轮,但总觉得配置是东拼西凑抄来的、出了问题不知道该从哪一层下手的。我会把每个组件为什么这么配、参数怎么算出来的、SELinux 和 firewalld 到底在哪一步会拦你,都拆开讲明白。

先说一个容易被忽略的前提:CentOS 9 严格讲叫CentOS Stream 9,它默认开着 firewalld、默认 SELinux 是 enforcing、默认用 dnf 和 cgroup v2。这三件事决定了后面的所有操作。很多人从别的发行版搬命令过来,第一步就卡在“我明明启动了服务,浏览器就是打不开”,其实服务活得好好的,是防火墙没放行,或者 SELinux 不让 Nginx 连本地端口。

1.1 为什么是这五件套,而不是别的组合

选型这件事没有绝对对错,只有匹配不匹配。这套组合的核心优势在于组件之间耦合松、单机可承载、故障域清晰。

  • Nginx 做统一入口:它负责 TLS 卸载、gzip、静态文件、反代和限流。业务服务只监听 127.0.0.1 的高位端口,外网永远只看到 80/443 两个口,安全面收得很窄。
  • Node.js + pm2:Node 适合 I/O 密集的接口服务,pm2 解决“进程崩了谁拉起来”“机器重启谁先启动”这两个最实际的问题。我不建议用 systemd 直接托管 node 脚本,除非项目非常单一,多实例、多环境时 pm2 的 ecosystem 文件更好管。
  • MySQL 8 存主数据:8.0 之后默认 utf8mb4、默认 caching_sha2_password,性能和信息安全都比 5.7 好,但这两点也恰好是迁移时最容易翻车的地方,后面细讲。
  • Redis 做缓存和队列:会话、验证码、排行榜、简单队列,基本都靠它。但它内存有限,绝不能当数据库用。
  • firewalld + SELinux:这不是“可选加固”,而是 CentOS 9 的默认状态。想关掉它们当然可以,但我的建议是留着,学会怎么放行,因为你永远不知道这台机器明天会不会被要求做等保整改。

有一点要先摆明:这套栈适合日活几千到几万的单体应用。再往上,Nginx 要做集群、MySQL 要做主从、Redis 要做哨兵,单机这套配置只能当参考骨架,不能当终态。

1.2 装机当天就必须拍板的四个开关

我踩过最深的坑,是装到一半才发现某个前置决策错了,只能推倒重来。所以这四件事建议在敲第一条命令前就想清楚:

决策项我的默认选择什么情况下改
SELinux保持 enforcing,按需放行项目有强制要求且明确接受风险时才考虑 permissive
firewalld保持开启,只放 80/443 和运维端口前面有硬件防火墙且策略已覆盖,可只留必要端口
Node 安装方式NodeSource 官方仓库装 LTS一台机器跑多个 Node 版本时改用 nvm
MySQL 来源系统 AppStream 自带 8.0需要特定小版本或官方补丁时用 MySQL 官方仓库

这里解释一下为什么 SELinux 我坚持留着。很多教程第一句就是setenforce 0,图省事。问题是这台机器一旦交给别人运维、或者过几个月你自己都忘了改过什么,SELinux 的告警就被永久性地关掉了,等于少了一层保险。而实际需要放行的场景就那么几个:Nginx 反代本地端口(httpd_can_network_connect)、非标准端口(semanage port)、自定义站点目录(semanage fcontext)。学会这三招,比关掉它划算得多。

Node 的安装方式也一样。NodeSource 装出来的是系统级、路径固定、开机就在的 Node,适合“一台机器一个应用”的常态;nvm 装在用户目录下,切换版本方便,但pm2 的开机自启会变得麻烦,因为 systemd 拉起进程时的 PATH 和你的登录 shell 不一样,nvm 装的 node 常常找不到。这一点在第 3 节会展开。

2. 系统初始化:先把地基打平

刚装完的 CentOS Stream 9 其实挺干净,但离“能跑业务”还差几步。我习惯在装任何组件之前先做一轮初始化,顺序是:更新系统 → 校对时间 → 装常用工具 → 调内核参数。顺序不能反,尤其是时间同步,必须放在 MySQL 和 Redis 之前。

2.1 换源、时间同步与常用工具

首先更新一遍系统并装基础包:

dnf update -y dnf install -y epel-release dnf groupinstall -y "Development Tools" dnf install -y vim wget curl git unzip lsof net-tools sysstat chrony tar

Development Tools这个包组看着多余,其实后面全用得上:Redis 如果走编译安装需要编译链,Node 的一些原生模块(数据库驱动、图像处理库)也需要 gcc、g++、make 和 python3。现在装好,省得以后npm install报node-gyp错误再回头补。

时间同步是重中之重。MySQL 的主从复制、Redis 的过期键、JWT 令牌校验、日志排查,全都依赖时间一致。CentOS Stream 9 自带 chrony,确认一下:

systemctl enable --now chronyd chronyc sources -v chronyc tracking

看到Leap status : Normal和System time偏移在毫秒级就算正常。如果机器在云上,一般云厂商有内网 NTP 地址,填到/etc/chrony.conf里比用公网源稳定得多。我见过一次线上事故:两台机器时间差了四分钟,用户登录后拿到的 token 一落地就提示“已过期”,查了整整一下午才发现是 NTP 没同步。

时区也要一起定:

timedatectl set-timezone Asia/Shanghai timedatectl status

时区不定,MySQL 默认时间戳和你日志里看到的差八小时,排查问题时能把人逼疯。后面 MySQL 配置里我还会再设一次default-time-zone,两边对齐才保险。

主机名顺手改掉,不要留着localhost.localdomain,后面 Nginx 和证书标识、日志标识都会用到:

hostnamectl set-hostname app-node-01

2.2 文件句柄、内核参数与 swap

这一块是“不调也能跑,一压就出事”的典型。默认的ulimit -n通常是 1024,Nginx 和 Node 一上并发就报too many open files。我一般直接写到/etc/security/limits.d/99-app.conf:

cat > /etc/security/limits.d/99-app.conf <<'EOF' * soft nofile 65535 * hard nofile 65535 * soft nproc 65535 * hard nproc 65535 EOF

注意*通配对 root 在某些版本上不生效,稳妥做法是再单独写一行root soft nofile 65535。改完必须重新登录才生效,ulimit -n只对当前会话有效,别用它验证配置。

内核参数写在/etc/sysctl.d/99-app.conf:

cat > /etc/sysctl.d/99-app.conf <<'EOF' net.core.somaxconn = 65535 net.ipv4.tcp_max_syn_backlog = 65535 net.ipv4.ip_local_port_range = 1024 65000 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15 fs.file-max = 1000000 vm.overcommit_memory = 1 vm.swappiness = 10 EOF sysctl --system

逐个说下理由:somaxconn是 listen 队列长度,Nginx 的backlog和 Node 的默认 listen backlog 都会受它限制,默认 4096 在高并发瞬间会丢连接;ip_local_port_range扩大本地端口范围,避免反代建立大量短连接时端口耗尽;vm.overcommit_memory=1是 Redis 官方明确要求的,0 的时候 fork 做持久化可能因为内存预估失败而被拒绝;swappiness=10让系统尽量少用 swap,数据库类应用对换页延迟极其敏感。

关于 swap 我的态度是:保留,但别依赖。一台 2G 内存的机器不配 swap,Node 编译时直接被 OOM killer 杀掉;配了 swap 又容易让 MySQL 出现“看起来没卡其实在换页”的假象。折中方案是给 1~2G swap,同时把vm.swappiness压低到 10,让系统只在真没内存时才用。

3. Node.js 与 pm2:进程怎么才能不死

这一节是整套栈里最容易出“玄学问题”的地方——明明node app.js手动跑得好好的,一挂到 pm2 就报模块找不到;明明pm2 start成功了,重启机器进程就没了。根因基本都在环境变量和权限上下文上。

3.1 Node 装哪个版本,走哪条路

CentOS Stream 9 的 AppStream 里也有 nodejs,但版本通常偏旧,我不建议用。生产上我固定走 NodeSource 官方仓库装 LTS:

curl -fsSL https://rpm.nodesource.com/setup_20.x | bash - dnf install -y nodejs node -v npm -v

装完顺手确认一下which node,正常应该是/usr/bin/node。这个路径很重要,因为后面 pm2 生成 systemd 服务时,会把这个路径写进去,系统级安装的好处就在这里体现。

如果你确实需要多版本共存,用 nvm:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 20 nvm alias default 20

但请记住:nvm 装在哪个用户下,pm2 就只能在哪个用户下用。如果以后要用 systemd 托管 pm2,unit 文件里必须显式写Environment=PATH=/home/用户名/.nvm/versions/node/v20.x/bin:/usr/bin,否则 systemd 找不到 node,服务状态会显示status=127。这是新手最常见的死结之一。

还有一个创建账户的细节:生产环境我不建议用 root 跑 Node。新建一个普通用户专门跑应用:

useradd -m -s /bin/bash deploy passwd deploy

后面 pm2 的所有操作都su - deploy后再执行,日志、代码、进程都归这个用户所有,权限边界清晰。用 root 跑 node 的问题不只是安全,还有pm2 restart之后文件属主变了、Nginx 读不到静态文件这类连锁反应。

3.2 pm2 的 ecosystem 文件与开机自启

先全局装:

npm install -g pm2 pm2 -v

全局装完在/usr/lib/node_modules/pm2,命令软链到/usr/bin/pm2。如果装的时候用sudo,记得检查pm2命令属于哪个用户,混用会导致“root 启动的进程 deploy 用户看不到”这种诡异现象。

真正的关键是ecosystem.config.js。手敲pm2 start app.js只适合临时调试,生产一律用配置文件:

module.exports = { apps: [{ name: 'web-api', script: './server.js', cwd: '/home/deploy/apps/web-api', instances: 2, exec_mode: 'cluster', max_memory_restart: '512M', autorestart: true, max_restarts: 10, min_uptime: '10s', restart_delay: 3000, env: { NODE_ENV: 'production', PORT: 3000 }, error_file: '/home/deploy/logs/web-api-error.log', out_file: '/home/deploy/logs/web-api-out.log', merge_logs: true, time: true }] };

几个参数值得单独解释:

  • instances和exec_mode: 'cluster'是配套的,cluster 模式下 pm2 用 Node 的 cluster 模块起多进程,共享同一个端口。核数建议是CPU 核数或核数 - 1,2 核机器起 2 个进程是合理的,起 8 个只会互相抢 CPU。
  • max_memory_restart: '512M'是防内存泄漏的兜底。Node 的内存泄漏很难一次性根治,与其等进程把机器撑爆,不如让它到阈值自动重启。阈值怎么定?看你的应用稳态内存,取 1.5 到 2 倍。稳态 300M 就设 512M,设太小会频繁重启,反而更糟。
  • min_uptime: '10s'配合max_restarts是防止“启动即崩、崩了再启”的死循环把 CPU 占满。10 秒内挂掉的算启动失败,连续 10 次就进入 errored 状态不再重试,把问题暴露出来。
  • time: true给日志加时间戳。别小看这一行,日志没时间戳,出事的时候你根本对不上哪条日志对应哪个请求。

然后启动并保存:

pm2 start ecosystem.config.js pm2 save pm2 startup systemd -u deploy --hp /home/deploy

pm2 save把当前进程列表写入~/.pm2/dump.pm2,pm2 startup生成开机自启的 systemd unit。这两条的顺序不能反,而且必须在同一个用户下执行。执行pm2 startup后它会打印一条需要 root 执行的命令,照着复制执行一遍才真正生效。很多人只跑了第一条,重启后进程全没了,就是漏了这步。

3.3 日志切割与内存守护

pm2 的日志默认无限增长,一个中等流量的接口服务,一周写满 10G 磁盘不稀奇。装个官方模块自动切割:

pm2 install pm2-logrotate pm2 set pm2-logrotate:max_size 100M pm2 set pm2-logrotate:retain 10 pm2 set pm2-logrotate:compress true pm2 set pm2-logrotate:rotateInterval '0 0 * * *'

这里我遇到过一个小坑:pm2-logrotate是装在 pm2 内部的模块,用 npm 装无效,必须用pm2 install。装完它自己会以守护进程形式存在,pm2 list里能看到pm2-logrotate这一项。

另外这套配置在 cgroup v2 下有个已知的兼容性问题:pm2 的 systemd 集成(pm2 install pm2-systemd)在老版本上不支持 cgroup v2,装完重启后进程状态异常。CentOS Stream 9 默认就是 cgroup v2,所以我的做法是只用pm2 startup,不装pm2-systemd,开机自启交给 systemd unit 就够了,不需要额外的集成模块。

最后做一次完整验证,这一步别省:

pm2 list pm2 describe web-api reboot # 重启后重新登录 pm2 list curl -s http://127.0.0.1:3000/health

重启后能看到进程、能打通健康检查,才算这套进程管理真正落地。只看pm2 list显示 online 是不够的,那可能只是你重启前看到的缓存。

4. MySQL 8:装得快,调得好才叫本事

MySQL 我一般直接用系统仓库的版本,除非项目指定小版本号:

dnf install -y mysql-server systemctl enable --now mysqld systemctl status mysqld

首次启动会做初始化,临时密码在日志里:

grep 'temporary password' /var/log/mysqld.log

拿到后立刻跑安全脚本:

mysql_secure_installation

这个脚本会依次问你:要不要装密码强度插件、要不要改 root 密码、要不要删匿名用户、要不要禁止 root 远程登录、要不要删 test 库、要不要重载权限。我的回答一律是:装强度插件(y)、改密码、删匿名(y)、禁止 root 远程(y)、删 test 库(y)、重载(y)。

注意:MySQL 8 默认密码策略是MEDIUM,要求至少 8 位、含大小写字母、数字和特殊字符。如果你的密码一直提示不满足策略,不是命令错了,是策略拦的。生产环境别关这个策略,改成LOW只是把风险往后推。

4.1 我常用的一份配置参数

配置文件放/etc/my.cnf.d/mysql-server.cnf(CentOS 系的惯例是主配置my.cnf只放 include,实际参数分文件写,升级时不容易被覆盖):

[mysqld] bind-address = 127.0.0.1 port = 3306 character-set-server = utf8mb4 collation-server = utf8mb4_0900_ai_ci default-time-zone = '+08:00' max_connections = 500 max_connect_errors = 1000 wait_timeout = 600 interactive_timeout = 600 innodb_buffer_pool_size = 1G innodb_log_file_size = 256M innodb_flush_log_at_trx_commit = 1 innodb_flush_method = O_DIRECT slow_query_log = 1 slow_query_log_file = /var/log/mysql-slow.log long_query_time = 1 log_queries_not_using_indexes = 0 sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

innodb_buffer_pool_size是最该算的一个参数,不是拍脑袋写的。经验公式是物理内存的 50%~70%,前提是这台机器只跑 MySQL。如果像我这种单机五件套,MySQL 和 Node、Redis 挤在一起,就要往下压:4G 内存的机器,MySQL 给 1G,Redis 给 512M,剩下留给系统和 Node。

innodb_log_file_size默认 48M 偏小,写密集场景会频繁 checkpoint。256M 是个性价比不错的档位,改完必须重启才生效,且旧日志文件要清理干净,否则启动会报错。

innodb_flush_log_at_trx_commit = 1是最安全的设置,每次事务提交都刷盘,掉电最多丢一个事务。改成 2 能提升写入性能,但掉电可能丢一秒数据。这个取舍必须让业务方知情,不能运维自己决定。

bind-address = 127.0.0.1很关键。单机部署时 MySQL 根本不需要对公网开放,绑本地既减少攻击面,也省掉防火墙放行 3306 的必要。如果确实要远程连,走 SSH 隧道或者内网专线,别直接开放端口。

慢查询日志一定要开。long_query_time = 1意味着超过 1 秒的查询被记录,这个阈值对小项目合适;大表多的项目可以放到 2 秒,否则日志刷得太快。

4.2 账号授权与字符集的两个坑

建库建号有一套固定动作:

CREATE DATABASE app_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; CREATE USER 'app_user'@'127.0.0.1' IDENTIFIED BY '强密码放这里'; GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'127.0.0.1'; FLUSH PRIVILEGES;

注意主机位写127.0.0.1而不是%。%意味着任意来源,单机部署完全没必要。另外权限只给 DML,不给DROP、ALTER,建表改表用另一个迁移账号做。这个习惯在出过一次“误删表”的事故后我就再没改过。

坑一:caching_sha2_password 认证失败。MySQL 8 默认认证插件是caching_sha2_password,一些老的客户端库、老版本的 Navicat、部分语言的旧驱动连不上,报Authentication plugin 'caching_sha2_password' cannot be loaded。解决办法不是把全局默认改回mysql_native_password(那等于降级安全),而是只给特定账号改:

ALTER USER 'legacy_user'@'%' IDENTIFIED WITH mysql_native_password BY '密码';

坑二:字符集不统一导致的乱码和索引失效。建库用 utf8mb4,连接层也要设。JDBC 要加characterEncoding=utf8&useUnicode=true,Node 的 mysql2 驱动默认就是 utf8mb4,但连接池配置里显式写一遍更保险。还有排序规则,utf8mb4_0900_ai_ci和utf8mb4_general_ci混用会导致 JOIN 时索引失效,建库时就定死一种,全项目统一。

5. Redis:别把缓存当数据库用

Redis 的安装有两种路子。系统仓库版本稳定、开箱即用:

dnf install -y redis systemctl enable --now redis redis-cli ping

返回PONG就通了。如果你需要新版本特性(比如某些 Stream 命令、新的数据结构),可以加 remi 源,或者下载源码编译。编译安装的好处是版本可控,坏处是升级麻烦、systemd unit 要自己写。我的建议是:没有明确的新版本需求,就用系统仓库的版本,稳定压倒一切。

5.1 一份生产可用的 redis.conf 关键项

配置文件在/etc/redis/redis.conf,需要改的其实就那么几行:

bind 127.0.0.1 protected-mode yes port 6379 requirepass 这里放一个足够长的随机串 timeout 300 tcp-keepalive 300 maxmemory 512mb maxmemory-policy allkeys-lru appendonly yes appendfsync everysec save 900 1 save 300 10

逐条解释:

  • bind 127.0.0.1+protected-mode yes是双保险。只绑本地,外面的机器根本连不上。很多人图方便绑0.0.0.0又不设密码,等于把数据库放在公网上裸奔。
  • requirepass必须有。哪怕只绑本地也要设,因为同机的其他进程、被入侵的低权限账户都可能连上来。密码用openssl rand -base64 32生成,别自己编。
  • maxmemory和maxmemory-policy是搭配的。不设maxmemory,Redis 会一直吃内存直到系统 OOM;设了allkeys-lru,内存满了就淘汰最久未用的键。注意这是删除数据,如果你的键不能丢,就不能用这个策略,得用noeviction并做好容量监控。
  • appendonly yes开 AOF 持久化,appendfsync everysec是性能和安全折中,最多丢一秒数据。RDB 的save规则也保留着,两种持久化并存是最稳的。

改完重启并验证:

systemctl restart redis redis-cli -a '你的密码' ping redis-cli -a '你的密码' config get maxmemory

config get能读到正确值,说明配置真的生效了,而不是改了个没被引用的文件。

5.2 系统层面的两个隐藏开关

Redis 有两个必须调的系统参数,不调会在日志里一直报警告:

第一个是vm.overcommit_memory = 1。Redis 做 RDB 持久化时要 fork 一个子进程,Linux 的 overcommit 策略设成 0 时,内核会保守估计内存,可能导致 fork 失败,日志里出现Background saving error。这个参数我们在第 2 节已经写进 sysctl 了,这里确认一下sysctl vm.overcommit_memory输出是 1。

第二个是关闭透明大页(THP)。Redis 官方文档明确说 THP 会让 fork 时的内存复制延迟飙升。临时关闭:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

永久生效要写进 systemd 服务,或者加个开机脚本。CentOS 上用 systemd 的覆盖文件比较规范:

mkdir -p /etc/systemd/system/redis.service.d cat > /etc/systemd/system/redis.service.d/override.conf <<'EOF' [Service] OOMScoreAdjust=-900 EOF systemctl daemon-reload

OOMScoreAdjust=-900让系统在内存紧张时优先杀别的进程而不是 Redis。这个值不能设成 -1000,那会完全禁用 OOM killer,反而危险。

6. Nginx:反向代理才是主战场

dnf install -y nginx systemctl enable --now nginx nginx -v

系统仓库的 Nginx 版本通常落后一两个大版本。如果需要 HTTP/3 或者新版特性,加官方仓库:

cat > /etc/yum.repos.d/nginx.repo <<'EOF' [nginx-stable] name=nginx stable repo baseurl=http://nginx.org/packages/centos/9/$basearch/ gpgcheck=1 enabled=1 gpgkey=https://nginx.org/keys/nginx_signing.key module_hotfixes=true EOF dnf install -y nginx

module_hotfixes=true这行不加,可能和 AppStream 里的模块冲突导致装不上,这是 CentOS 8 之后的老问题。

目录约定我习惯这样:站点配置全部放/etc/nginx/conf.d/*.conf,不要动nginx.conf主文件;静态资源放/var/www/站点名/;日志默认在/var/log/nginx/。这样以后换机器,把conf.d和/var/www打包带走就行。

6.1 一份能直接用的反代配置

upstream node_backend { server 127.0.0.1:3000; server 127.0.0.1:3001; keepalive 32; } server { listen 80; server_name example.internal; client_max_body_size 50m; access_log /var/log/nginx/app-access.log main; error_log /var/log/nginx/app-error.log warn; location / { proxy_pass http://node_backend; proxy_http_version 1.1; 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 Connection ""; proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_read_timeout 60s; } location /ws/ { proxy_pass http://node_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { root /var/www/app-static; expires 30d; add_header Cache-Control "public, immutable"; access_log off; } }

几个点值得展开:

proxy_set_header Connection ""配合keepalive 32是开启长连接的关键。不写这行,Nginx 到后端的连接每个请求都要重新握手,QPS 高的时候 TIME_WAIT 能把端口耗尽。

X-Forwarded-For和X-Real-IP必须传。否则 Node 里req.ip永远拿到的是 127.0.0.1,做限流、审计、风控全都失效。

WebSocket 那个 location 的proxy_read_timeout设到 3600s,是因为默认 60 秒会把长连接掐断,前端表现为“每隔一分钟重连一次”。这个问题我排查过两次,每次都是忘了配这一行。

静态资源单独 location,expires 30d加immutable,让浏览器强缓存。注意这些文件如果带 hash 文件名才敢这么设,不带 hash 的话更新后用户看到的是旧版本。

6.2 HTTPS、上传大小与超时

证书这块,如果是对外服务,直接用 Let's Encrypt 最省事:

dnf install -y certbot python3-certbot-nginx certbot --nginx -d your.domain

它会自动改配置、加 443 监听、配好跳转,还会装一个续期定时器。验证一下:

systemctl list-timers | grep certbot certbot renew --dry-run

--dry-run一定要跑,不然 90 天后证书过期,凌晨三点被告警叫醒的滋味不好受。

内网服务用自签证书的话,重点是给客户端分发 CA,而不是在 Nginx 上关掉校验。另外建议加上这几个安全头:

add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; add_header Referrer-Policy strict-origin-when-cross-origin;

client_max_body_size默认是 1m,用户传个头像就 413。这个值要按业务定,传文档的给 50m,纯接口的给 5m 就够。设太大也有风险,大文件上传会占用 worker 连接和磁盘临时空间。配套还要调client_body_timeout,默认 60 秒,大文件在慢网络下传不完会被中断。

7. 防火墙与 SELinux:九成“端口不通”发生在这里

这两块是 CentOS 9 上最容易被误伤的部分。奇怪的是,大部分人排查“网页打不开”时,会先去翻 Nginx 日志,其实日志里干干净净,因为请求根本没到 Nginx。

7.1 firewalld 的日常动作

先看当前状态,别急着加规则:

firewall-cmd --state firewall-cmd --get-default-zone firewall-cmd --list-all

--list-all会显示当前 zone 的 services、ports、sources。默认 zone 通常是public,默认放行的服务只有 ssh、dhcpv6-client 这些。

放行 Web 服务:

firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --reload firewall-cmd --list-services

放行自定义端口:

firewall-cmd --permanent --add-port=8080/tcp firewall-cmd --reload

三个必须记住的点:

第一,--permanent和--reload是一对。不加--permanent,规则重启就没了;加了不--reload,当前运行时没生效。很多人加了规则测不通,就是漏了 reload。

第二,不要用systemctl stop firewalld来“解决”问题。停掉之后systemctl disable一开,这台机器就永久失去网络层防护,而且下一个人接手时根本不知道。

第三,如果这台机器前面有云厂商的安全组,安全组和 firewalld 是两道独立的门,两道都要开。我见过在 firewalld 里开了 443,安全组没开,对着服务器查了两小时。

7.2 SELinux 对 Nginx 的两个强制限制

SELinux 默认策略下,Nginx 相关的限制主要有两条,几乎每个搭反代的人都会撞上:

限制一:Nginx 不允许主动发起网络连接。表现是 502 Bad Gateway,/var/log/nginx/error.log里写着Permission denied while connecting to upstream。注意,这不是文件权限问题,是 SELinux 的布尔值没开:

setsebool -P httpd_can_network_connect 1

-P是持久化,重启后仍然生效。不加-P只是临时改,重开机就还原。这一条是反代场景的必做项,没有例外。

限制二:Nginx 不能监听非标准端口。比如你想让 Nginx 监听 8080,直接改配置会启动失败,journalctl -u nginx里报bind() to 0.0.0.0:8080 failed (13: Permission denied)。解决方法是把这个端口注册给 http_port_t:

semanage port -a -t http_port_t -p tcp 8080 semanage port -l | grep http_port_t

如果提示端口已存在,用-m修改而不是-a添加。

还有一个容易忽略的是网站目录。如果静态资源没放/var/www,而是放在/home/deploy/apps/web/dist,Nginx 会 403。因为 SELinux 给文件的标签不对:

semanage fcontext -a -t httpd_sys_content_t "/home/deploy/apps/web/dist(/.*)?" restorecon -Rv /home/deploy/apps/web/dist

semanage fcontext只是登记规则,restorecon才真正打标签。两条一起敲才算完。

排查 SELinux 拒绝的最快方式是看审计日志:

ausearch -m AVC -ts recent

或者临时开 permissive 模式验证一下,确认是 SELinux 的问题再回去改策略,不要长期停留在 permissive。这个排查手法我用了很多次,比一个个猜布尔值快得多。

7.3 一份端口规划表

单机多服务最容易乱的就是端口。我习惯在部署前先画一张表,写进文档:

服务监听地址端口是否对外说明
Nginx0.0.0.080/443是唯一对外入口
Node API127.0.0.13000/3001否pm2 cluster 双实例
MySQL127.0.0.13306否只允许本机
Redis127.0.0.16379否有密码 + 绑本地
SSH0.0.0.022是建议改端口 + 密钥登录

这张表的价值在交接和排查。出了访问问题,先看表:这个端口本来就不该对外,那外部连不上是正常的,问题在 Nginx 配置;这个端口需要对外,那就去查 firewalld 和安全组。

8. 上线后最容易撞上的问题与排查顺序

装完到跑通之间,一定会有几轮反复。我把自己遇到过、也帮别人处理过的高频问题整理成表,按现象查原因,比一上来就翻日志快。

8.1 十类高频问题对照表

现象最可能的原因快速验证处理
502 Bad GatewaySELinux 禁止 Nginx 连本地端口ausearch -m AVC -ts recentsetsebool -P httpd_can_network_connect 1
502 但日志无异常后端进程没起或端口不符ss -lntp | grep 3000检查 pm2 状态与 PORT 环境变量
浏览器超时打不开firewalld 未放行firewall-cmd --list-all加 service/port 后 reload
重启后进程消失没执行pm2 savels ~/.pm2/dump.pm2pm2 save后重跑 startup
npm 装原生模块报错缺编译链gcc -v装 Development Tools
MySQL 连不上(本机)密码策略/认证插件mysql -u user -p -h 127.0.0.1查错误码 1045/2059
Redis 报 NOAUTH客户端没传密码redis-cli ping客户端配置加 password
上传文件 413client_max_body_size太小响应头状态码调整后nginx -t并 reload
内存持续上涨最后 OOMNode 泄漏无限制pm2 monit设max_memory_restart
时间对不上导致登录异常chrony 未同步chronyc tracking修 NTP 源并重启 chronyd

表格里“502 但日志无异常”这一条特别值得说。这种情况九成是后端真的没起来,而不是 Nginx 的问题。验证顺序应该是:ss -lntp看端口有没有在监听 →pm2 list看进程状态 →curl 127.0.0.1:3000直接打后端。三步下来基本能定位。

8.2 我的排查顺序:从外到内五层

遇到“服务访问不了”,我固定按这五层往下走,不跳步:

第一层,DNS 和网络。ping 域名看解析对不对,curl -v http://IP看能不能连上。如果 IP 能通域名不通,问题在 DNS,不在服务器。

第二层,防火墙。firewall-cmd --list-all看规则,再看云厂商安全组。这两处是外网流量的必经之路。

第三层,Nginx。nginx -t检查配置语法,systemctl status nginx看进程,tail -f /var/log/nginx/error.log看实时错误。注意nginx -t通过不代表配置生效,改完必须systemctl reload nginx。

第四层,后端服务。ss -lntp确认端口在听、curl 127.0.0.1:端口确认本地能通、pm2 logs看应用日志。本地通但通过 Nginx 不通,问题就在反代配置或者 SELinux。

第五层,应用本身。前面四层都正常,那就是代码或数据库的问题。看应用日志、看慢查询、看 Redis 连接数。

这个顺序的价值在于它保证你不会在错误的地方浪费时间。我见过太多人一上来就翻应用日志,翻了半小时发现请求压根没到应用——因为被防火墙挡住了。

几个额外的实操心得。nginx -T会把所有 include 的配置合并打印出来,排查“我改的这个文件到底有没有被加载”特别好用。pm2 logs --lines 200能一次看两百行,比tail方便。journalctl -u 服务名 --since "10 min ago"是 systemd 服务的万能日志入口,MySQL、Redis、Nginx 都适用。

还有一个经常被忽略的小技巧:改完任何配置,先nginx -t或mysqld --validate-config验证,再 reload,不要直接 restart。reload 是平滑的,restart 会断连接。而且语法错误的情况下 reload 会失败并保留旧进程,restart 会直接起不来,线上当场挂掉。这个习惯救过我至少两次。

我个人在实际操作中的体会是,这套环境真正的难点从来不在“装”,而在“装完之后每一层都要对得上”。Nginx 监听 80、firewalld 放行 80、安全组放行 80、SELinux 允许 Nginx 连 3000、Node 真的在 3000 上听着、MySQL 密码能过认证、Redis 绑本地且有密码——这七个条件缺一个,页面就是打不开。所以我现在的做法是,装完一个组件就立刻做一次端到端验证,而不是六个全装完再一起测。一步一步稳,比一口气装完再回头查要快得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 21:07:09

Moltbot个人AI助手:低硬件需求与隐私保护的创新实践

1. Moltbot为何一夜爆红&#xff1f;深度解析这款个人AI助手的独特价值 去年12月突然在开发者社区走红的Moltbot&#xff0c;三个月内GitHub star数突破2万&#xff0c;Discord社群涌入15万用户。这款开箱即用的个人AI助手究竟有何魔力&#xff1f;作为首批深度使用者&#xff…

作者头像 李华
网站建设 2026/10/27 21:07:08

Windows后台服务跨会话打开文件:Session 0隔离与令牌复制实践

服务里调用ShellExecute返回 TRUE&#xff0c;日志干干净净&#xff0c;用户屏幕上却什么都没有——这个现象我前后撞上过三次&#xff0c;最早那次还怀疑是资源管理器抽风&#xff0c;重装系统都没解决。问题从来不在"打开文件"这行代码上&#xff0c;而在于 Window…

作者头像 李华
网站建设 2026/10/27 21:05:52

提升付费转化的3个关键维度与5个设计技巧

1. 为什么用户不愿意付钱&#xff1f;先找到核心痛点做产品的朋友经常遇到一个灵魂拷问&#xff1a;为什么用户就是不愿意掏钱&#xff1f;我见过太多团队花了几个月开发功能&#xff0c;上线后发现付费转化率低得可怜。问题往往出在没找准真正的付费痛点。去年我们做了一款效率…

作者头像 李华
网站建设 2026/10/27 21:05:39

Win10下DirectShow视频采集实操指南:组件定位、注册与调试

简介&#xff1a;本资源是面向Windows平台C多媒体开发者的DirectShow实战工具包&#xff0c;专为在Win10环境下快速上手和稳定运行DirectShow框架而整理优化。内容涵盖基础过滤器模型解析、COM接口调用范例、Filter Graph构建逻辑及GraphEdit调试方法&#xff0c;适用于视频播放…

作者头像 李华
网站建设 2026/10/27 21:05:36

Kinodynamic RRT*:带动力学约束的机器人轨迹规划算法

简介&#xff1a;本资源是Kinodynamic RRT 算法的MATLAB实现&#xff0c;面向机器人运动规划方向的研究者、控制理论学习者及自动驾驶/无人机路径规划相关工程人员&#xff0c;聚焦于满足线性微分约束&#xff08;如双积分器、四旋翼动力学&#xff09;的最优轨迹生成问题。代…

作者头像 李华
网站建设 2026/10/27 21:05:19

Web可访问性实践指南:从语义化HTML到ARIA应用

1. 为什么Web可访问性如此重要&#xff1f; 我至今记得2018年参与的一个政府项目&#xff0c;验收时一位视障测试员用屏幕阅读器操作我们精心设计的页面&#xff0c;结果导航完全失效的尴尬场景。那次经历让我深刻认识到&#xff1a;可访问性不是锦上添花&#xff0c;而是现代W…

作者头像 李华