先说个事:这篇文章,就是这套系统自己渲染出来的
之前我写过一篇《用 Flask + SQLite 手搓一个完整博客系统》,讲的是它的原型 NeonScript——一个 870 行代码的玩具。当时不少朋友留言说"等它长大一点再告诉我"。半年过去,它长大了不少:870 行变成了 3000 多行,从"能跑"变成了"真在用",现在我把代码开源了。
这篇文章回答两件事:我为什么放着现成的不用,非要自己写;以及,如果你也想搭一个,怎么把这份代码跑起来——我会尽量写得让完全没部署过服务器的人也能跟着做。
先说结论:自己写博客系统,多半不划算
我不是要劝你也手搓一个。真的,写博客系统是那种"别人一小时搭完、我要写一个季度"的事。如果你只是想好好写文章,用现成的就行,省下的时间够你多写五十篇。
但我还是写了,而且不后悔。原因下面一个个说。
1. 我受够了"换个工具丢一次配置"
早期我用过静态博客生成器:Hexo、Hugo 都折腾过。它们的好处是快、免费、能放 GitHub Pages,但坏处也很实在:
- 每次换电脑,先装 Node 或 Go 环境,再
npm install或下载主题,装完还经常因为版本对不上跑不起来; - 想加个评论?要么挂第三方服务(国内访问时好时坏),要么自己折腾 gitment / giscus 之类;
- 想要个后台管理界面?不存在的,改个分类都要去翻 Markdown 的 frontmatter;
- 图床、代码高亮、阅读统计,全是零零散散拼起来的,哪天某个服务挂了,页面就少一块。
文章越写越多,配置越来越散,我越来越烦。我真正想要的东西其实特别朴素:文章是纯 Markdown 存在本地,数据是能被一个文件带走的,管理有界面,部署之后不用天天伺候。
找了一圈:WordPress 功能全但太重(一台 1G 内存的小鸡跑它加 PHP + MySQL 有点喘);Typecho 轻一些,但主题和插件生态这些年明显沉寂;动态博客三件套(服务器 + 数据库 + 框架)里又没有什么"开箱即用还轻"的选项。最后我决定自己写一个——反正我是写 Python 的,Flask 又不重。
2. 数据要能"一整个拷走"
这是我特别在意的一点。这个博客的数据库是 SQLite,单文件 data/blog.db,加上 posts/ 里 Markdown 源文件、uploads/ 里上传的图片,整个博客的全部内容就是几个文件夹。备份 = 拷贝;迁移服务器 = 打包带走;想导出 = 直接把 Markdown 拿走,永不绑定。
数据在自己手里,才谈得上"这是我的博客"。平台再好,哪天它关了、或者我账号没了,文章还在,这个安全感是现成方案给不了的。
3. 有些需求,只有自己写才会被认真对待
用现成系统时,遇到不满意的地方,你的选择只有"忍着"或"换一个"。自己写之后,需求会变成一行行真实代码,你会发现很多"我以为很简单"的功能其实坑很深。举几个这半年真实踩过的:
- 评论盖楼。评论要做到树形回复(楼中楼)不难,难的是"删一条评论要把它的整棵子树一起删掉""待审核的回复不能先通知对方,审核通过后才补发""博主自己回复不用走审核也不打扰自己"。这些细节,用第三方评论系统你根本碰不到,自己写才懂。
- 头像是个玄学。我做了 Cravatar → Gravatar → QQ 头像逐级回退,结果某些精简版服务器连证书校验都过不了,还得加"校验证书失败就降级重试"。最后又补了一个本地上传头像的选项——传上去之后还出过一个很隐蔽的 bug:上传目录从
/static/uploads挪到根目录/uploads之后,判断"头像文件是否存在"的函数还只认/static/开头的路径,结果头像明明在,页面却一直显示"未设置"。 - 移动端是真的烦。做文章列表卡片时,鼠标悬停有背景色,桌面看着挺好。结果手机上、以及把浏览器窗口拖窄时,问题全来了:卡片用了负 margin 外扩,移动端悬停底色会把整行顶到屏幕边缘、圆角被裁掉;更诡异的是触摸屏上"悬停"状态会一直黏住,点完一篇文章底色还亮着。这些不拿真机一个个试,永远发现不了。
- 登录安全不敢马虎。后台登录我加了基于 IP 的失败计数 + 验证码,防爆破。虽然个人博客被爆破的概率不高,但"后台裸奔"这件事我心理上过不去。
这些坑,单个看都不大,但加起来就是"为什么现成系统满足不了我"的答案——不是它们做不到,是它们没理由为"我"做到。
现在的样子
简单介绍一下这套系统现在(v1.3.20)的能力,仓库 README 里有完整清单,这里挑重点:
- 写作:后台是 Markdown 编辑器(Vditor),支持传图、代码高亮、Mermaid 流程图、数学公式、思维导图一类的扩展;
- 前台:首页、文章详情、分类/标签筛选、搜索、分页、关于页、时间线都有;内置两套主题(default / tech),代码里主题是独立目录,可以照着写自己的;
- 动态能力:评论系统(可开关)、友情链接申请与审核、GitHub 项目自动抓取展示、文章阅读时长;
- 后台管理:文章、设置、友情链接、项目、评论、站点信息都在
/admin里管; - 零外部服务:除了可选的第三方头像源,整站不依赖任何外部 API,静态资源全部本地。
技术栈非常简单,没有一处是为了炫技:
| 层次 | 用啥 | 原因 |
|---|---|---|
| Web 框架 | Flask | 轻,一个文件能塞下整个应用 |
| 数据库 | SQLite | 单文件、零配置、备份就是拷文件 |
| 模板 | Jinja2 | Flask 标配,继承机制够用 |
| Markdown | Python-Markdown | 栅栏代码块、表格、列表都能开 |
| 前端 | 原生 CSS / JS | 无框架、无构建步骤,加载快 |
| 图标 | 内联 SVG | 按需引入,不引图标库 |
架构上最"离谱"但也最省事的一点:整个后端就一个 app.py 文件。从 870 行的原型长到现在 3000 多行,还是单文件。好处是部署就拷一个文件、逻辑跳转 Ctrl+F 就行;坏处是……也明显,超过 3000 行之后单文件已经有点"缝合怪"了。等它再长大,我可能会拆成包结构,但目前没有拆的紧迫理由——能用就别动,是我写个人项目最大的原则。
开源了,仓库在这里
- 仓库地址:https://github.com/Contribuv/infowe_blog
如果你想先看看再决定要不要部署,README 里写了功能、目录结构、快速开始;这篇博客剩下的大半篇幅,就是给"看完了,我想自己部署一个"的人准备的。
保姆级部署教程
目标读者:有一台 Linux 服务器(或云服务器),有一个域名(没有也能部署,后面说),但没怎么碰过 Nginx、systemd 的人。我会把每一步该敲什么、为什么敲,都写清楚。
本教程基于 Ubuntu / Debian。其它发行版把 apt 换成你的包管理器即可。整个流程走完大概需要 20~40 分钟。
第 0 步:准备
你需要:
- 一台 Linux 服务器(内存 1G 就非常够用,这套系统真的不挑);
- 一个解析到这台服务器的域名(想先用 IP 访问也可以,跳过第 8 步 HTTPS 部分即可);
- SSH 能连上服务器,并且用户有
sudo权限。
第 1 步:装环境
SSH 登录服务器,先更新软件源,然后安装 Python 3、pip、venv 和 Nginx:
sudo apt update
sudo apt install -y python3 python3-venv python3-pip nginx git
装完确认一下版本(要求 Python 3.8+):
python3 --version
第 2 步:拉代码
# 把项目克隆到 /var/www 下,目录名叫 blog
cd /var/www
sudo git clone https://github.com/Contribuv/infowe_blog.git blog
# 把目录所有权交给当前用户,后面操作就不用一直 sudo 了
sudo chown -R $USER:$USER /var/www/blog
cd /var/www/blog
第 3 步:建虚拟环境 + 装依赖
强烈建议用虚拟环境,别把依赖装进系统 Python,不然以后容易打架:
cd /var/www/blog
python3 -m venv venv
source venv/bin/activate
# 项目依赖(requirements.txt)
pip install -r requirements.txt
# gunicorn 是生产用的 WSGI 服务器,单独装一下
pip install gunicorn
第 4 步:设置密钥(这一步别跳过)
Flask 用 SECRET_KEY 签名登录 session。如果你不设置,代码会 fallback 到源码里的一个默认值——这意味着任何看过源码的人都能伪造 session 直接进你的后台。
生成一个随机串并记下来:
python3 -c "import secrets; print(secrets.token_hex(32))"
先临时设成环境变量试跑用(第 5 步),第 7 步我们会把它写进 systemd 配置,让它永久生效:
export BLOG_SECRET_KEY="上一步输出的那一长串"
第 5 步:本地先跑起来看看
cd /var/www/blog
source venv/bin/activate
python app.py
看到类似 Running on http://127.0.0.1:5000 就成功了。浏览器访问 http://你的服务器IP:5000 看看首页:
- 前台首页:
http://IP:5000/ - 后台登录:
http://IP:5000/admin/login - 默认账号:
admin/ 密码admin123
⚠️ 这个默认密码是给首次登录用的,登录后立刻去后台「设置」里改掉账号名和密码。我后面还会再提醒一次,因为它真的很重要。
看完没问题,Ctrl+C 停掉。python app.py 只适合开发调试,不适合长期跑——没有守护进程、没有自动重启,SSH 一断服务就没了。生产环境用下面这套。
第 6 步:gunicorn 先跑起来
项目里已经带了一份 gunicorn 配置(gunicorn.conf.py):绑定 127.0.0.1:5000、4 个 worker、每处理 1000 个请求自动重启防内存泄漏。默认值对个人博客完全够用,不需要改。
先手动启动确认它能跑:
cd /var/www/blog
source venv/bin/activate
gunicorn -c gunicorn.conf.py app:app
看到 Listening at: http://127.0.0.1:5000 就是好的。Ctrl+C 停掉,我们把它交给 systemd 托管。
第 7 步:systemd 让它开机自启、挂了自动拉起
项目里有个 blog.service,复制到系统目录再改几个占位符:
sudo cp /var/www/blog/blog.service /etc/systemd/system/blog.service
sudo nano /etc/systemd/system/blog.service
打开后对照改这几处:
WorkingDirectory=/var/www/blog # 改成你的项目实际路径
User=www-data # 改成运行用户(默认 www-data 也行)
Environment=BLOG_SECRET_KEY=CHANGE_THIS_TO_A_LONG_RANDOM_STRING
# ↑ 把第 4 步生成的那串随机字符填进来,这是最关键的一行
保存退出(nano 里 Ctrl+O 保存,Ctrl+X 退出)。然后:
sudo systemctl daemon-reload
sudo systemctl enable --now blog
sudo systemctl status blog
看到 active (running) 就成了。以后想重启服务:
sudo systemctl restart blog
看日志排错:
journalctl -u blog -f
第 8 步:Nginx 反代 + 域名
gunicorn 现在监听在 127.0.0.1:5000,外面访问不到——这是故意的,真正对外的是 Nginx,它负责把请求转给 gunicorn,顺便处理静态文件和 HTTPS。
项目里带了样例配置 deploy_infowe.site.conf,复制过来改两处:
sudo cp /var/www/blog/deploy_infowe.site.conf /etc/nginx/sites-available/blog
sudo nano /etc/nginx/sites-available/blog
要改的地方:
server_name blog.example.com;→ 改成你的域名;- 配置里有 两个
alias路径要改(一个指static/,一个指uploads/):
location /static {
alias /var/www/blog/static; # 改成实际路径
}
location /uploads {
alias /var/www/blog/uploads; # 改成实际路径
}
我特意把
/uploads也标出来:上传的图片、头像在 v1.0.6 之后都存在项目根目录的uploads/里,如果忘了配这一段,头像和文章图片会全部 404——这是我上线时真实踩过的坑。
启用配置并检查语法:
sudo ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default # 删掉默认站点,避免它抢占 80 端口
sudo nginx -t
sudo systemctl reload nginx
现在用浏览器访问 http://你的域名,应该能看到首页了。
第 9 步:上 HTTPS(推荐)
用 certbot,一条命令自动申请证书并改写 Nginx 配置:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d 你的域名 -d www.你的域名
跟着提示选"把 HTTP 重定向到 HTTPS"即可。证书快到期时 certbot 会自动续期,不用管。
第 10 步:收尾必做三件事
- 改后台密码:访问
https://你的域名/admin/login,用admin/admin123登录,立刻到「设置」里改掉账号名和密码; - 确认真实 IP:在文章下随便留一条评论(或开评论后自己发一条),然后
journalctl -u blog -f看访问日志,确认记录的是访客真实 IP 而不是127.0.0.1。如果全是127.0.0.1,说明 Nginx 的X-Forwarded-For头没传对,检查第 8 步的proxy_set_header X-Forwarded-For那几行; - 备份习惯:整站内容 =
data/(数据库)+posts/(文章源文件)+uploads/(图片)。定期把这几个目录打包下载,就是完整备份:
# 在服务器上打成一个包(先停一下服务保证数据库一致)
sudo systemctl stop blog
cd /var/www && tar czf blog-backup.tar.gz blog/data blog/posts blog/uploads
sudo systemctl start blog
没有域名能玩吗
能。第 8 步把 server_name 直接写成服务器 IP,跳过第 9 步 HTTPS,一样能跑。只是浏览器地址栏会有"不安全"提示、并且没法用 cookie 相关的某些高级特性——个人随便玩玩完全够。想认真用,还是建议花几十块买个域名。
常见问题
问:直接用 python app.py 跑行不行?
开发调试行,长期跑不行。没有守护、没有自动重启、SSH 断开服务就没了。生产请走 systemd + gunicorn。
问:能改端口吗?
能。改 gunicorn.conf.py 里的 bind,然后 Nginx 的 proxy_pass 同步改成对应端口。
问:文章怎么发布?
登录后台 → 文章管理 → 写文章,粘贴 Markdown 或直接用内置编辑器写。文章会存进数据库,posts/ 里的 Markdown 是给"数据在手"的保险和方便导入用的。
问:忘了后台密码 / 想改数据库里的东西怎么办?
SQLite 是单文件,用 sqlite3 data/blog.db 进去就能查改(服务器上装个 sqlite3 即可)。这是自建系统的好处,也是坏处——权限全在你手里,别把它暴露到公网。
问:有 Bug 或者想要新功能?
欢迎去仓库提 issue。个人项目我会按自己的节奏修,但每一条我都会看——毕竟这个项目每一个功能,最初都是从一个"要是能……就好了"开始的。
最后说几句
写这套系统花的时间,够我在现成平台上发几百篇文章了。但它带给我的东西,不是"省时间"能衡量的:
- 我彻底搞懂了博客背后那些我以为懂、其实不懂的东西:session 安全、反代、上传、防爆破、移动端适配;
- 我想要什么,它就有什么,不用跟任何人商量;
- 所有数据都在我手里,哪天服务器没了,文章也丢不了。
开源它,一半是记录这段经历,一半是想让有同样"控制欲"的人少走点弯路——你可以直接拿去用,也可以把它当一个"能看懂每一行"的参考实现。它不大、不炫,但每一行都是真实需求长出来的,没有一行是凑数的。
如果这篇部署教程哪里写得不够清楚,或者你照着做卡在哪一步,欢迎来评论区骂我——这个评论系统,正好也能顺便帮我测测扛不扛得住。
仓库:https://github.com/Contribuv/infowe_blog
评论 4
在使用过程中遇到bug;可在评论区留言,我第一时间修复。
宝塔面板,怎么配置?
Github上有bt的教程
哈哈哈,测试评论