为什么个人 Flask 博客上线后容易出问题

很多个人博客在本地能跑,放到服务器上就开始出现各种小故障:直接用 flask run 顶生产流量,服务断了没人拉起;Nginx 只会反代不会传 X-Forwarded-* 头,结果 canonical、跳转地址和日志 IP 都不对;静态文件、上传目录和应用进程混在一起,权限一改就互相影响;文章发出去了,但 sitemap 没有及时提交,搜索引擎抓取也慢半拍。

如果你准备长期维护一个 Flask 博客,最稳妥的思路不是追求“最省步骤”,而是把职责拆清楚:应用进程交给 Gunicorn,进程守护交给 systemd,80/443 入口交给 Nginx,SEO 基础设施交给 sitemap 和自动提交流程。这样出问题时你能快速定位,也方便以后从 SQLite 升级到 MySQL、从单机升级到对象存储或 CDN。

一套可复用的部署结构

下面这套结构适合 Debian / Ubuntu 系 Linux 服务器,也适合大多数云主机:

  1. 代码放在 /var/www/blog
  2. 虚拟环境放在 /var/www/blog/.venv
  3. Gunicorn 只监听 127.0.0.1:8000 或 Unix Socket,不直接暴露公网端口。
  4. Nginx 对外监听 80/443,转发动态请求,直接处理静态文件。
  5. systemd 负责开机自启、异常重启、日志管理。
  6. 发布文章后通过脚本补交 sitemap,让搜索引擎更快发现新 URL。

先安装基础依赖:

sudo apt update
sudo apt install -y python3 python3-venv nginx

然后准备项目环境:

cd /var/www
sudo mkdir -p blog
sudo chown -R $USER:$USER blog
cd blog
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
deactivate

如果你的项目已经有 .env,上线前至少确认这些变量:

FLASK_DEBUG=0
DATABASE_URL=sqlite:////var/www/blog/database.db
SITE_PUBLIC_URL=https://example.com
ONLINE_BLOG_SITE_URL=https://example.com
AUTO_PUBLISH_TOKEN=replace-with-random-token

第一步:不要用 flask run 顶生产

Flask 官方文档明确不建议把开发服务器直接用于生产环境。生产上至少要换成专门的 WSGI 服务器。对个人博客来说,Gunicorn 够成熟,部署也直接。

一个最小可用启动命令可以先这样写:

/var/www/blog/.venv/bin/gunicorn -w 2 -b 127.0.0.1:8000 'app:create_app()'

如果你的入口已经暴露 app 对象,也可以写成:

/var/www/blog/.venv/bin/gunicorn -w 2 -b 127.0.0.1:8000 app:app

-w 2 不一定是最终值。个人博客大多不是高并发系统,先从 2 到 4 个 worker 起步更实际,观察 CPU 和内存再调。不要一上来把 worker 开很大,内存小的云主机会先被你自己打爆。

第二步:用 systemd 托管 Gunicorn

直接在 SSH 里跑 Gunicorn 的问题是:断开连接后容易丢进程,重启服务器后也不会自动恢复。systemd 解决的就是这个问题。

新建服务文件 /etc/systemd/system/blog.service

[Unit]
Description=Flask Blog Gunicorn Service
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/blog
EnvironmentFile=/var/www/blog/.env
ExecStart=/var/www/blog/.venv/bin/gunicorn -w 2 -b 127.0.0.1:8000 'app:create_app()'
Restart=always
RestartSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target

加载并启动:

sudo systemctl daemon-reload
sudo systemctl enable --now blog.service
sudo systemctl status blog.service

出问题先看日志,而不是盲目重启:

sudo journalctl -u blog.service -n 100 --no-pager

这里有两个常见坑:

  • WorkingDirectory 不对时,模板、SQLite 文件、上传目录经常全部找不到。
  • EnvironmentFile 没带上时,本地能跑、线上配置失效,通常就卡在数据库连接或发布鉴权。

第三步:让 Nginx 只做自己擅长的事

Nginx 的职责很简单:接收公网请求、处理静态资源、把动态请求转给 Gunicorn。一个够用的配置如下:

server {
    listen 80;
    server_name example.com www.example.com;

    client_max_body_size 10m;

    location /static/ {
        alias /var/www/blog/static/;
        access_log off;
        expires 7d;
    }

    location / {
        proxy_pass http://127.0.0.1:8000/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Prefix /;
    }
}

启用配置后检查语法:

sudo nginx -t
sudo systemctl reload nginx

如果你的博客有图片上传,client_max_body_size 必须和 Flask 侧的 MAX_CONTENT_LENGTH 一起看,不然会出现一种很烦的现象:前端提示“上传失败”,但你分不清是 Nginx 挡掉了,还是 Flask 自己拒绝了。

第四步:反向代理后要修正 Flask 对外感知

Flask 放在反向代理后,应用看到的客户端地址、协议和 Host 都可能变成内网值。官方建议是只在确定有可信反代时启用 ProxyFix,并且明确每个头前面到底有几层代理。

示例:

from werkzeug.middleware.proxy_fix import ProxyFix

app = create_app()
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)

这一步对博客尤其重要,因为它会直接影响:

  • url_for(..., _external=True) 生成的绝对地址。
  • canonical URL 和 sitemap 里的域名、协议是否正确。
  • 后台日志里记录的访客来源 IP 是否可用。

坑点也很明确:不要在没有反向代理的场景下乱开,也不要把代理层数随便写大。X-Forwarded-* 头是可以伪造的,信任范围必须收紧。

第五步:静态文件、上传目录和权限分开

生产环境最怕“为了修一个权限问题,把整个项目目录 chmod -R 777”。正确做法是把职责拆开:

  • 应用代码目录由部署用户维护。
  • static/ 由 Nginx 读取。
  • 上传目录只给应用进程必要写权限。
  • SQLite 数据库文件与备份目录单独管理。

例如:

sudo mkdir -p /var/www/blog/static/uploads
sudo chown -R www-data:www-data /var/www/blog/static/uploads
sudo chmod 755 /var/www/blog/static
sudo chmod 775 /var/www/blog/static/uploads

如果你暂时还在用 SQLite,至少加一个定时备份。因为 SQLite 很适合个人博客起步,但它不是“我永远不用备份”的理由。

第六步:把发布和收录提交串起来

文章发布完成,不代表搜索引擎马上知道新内容。更稳妥的做法是:发布成功后,等待文章详情页和 sitemap 真正可访问,再触发提交脚本。

这个项目里的自动化思路是正确的:

python scripts/publish_article_api.py --input article.zh.json --submit-after-publish

这样做有三个好处:

  • 先拿到真实线上 slug 和文章 URL,再提交,避免提交草稿地址或错误路径。
  • 给页面渲染、缓存刷新、sitemap 更新留缓冲时间。
  • 后续可以继续接百度普通收录和 Google Search Console sitemap 提交,不需要把“发文”和“收录”拆成两套手工流程。

如果你已经生成 sitemap.xml,还要确认三件事:

  • sitemap 使用 UTF-8。
  • 里面放的是绝对 URL,不是相对路径。
  • 最好放在站点根路径,方便覆盖整站文章。

最后给自己留一份上线检查单

每次发版前后,把这份检查单跑一遍,能省掉很多重复排障时间:

  • systemctl status blog.service 是否为 active (running)
  • nginx -t 是否通过。
  • 首页、文章页、后台登录页是否都能打开。
  • 新文章 URL 是否为 HTTPS 和正确域名。
  • /sitemap.xml 是否包含新文章。
  • 发布脚本是否拿到了 ONLINE_BLOG_PUBLISH_URLAUTO_PUBLISH_TOKEN
  • 搜索平台提交凭据缺失时,日志里是否能明确报出缺哪一项,而不是静默失败。

总结

个人 Flask 博客的生产部署,重点从来不是“命令多高级”,而是边界清不清楚。Gunicorn 负责跑应用,systemd 负责保活和开机自启,Nginx 负责入口和静态资源,ProxyFix 负责纠正外部请求信息,sitemap 自动提交负责把新内容尽快送到搜索引擎面前。把这几个环节一次搭对,后面无论你继续加图片上传、后台权限、MySQL、对象存储还是自动化发布,成本都会低很多。