豪子博客开源,附上源码和部署方法

目录

先说个事:这篇文章,就是这套系统自己渲染出来的

之前我写过一篇《用 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 步:准备

你需要:

  1. 一台 Linux 服务器(内存 1G 就非常够用,这套系统真的不挑);
  2. 一个解析到这台服务器的域名(想先用 IP 访问也可以,跳过第 8 步 HTTPS 部分即可);
  3. 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

要改的地方:

  1. server_name blog.example.com; → 改成你的域名;
  2. 配置里有 两个 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 步:收尾必做三件事

  1. 改后台密码:访问 https://你的域名/admin/login,用 admin / admin123 登录,立刻到「设置」里改掉账号名和密码;
  2. 确认真实 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 那几行;
  3. 备份习惯:整站内容 = 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;可在评论区留言,我第一时间修复。

𤠌again
𤠌again 重庆

宝塔面板,怎么配置?

豪子
豪子 博主 @𤠌again 重庆

Github上有bt的教程

麒麟猪
麒麟猪 广东 广州

哈哈哈,测试评论

写评论
0 / 2000