为什么个人 Flask 博客上线后容易出问题
很多个人博客在本地能跑,放到服务器上就开始出现各种小故障:直接用 flask run 顶生产流量,服务断了没人拉起;Nginx 只会反代不会传 X-Forwarded-* 头,结果 canonical、跳转地址和日志 IP 都不对;静态文件、上传目录和应用进程混在一起,权限一改就互相影响;文章发出去了,但 sitemap 没有及时提交,搜索引擎抓取也慢半拍。
如果你准备长期维护一个 Flask 博客,最稳妥的思路不是追求“最省步骤”,而是把职责拆清楚:应用进程交给 Gunicorn,进程守护交给 systemd,80/443 入口交给 Nginx,SEO 基础设施交给 sitemap 和自动提交流程。这样出问题时你能快速定位,也方便以后从 SQLite 升级到 MySQL、从单机升级到对象存储或 CDN。
一套可复用的部署结构
下面这套结构适合 Debian / Ubuntu 系 Linux 服务器,也适合大多数云主机:
- 代码放在
/var/www/blog。 - 虚拟环境放在
/var/www/blog/.venv。 - Gunicorn 只监听
127.0.0.1:8000或 Unix Socket,不直接暴露公网端口。 - Nginx 对外监听 80/443,转发动态请求,直接处理静态文件。
- systemd 负责开机自启、异常重启、日志管理。
- 发布文章后通过脚本补交 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_URL与AUTO_PUBLISH_TOKEN。 - 搜索平台提交凭据缺失时,日志里是否能明确报出缺哪一项,而不是静默失败。
总结
个人 Flask 博客的生产部署,重点从来不是“命令多高级”,而是边界清不清楚。Gunicorn 负责跑应用,systemd 负责保活和开机自启,Nginx 负责入口和静态资源,ProxyFix 负责纠正外部请求信息,sitemap 自动提交负责把新内容尽快送到搜索引擎面前。把这几个环节一次搭对,后面无论你继续加图片上传、后台权限、MySQL、对象存储还是自动化发布,成本都会低很多。