资讯动态

CentOS 9部署Flask项目:从MySQL 8.0到Nginx反向代理实战

发布时间:2026/10/2 8:54:26 来源:尧图企业网站定制
最近在 CentOS 9 上把一个 Flask 项目完整跑通从 MySQL 8.0 的安装到 Gunicorn 托管再到 Nginx 反向代理和 SSL 证书配置整个过程踩了不少坑MySQL 认证插件导致客户端连不上、SELinux 拦截导致 502、替换 SSL 证书后不生效、前端跨域请求被 Nginx 拒绝……每个问题单拎出来都不复杂但连在一起足以让新手折腾一个通宵。这篇文章就把整套部署流程和踩坑排查思路完整记录下来适合有基本 Linux 操作经验、想把 Flask 应用正式上线运行的同学参考。1. 部署前的整体思路与CentOS 9环境准备1.1 为什么这三件套是Web后端部署的标配很多人第一次上线 Flask 项目时脑子里想的是开发环境能跑通生产环境照搬就行——这是最大的误区。开发时你跑的是flask run它自带一个单进程的开发服务器Werkzeug性能差、不支持并发、没有安全防护生产环境直接暴露在外网就是给自己找麻烦。所以生产部署的标准做法是Nginx 在最前面接管流量Gunicorn或 uWSGI在后面跑 Flask 应用MySQL 负责数据持久化三者各司其职。Nginx 的角色是门卫经纪人接收所有外部请求把动态请求转发给后端的 Gunicorn把静态文件请求图片、CSS、JS直接自己处理同时承担 SSL 证书加解密、负载均衡、请求头改写这些脏活累活。Gunicorn 的角色是业务工人真正执行 Flask 代码处理业务逻辑跟 MySQL 打交道。MySQL 就不用多说了所有数据最终都落在它里面。这套架构的好处是每一层都可以独立扩容、独立排错任何一个环节出问题不会直接拖垮整个服务。1.2 CentOS 9 Stream 与 CentOS 7/8 的差异CentOS 9 Stream 和以前的 CentOS 7/8 有本质区别。CentOS 8 时代你还能用yum到 9 这里彻底切换成了dnf虽然yum命令做了兼容软链但底层已经是 dnf。软件源结构也变了默认源的包版本更新Python 直接自带 3.9GCC 也升级到了 11 系列。这个版本变化带来的直接影响是网上大量基于 CentOS 7 的教程用yum install、systemctl start mysqld、包名带-el7在 CentOS 9 上基本不可用照抄只会收获一堆Unable to locate package或者依赖冲突。我这次用的服务器是阿里云的 CentOS Stream 9 镜像装软件时发现默认源里居然没有mysql-community-server只有 MariaDB——这是很多教程不会告诉你的事所以下面我专门用一章来写 MySQL 的安装。1.3 基础环境准备软件包、防火墙与SELinux在动手装任何服务之前先把系统基础环境收拾干净。我按下面顺序执行# 更新系统到最新状态 dnf update -y # 安装编译工具和Python开发包 dnf install -y gcc gcc-c make python3-devel openssl-devel pcre-devel zlib-devel # 安装网络工具和文本编辑器 dnf install -y wget curl vim lsof net-tools防火墙这块CentOS 9 默认用的是firewalld。生产环境我建议尽量白名单开放而不是图省事直接firewall-cmd --permanent --add-port80/tcp把端口全打开。以下是我常用的三条规则# 开放 HTTP 和 HTTPS 端口 firewall-cmd --permanent --add-servicehttp firewall-cmd --permanent --add-servicehttps # 3306 端口不要直接对外开放用前文提到的 ssh 隧道或者只放开内网IP firewall-cmd --permanent --add-rich-rulerule familyipv4 source address192.168.1.0/24 port protocoltcp port3306 accept # 重载让规则生效 firewall-cmd --reload接下来是 CentOS 上最容易被忽略的坑SELinux。新手部署时如果发现本机 curl 正常外部访问 502 或者响应异常十有八九是 SELinux 在拦截。先看一眼状态getenforce如果输出Enforcing那后面配完 Nginx 和 Gunicorn 后很可能要做这一步# 允许 Nginx 作为反向代理访问后端服务 setsebool -P httpd_can_network_connect 1很多人嫌麻烦直接setenforce 0关闭 SELinux我个人不建议在生产环境这么干。SELinux 是纵深防御的一部分真被入侵了能多挡一刀。正确的思路是出了问题先用audit2why查 SELinux 拦截日志后面第六章会展开讲然后用setsebool精准放行而不是一刀切关闭。2. MySQL 8.0 安装时最容易翻车的三个环节2.1 官方RPM源安装而不是dnf默认源前面说过CentOS 9 默认源里没有 MySQL只有 MariaDB。有些人图省事直接dnf install mariadb-server但如果你项目里用到了 MySQL 独有的语法、依赖特定的 MySQL 8 特性比如窗口函数、CTE跑在 MariaDB 上迟早要出问题。我这次是老老实实走官方 RPM 源安装。先去 MySQL 官网下载页找对应 el9 的 RPM 包。以 MySQL 8.0.44 为例我的操作流程是这样# 下载官方仓库RPM包 wget https://dev.mysql.com/get/mysql80-community-release-el9-1.noarch.rpm # 导入并安装仓库配置 rpm -ivh mysql80-community-release-el9-1.noarch.rpm # 安装 MySQL 服务端 dnf install -y mysql-community-server装完后先别急着启动检查一下版本和配置文件位置mysql --version # 输出类似 mysql Ver 8.0.44 for Linux on x86_64MySQL 8 的配置文件是/etc/my.cnf它内部通过!includedir引入了/etc/my.cnf.d/目录下的所有.cnf文件。以后要改字符集、改端口、调性能参数建议在/etc/my.cnf.d/下单独建文件不要直接动主配置。这里多说一句如果你所在环境无法访问外网需要离线安装不要把 RPM 包死磕到底。更稳妥的方式是在一台能联网的同版本 CentOS 机器上用dnf download把整个依赖树拉下来然后把 RPM 文件传到目标机器统一rpm -ivh *.rpm安装。我试过只拷主 RPM 包的结果——缺依赖缺到怀疑人生。2.2 认证插件升级导致客户端连不上MySQL 8.0 默认的认证插件从mysql_native_password换成了caching_sha2_password这是个安全升级但也带来一堆兼容性问题。你的 Python 代码里如果用旧版 PyMySQL1.0 之前或者用的连接工具版本太老就会报Authentication plugin caching_sha2_password cannot be loaded之类的错误。启动 MySQL 后第一次登录需要去日志里找临时密码systemctl start mysqld systemctl enable mysqld grep temporary password /var/log/mysqld.log拿着临时密码登录后第一步就要修改密码并处理认证插件。我建议从客户端角度双管齐下代码侧升级 PyMySQL 到最新版现在pip install pymysql拉到的版本天然支持caching_sha2_password这是根治方案如果实在因为某些原因没法升级客户端再在 MySQL 侧给对应账号补一个兼容配置ALTER USER flaskuserlocalhost IDENTIFIED WITH mysql_native_password BY YourStrongPass123!; FLUSH PRIVILEGES;注意MySQL 8 的密码策略默认是VALIDATE_PASSWORD_COMPONENT要求密码至少 8 位且包含大小写字母、数字和特殊字符。我一开始设了个简单密码被拒白白多花了几分钟。如果你只是在测试环境可以在/etc/my.cnf.d/mysql-server.cnf里加一行validate_password.policyLOW再重启但生产环境千万别干这事。2.3 建库建用户时的字符集与排序规则这一步看着基础踩坑的人却不少。建库的时候如果不显式指定字符集MySQL 8 默认用utf8mb4_0900_ai_ci排序规则。这个规则本身没问题但如果你用的是 MySQL 5.7 时代的备份文件、或者有业务敏感依赖于旧的utf8mb4_general_ci排序行为排序结果可能会有细微差别——热词里那个mysql排序的搜索多半就是这么来的。我的建库语句是CREATE DATABASE flaskapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;为什么选utf8mb4_unicode_ci而不是默认的0900_ai_ci主要是兼容性考虑。你后续可能要从其他环境导入数据或者用老版本客户端做管理0900_ai_ci在某些老客户端里不认。unicode_ci则是 MySQL 5.5 之后都比较通用的方案覆盖面广。接着创建业务账号记得把权限最小化CREATE USER flaskuserlocalhost IDENTIFIED BY YourStrongPass123!; GRANT ALL PRIVILEGES ON flaskapp.* TO flaskuserlocalhost; FLUSH PRIVILEGES;别给flaskuser授予所有库的权限万一以后服务器上还有其他数据库SQL 注入一旦得手就能拖走全部库。只授权flaskapp.*足够了。2.4 顺手处理事务、存储过程与连接池MySQL 8 在事务和存储过程上有不少增强但部署阶段我最关心的其实是连接池。Flask 应用默认每次请求都新建数据库连接在高并发下很快会把 MySQL 的连接数打满报Too many connections。我用 Flask-SQLAlchemy 连接时通常这样配置app.config[SQLALCHEMY_ENGINE_OPTIONS] { pool_size: 10, pool_recycle: 3600, pool_pre_ping: True, }pool_size10表示连接池最多保持 10 个连接pool_recycle3600表示连接每 1 小时回收重建一次避免 MySQL 的wait_timeout把空闲连接断开后池子里还存着死连接pool_pre_pingTrue会在从池中取连接前先探测一次连通性这是我最推荐开启的选项能有效避免经典的MySQL server has gone away错误。3. Flask 应用从开发到 Gunicorn 托管3.1 为什么生产环境不能用 flask run如果你试着在生产服务器上直接python app.py用开发服务器跑 Flask会发现两个问题第一Werkzeug 开发服务器是单进程单线程模型处理完一个请求才能处理下一个。并发稍微上来一点整个应用就跟冻住一样。第二开发服务器启动时会在终端输出一堆调试信息而且默认没有做安全加固直接暴露外网等于把调试后门敞开。生产环境的做法是引入 WSGI 服务器。WSGI 是什么简单说Flask 是一个符合 WSGI 规范的 Web 框架它本身只负责业务逻辑怎么处理不负责怎么接收网络请求、怎么并发。接收网络请求这件事需要专门的服务器来做Gunicorn 就是其中用得最广的一个。用 Gunicorn 托管 Flask 应用的命令很简单# 安装 pip install gunicorn # 启动3个worker进程绑定到127.0.0.1:8000 gunicorn -w 3 -b 127.0.0.1:8000 run:app这里run:app的意思是从run.py通常是你的启动文件中导入名为app的 Flask 实例。.wsgi.文件项目里没有就自己建一个里面写一行from your_project import app as application就行。-w 3这个参数有个经验公式worker 数 CPU 核心数 × 2 1。不是 worker 越多越好因为每个 worker 都会占用内存而且 Gunicorn 的每个 worker 是单进程单线程的默认 sync worker 类型worker 之间切换也有开销。我在 2C4G 的机器上就设 3 个 worker实测够用。3.2 连接MySQL的配置细节Flask 连 MySQL 我强烈建议直接用Flask-SQLAlchemy PyMySQL不要自己裸写pymysql.connect()。原因有三SQLAlchemy 帮你管连接池、帮你做模型映射ORM 让你改表结构时不用写一堆手拼 SQLFlask-SQLAlchemy 把配置集成到app.config里部署时切换环境很方便。连接字符串长这样# config.py import os class Config: SECRET_KEY os.environ.get(SECRET_KEY, dev-only-key) SQLALCHEMY_DATABASE_URI ( fmysqlpymysql://{os.environ.get(DB_USER)}:{os.environ.get(DB_PASSWORD)} f{os.environ.get(DB_HOST, 127.0.0.1)}:{os.environ.get(DB_PORT, 3306)} f/{os.environ.get(DB_NAME)}?charsetutf8mb4 ) SQLALCHEMY_ENGINE_OPTIONS { pool_size: 10, pool_recycle: 3600, pool_pre_ping: True, }看到没有数据库密码绝对不要写死在代码里用环境变量注入。我见过太多代码上传 GitHub 导致数据库密码泄露的事故了。部署时在 systemd 服务文件里通过EnvironmentFile指定一个权限 600 的配置文件或者在 shell 里export好再让 Gunicorn 继承都行。如果项目本身是纯查询场景、又不想引入重量的 ORM那用 PyMySQL 裸连也可以但一定要自己写连接池或者使用DBUtils之类的库。记得在连接字符串里加cursorclasspymysql.cursors.DictCursor这样查询结果会是字典而不是元组写接口时省很多事。3.3 systemd 托管 Gunicorn 的完整配置手动敲gunicorn ...启动的问题在于服务器重启后服务不会自动恢复进程意外挂了没人拉起日志也没有统一管理。所以生产环境要把 Gunicorn 注册成 systemd 服务。先建一个专门运行项目的系统用户不要用 root 直接跑 Web 应用useradd -r -s /sbin/nologin flaskapp假设你的项目放在/var/www/flaskapp并且已经建好了虚拟环境python3 -m venv venv source venv/bin/activate pip install flask flask-sqlalchemy pymysql gunicorn然后创建 systemd 服务文件/etc/systemd/system/flaskapp.service[Unit] DescriptionGunicorn instance to serve flaskapp Afternetwork.target [Service] Userflaskapp Groupflaskapp WorkingDirectory/var/www/flaskapp EnvironmentFile/etc/flaskapp/env ExecStart/var/www/flaskapp/venv/bin/gunicorn -w 3 -b 127.0.0.1:8000 run:app Restartalways RestartSec5 [Install] WantedBymulti-user.target几个关键点逐个说EnvironmentFile指向一个存放环境变量的文件比如/etc/flaskapp/env里面写DB_USERflaskuser、DB_PASSWORDxxxx。这个文件权限设为 600只有 root 能读。Restartalways保证进程意外挂了自动拉起RestartSec5是拉起重试间隔防止进程频繁崩溃时 systemd 疯狂重启。前面创建flaskapp用户时用了-s /sbin/nologin只允许它运行服务不允许登录系统这是安全基线。服务写好后启动并设置开机自启systemctl daemon-reload systemctl start flaskapp systemctl enable flaskapp systemctl status flaskapp这一步做完用curl -I http://127.0.0.1:8000应该能看到 Flask 应用返回 200 响应。确认后端通了再继续配 Nginx。4. Nginx 反向代理配置的关键细节4.1 安装与配置文件布局CentOS 9 安装 Nginx 出乎意料地简单dnf install -y nginx systemctl start nginx systemctl enable nginx装完直接访问服务器 IP 就能看到欢迎页。如果访问不到多半是前文提到的防火墙或者 SELinux 问题先去查这两块。CentOS 9 上 Nginx 的配置结构是/etc/nginx/ ├── nginx.conf # 主配置 ├── conf.d/ # 自定义配置文件目录推荐使用 └── modules/ # 动态模块目录主配置里会在http{}块中自动引入/etc/nginx/conf.d/*.conf所以我习惯给每个站点单独建一个 conf 文件比如flaskapp.conf。这样后期改某个站点配置不影响其他站点nginx -t也能精确定位是哪个文件语法出错。4.2 server块的核心参数解析下面这个配置是从我实际项目里整理出来的每行都别删server { listen 80; server_name yourdomain.com; # 改成你自己的域名 location / { proxy_pass http://127.0.0.1:8000; 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; } location /static/ { alias /var/www/flaskapp/static/; expires 30d; } }proxy_pass是反向代理的核心它把请求转发给后端的 Gunicorn。注意这里的地址必须是127.0.0.1:8000和之前 Gunicorn 的绑定地址一致。如果你 Gunicorn 绑定了0.0.0.0:8000Nginx 再通过公网 IP 转发反而多绕一圈还可能被防火墙拦。下面三个proxy_set_header是反向代理的灵魂Host $host把原始请求的 Host 头原样传给后端。没有这个头Flask 里取到的request.host会变成127.0.0.1:8000重定向和 URL 生成会乱套。X-Real-IP $remote_addr把真实客户端 IP 传给后端。不传的话Flask 里取到的request.remote_addr永远是127.0.0.1Nginx 的本地地址你在日志里看到的全是本机 IP做访问分析直接废掉。X-Forwarded-For和X-Forwarded-Proto同理一个管真实的客户端 IP 链一个管原始协议是 HTTP 还是 HTTPS后者在 Flask 里生成重定向 URL 时尤其重要——不然你从 HTTPS 访问Flask 返回的重定向链路却写成http://。4.3 静态资源分离与文件访问Flask 项目一般都有静态文件目录CSS、JS、图片。如果让 Flask 自己去服务这些文件既占用 worker 进程速度也慢。更优的做法是让 Nginx 直接用alias指向静态目录让 Nginx 这个高性能门卫直接返回静态文件。location /static/ { alias /var/www/flaskapp/static/; expires 30d; # 浏览器缓存30天 access_log off; # 静态文件访问不写日志省磁盘IO }这里用alias而不是root要特别注意root是拿完整 URI 拼路径比如请求/static/a.css实际找/var/www/flaskapp/static/a.css而alias会把/static/前缀替换成后面的路径。两者很容易混用错就会 404。如果你有需求要让 Nginx 代理访问服务器上的某个目录文件热词里nginx代理进行服务器文件访问就是这个场景本质上也是 alias 合适的权限控制但生产环境涉及文件下载时我建议在 Flask 层做权限校验后再重定向或流转文件不要在 Nginx 层裸开放目录权限。配置改完后一定要先测试再重载nginx -t systemctl reload nginxnginx -t会检查所有配置文件的语法报错会精确到行号。这一步省了我无数次明明改了配置却不生效的困惑。配置没问题后用curl -I http://yourdomain.com验证一下响应头能看到 200 就说明整条链路通了。5. SSL证书配置与替换不生效排查实录5.1 证书文件的存放与权限现在不配 HTTPS 基本没法见人了浏览器都把 HTTP 标为不安全。SSL 证书的获取路径有很多云厂商免费证书、Lets Encrypt、GoDaddy 这类域名服务商购买的证书。不管哪来的拿到手的核心文件就两个证书链文件.crt或.pem和私钥文件.key。我在 Nginx 里单独建了一个目录存放证书mkdir -p /etc/nginx/ssl # 证书文件权限限制私钥尤其重要 chmod 600 /etc/nginx/ssl/yourdomain.key chmod 644 /etc/nginx/ssl/yourdomain.crt私钥文件权限设成 600只允许 root 用户读写。如果私钥权限太开放比如 644Nginx 会直接拒绝启动并报错Permissions 0644 for xxx.key are too open。这是我第一次配证书就踩的坑搜了下发现这个问题常年排在 Nginx SSL 报错榜前列。然后在 server 块里加 SSL 配置server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/nginx/ssl/yourdomain.crt; ssl_certificate_key /etc/nginx/ssl/yourdomain.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; # 其余location配置和之前一致 location / { proxy_pass http://127.0.0.1:8000; # ... } }ssl_protocols TLSv1.2 TLSv1.3是安全基线别开 TLSv1.0/1.1它们都有公开的漏洞POODLE、BEAST 等很多安全扫描工具会直接判不合格。5.2 替换SSL证书不生效的完整排查链路这是热词榜上出现频率很高的问题nginx 替换 ssl 证书不生效。我之前的实际经历是这样的某证书快到期了我申请了新证书把/etc/nginx/ssl/下的旧.crt和.key直接覆盖成了新文件然后systemctl reload nginx。用浏览器访问一看——还是旧证书。我当时的排查过程是这样的先nginx -t语法通过说明配置文件没问题。再用curl -I https://yourdomain.com随便试发现响应正常但用openssl s_client一查证书有效期还是旧的。尝试systemctl restart nginx而不是 reload——结果证书变新的了。问题根因找到reload信号只是让 Nginx master 进程重新读取配置文件并 fork 新的 worker但 worker 进程持有的是已打开文件的文件描述符。如果证书文件是直接覆盖写的旧 worker 还拿着旧文件句柄自然还是旧证书。这也解释了为什么网上很多人说reload 不生效必须 restart。严格来说如果你的证书文件路径变了比如从旧路径指向了新路径的软链reload 是生效的但如果是覆盖同一个文件路径reload 可能不够。解决这个问题的最佳实践是替换证书时不要直接覆盖原文件而是先上传为新的文件名再修改 nginx 配置指向新文件然后 reload。这样既能避免旧 worker 的句柄问题出问题还能快速回滚到旧配置。后来我就养成了给证书文件名带日期的习惯yourdomain-2025-01.crt yourdomain-2025-01.key换证书只改配置里的两行路径nginx -t systemctl reload nginx干净利落。5.3 用 openssl 验证证书是否真的生效替换完证书后别光看浏览器浏览器有缓存会欺骗你。用 openssl 直接跟服务器握手看返回的证书指纹最客观openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2/dev/null | openssl x509 -noout -dates -fingerprint输出里能看到证书的notBefore、notAfter时间和指纹。拿这个指纹跟本地证书文件的指纹比对openssl x509 -in /etc/nginx/ssl/yourdomain.crt -noout -fingerprint两个指纹一致说明服务器上加载的确实是你上传的新证书。这个命令我现在每次换证书后都会跑一遍几秒钟的事杜绝以为生效了其实没有的乌龙。还有个常见问题是证书链不完整。有些证书服务商发的是单证书而不是pack 好的完整链如果你的.crt文件里只有服务器证书没包含中间证书Nginx 虽然能启动但 Android 手机、部分浏览器会报证书链错误。解决办法是把中间证书和服务器证书拼一起cat yourdomain-server.crt intermediate.crt yourdomain-fullchain.crt然后配置里用yourdomain-fullchain.crt。怎么判断有没有中间证书还是上面那个openssl s_client命令看Certificate chain段落是否输出了两行一个 s 结尾的服务器证书 一个 i 开头的中间证书只有一行的话基本可以确定链不完整。6. 上线后遇到的常见坑与排查思路6.1 502 Bad Gateway从外到内的排查顺序整套服务跑起来后第一个可能遇到的大故障就是 502。502 的意思是 Nginx 作为代理服务器没能从上游Gunicorn拿到有效响应。遇到 502我的排查顺序永远是从外到内第一步确认 Gunicorn 是否活着systemctl status flaskapp curl -I http://127.0.0.1:8000如果本地 curl 都失败问题在应用层。去查 Gunicorn 日志systemd 的journalctl -u flaskapp -n 50看是不是 Flask 崩溃、数据库连接失败、或者是 worker 被 OOM 杀了。第二步确认 Nginx 是否真的能连接到 Gunicorn如果 Gunicorn 活着但curl外部域名依然 502nc 测一下端口连通性nc -vz 127.0.0.1 8000能通就一定查SELinux。用audit2why看拦截日志grep nginx /var/log/audit/audit.log | tail -20 | audit2why如果输出里提到httpd_can_network_connect那跑前文那条setsebool -P httpd_can_network_connect 1即可。第三步确认防火墙firewall-cmd --list-all看 80/443 端口是否放行。如果这些都没问题最后才怀疑 Nginx 配置本身。这套从外到内的逻辑能帮你把盲人摸象式的排错变成有节奏的排查每次至少省半小时。6.2 Nginx invalid CORS request 跨域问题的解法热词里nginx invalid cors request搜索量不小这个报错我遇到时也愣了下。现象是你的前端页面可能单独跑在某个端口或 CDN 上向后端 API 发请求浏览器控制台报跨域错误同时 Nginx 错误日志里出现invalid CORS request。根因是跨域请求中浏览器会先发一个OPTIONS预检请求探测服务器是否允许实际的跨域方法。而 Nginx 默认不处理OPTIONS方法没有给它配置跨域响应头于是预检请求拿到 403 或者没有Access-Control-Allow-Origin浏览器就把真实请求拦下了。解决的思路是在 Nginx 里显式放行 OPTIONS 预检请求并返回跨域头server { listen 443 ssl http2; server_name yourdomain.com; location / { if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS; add_header Access-Control-Allow-Headers Authorization, Content-Type, X-Requested-With; add_header Access-Control-Max-Age 86400; return 204; } proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意Access-Control-Allow-Origin的值如果是前后端分离且只给自家产品用建议写具体域名比如https://front.yourdomain.com而不是*。写*虽然省事但等于把接口开放给了任何网站别人从恶意页面发起请求浏览器也会放行响应读取这就是所谓的 CSRF 风险面扩大。如果不想在 Nginx 层做 CORS也可以在 Flask 里用 Flask-CORS 扩展统一处理但我的经验是Nginx 层处理更早、更可控因为 CORS 本质是接入层问题不该塞进业务代码里。6.3 MySQL SSL连接错误的处理mysql ssl 连接错误也是一个高频搜索词。MySQL 8 默认开启了 SSLTLS如果你在连接字符串里显式指定了ssl_ca路径但配置错误或者客户端和服务端 SSL 协商失败就会报错。我遇到的具体场景是Python 代码里用了?ssl_ca/path/to/ca.pem但服务器上的 CA 证书路径写错了导致连接直接失败。排查时先看 MySQL 端 SSL 状态SHOW VARIABLES LIKE %ssl%;能看到have_ssl的值。如果have_sslYES但客户端不需要强制加密最省事的解法是在连接字符串里关闭 SSL 验证# SQLAlchemy URL 写法 SQLALCHEMY_DATABASE_URI mysqlpymysql://user:pass127.0.0.1:3306/flaskapp?ssl_disabledtrue注意是ssl_disabledtrue而不是sslfalse拼错了不生效。如果业务对安全性要求高必须启用 SSL那就认真把 CA 证书和require_secure_transport配好别用关闭的方式绕过。顺带提一句如果你的 MySQL 开启了require_secure_transportON那所有客户端都必须走 SSL这时候再想用ssl_disabledtrue关掉验证就会连接失败。这种环境只能把 CA 证书认认真真配好。6.4 日志与监控最后一道安全保障服务跑起来只是开始能持续稳定运行才是目的。我的习惯是上线当天就配好日志轮转# Nginx 日志和 Gunicorn 日志都接入 logrotate dnf install -y logrotateNginx 默认自带 logrotate 配置Gunicorn 的 systemd 日志则通过journalctl -u flaskapp查看。我建议在 service 文件里给 Gunicorn 指定独立的访问日志和错误日志文件ExecStart/var/www/flaskapp/venv/bin/gunicorn -w 3 -b 127.0.0.1:8000 --access-logfile /var/log/flaskapp/access.log --error-logfile /var/log/flaskapp/error.log run:app日志目录记得先mkdir -p /var/log/flaskapp chown flaskapp:flaskapp /var/log/flaskapp否则 Gunicorn 没有权限写日志启动会直接失败。至于监控热词里提到的 Zabbix 是挺成熟的选择。CentOS 9 上装 Zabbix 7.0 LTS MySQL 8.0 的组合我后来也试过Zabbix 主要监控三件事Nginx 存活与状态码、MySQL 连接数、系统 CPU/内存/磁盘。如果你不想一开始就上 Zabbix 这么重的方案可以用更轻量的方式写个 crontab 脚本每 5 分钟curl -f https://yourdomain.com/healthz失败就发送告警。健康检查接口在 Flask 里几行代码就写完app.route(/healthz) def healthz(): return {status: ok}, 200从成本收益比来说先上健康检查再上 Zabbix是我推荐的节奏。一上来就铺一套完整监控体系大概率会因为太麻烦而放弃维护那才是真正的浪费。这篇文章写到这里Nginx Flask MySQL 全栈部署的完整链路就算全部串起来了。说实话这套架构熟练之后在两台新服务器上从零部署到 HTTPS 上线我一个人两小时就能搞定。大部分时间不是花在敲命令上而是花在明明按教程做的为什么不生效这种问题上——所以我把排查思路看得比命令本身更重要。如果你在部署中也遇到类似问题希望这篇记录能帮你少走几个通宵。最后再啰嗦一句上线前记得备份/etc/nginx/、/etc/my.cnf.d/和应用代码目录一个 tar 命令的事灾难恢复时能救命。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑