一个人做个人网站(一):开篇——一个人怎么做自己的内容站
这是系列的第一篇。我会先交代这个站「是什么」「为什么这么选」「怎么跑起来」,后面 8 篇再逐个拆开讲实现细节。
如果你也想一个人拥有一个完全属于自己的内容站——不是公众号、不是各种笔记平台,而是域名、代码、数据都在自己手里的那种——希望这个系列能给你一条可复制的路径。
一、这个网站长什么样
线上地址 wulala.cc,我叫它「小欢喜」。它不是一个纯博客,而是一个把我想放在网上的东西都收拢进来的个人内容站:
| 板块 | 内容 | 备注 |
|---|---|---|
| 博客 | 文章、分类、标签、草稿、阅读量 | Markdown 写作,存源文+渲染后 HTML |
| 相册 | 相册集、照片拖拽排序、灯箱浏览 | 图片自动压缩、生成缩略图 |
| 视频 | 视频卡片、在线播放 | 大文件分片上传、自动截帧做封面 |
| 作品 | 项目/作品展示 | 与博客共用一套内容范式 |
| 习惯打卡 | GitHub 风格年度热力图 | 支持补卡、按年切换 |
| 小游戏 | 2048、合成大西瓜、恐龙跑酷 | 带全站排行榜,访客昵称即玩 |
| AI 网页宠物 | 桌宠挂件,可以聊天 | 接 DeepSeek,口令门禁+限流 |
除了访客能看到的这些,还有一个只有我能进的后台:文章/相册/视频/作品的增删改查、标签分类管理、打卡和游戏纪录管理、站点配置(首页 banner、宠物人设、社交链接等全部后台可视化配置)。日常更新内容完全不需要登录服务器。
技术栈一句话概括:
- 前端:Vue 3(
<script setup>)+ Vite 6 + Vue Router + Pinia,Element Plus 用于后台,访客端自己写样式 - 后端:Flask 3.0 + Flask-SQLAlchemy,Gunicorn 部署,MySQL 8
- 存储:阿里云 OSS(上海),后台图片视频浏览器直传
- 入口:一台 CentOS ECS,源码安装的 Nginx 做反向代理和 HTTPS
二、技术选型:一个人维护,简单和可控压倒一切
选型没有绝对的好坏,只有约束下的取舍。这个项目的约束是:开发者只有我一个人,需求会随时变,我希望三年后还能看懂自己的代码。
为什么是 Flask 而不是 Django
Django 当然更"全"——admin、ORM、migration、表单全家桶。但这个站的后台我本来就要用 Vue 自己写(需要完全贴合内容形态的交互,比如照片拖拽排序、视频上传进度),Django admin 用不上;它的 migration 体系对单人小项目也偏重,加个字段要生成迁移脚本、部署时还要 migrate。
Flask 给我的是:一张白纸 + 足够薄的抽象。我用应用工厂组织代码、用蓝图分模块、用一个统一响应信封收口 API 风格,剩下的全部自己掌控。数据表用 create_all() 随启动自动建,加表加列重启即可——这套策略的边界后面第 2 篇会专门讲。
反过来,我没有用 FastAPI:这个站是传统的服务端渲染页面 + CRUD API,没有异步 IO 密集场景,Flask 的同步模型更直白,生态里 markdown、Pillow、oss2 这些库也都是同步的,不用在 async/await 上付出额外心智。
为什么前端是 SPA,但又"留了一只 SSR 的脚"
访客端是 Vue SPA,体验顺滑;但有个绕不过去的问题:微信里分享文章,卡片需要标题、封面、描述(Open Graph 标签),而微信爬虫不执行 JavaScript——纯 SPA 抓到的永远是一个空 <div id="app">。
所以我的方案不是"SPA or SSR"二选一,而是:
- 真人浏览器 → Nginx 直接给 SPA 静态包
- 爬虫(微信/百度/Google…)→ Nginx 反代给 Flask,返回一套服务端渲染的 HTML,里面带齐 OG 标签
同一篇文章因此有两个"视图",但内容数据源只有一份。这套分流是整个架构里最有意思的部分,第 7 篇详谈。
其他几个选型
- MySQL 而不是 SQLite:线上要并发、要
utf8mb4、要方便我直接 SQL 排查和修数据(这套部署文档里就留了好几条运维 SQL),MySQL 是最稳妥的选择。 - OSS 而不是把文件堆在 ECS:图片视频会把磁盘吃光,也没有 CDN 能力;OSS 公共读 + 自定义域名 + 浏览器直传,ECS 连文件字节流都不用碰。第 6 篇是全套攻略。
- 不引入 Docker / Redis / 消息队列:单机、低流量、一个人,systemd 管进程足够,MySQL 扛所有持久化,增加中间件只会增加我要维护的东西。等真的有瓶颈再说——架构演进应该被需求触发,而不是被简历触发。
三、总体架构:一张图看清四条链路
┌─────────────────────────────────────────────┐
浏览器(普通访客)──────►│ Nginx :80/:443 │
│ ├─ 静态资源 → frontend/dist(SPA) │
│ ├─ /api/ /media/ sitemap robots rss │
│ │ → Gunicorn 127.0.0.1:5001 │
│ ├─ /ossimg/ → OSS 上海内网 endpoint(剥下载头)│
│ └─ 前台页面 + 爬虫 UA → Flask SSR(OG 标签) │
└───────────────┬─────────────────────────────┘
▼
Gunicorn(2 worker,sync,timeout 600)
Flask 应用工厂 → 蓝图(auth/posts/gallery/videos/
works/checkin/games/pet/site/upload/oss_direct/seo)
│
MySQL 8(personal_site, utf8mb4)
图片/视频 → 阿里云 OSS
DeepSeek API(密钥只在后端)
管理端浏览器 ──STS 临时凭证──► 直传 OSS(图片 PUT / 视频分片,字节不经 ECS)
四条链路值得记住:
- 页面链路:静态文件 Nginx 直接吐,只有爬虫回源 Flask;
- API 链路:
/api/全部反代 Gunicorn; - 图片链路:浏览器访问
wulala.cc/ossimg/xxx,Nginx 走内网 endpoint 回源 OSS——免外网流量费,还顺手剥掉了 OSS 默认域名 2025 年底开始强制加的Content-Disposition: attachment(不加这个,微信爬虫不认图片,分享卡片直接退化); - 上传链路:后台浏览器先向后端换 STS 临时凭证,然后直传 OSS,300MB 的视频也不经过 ECS。
后端只监听 127.0.0.1:5001,对公网完全不可见,所有出入都过 Nginx——限流、真实 IP、HTTPS、超时控制在一处收口。
四、代码怎么组织
单仓库,前后端平级:
self-web/
├── backend/
│ ├── app/
│ │ ├── __init__.py # create_app() 应用工厂
│ │ ├── api/ # 路由层:每个业务域一个蓝图
│ │ ├── models/ # SQLAlchemy 模型
│ │ ├── services/ # storage / sts_service / ai
│ │ ├── utils/ # JWT 装饰器、验证码、markdown、统一响应
│ │ └── seo.py # 爬虫 SSR + sitemap/robots/rss
│ ├── config.py # 全部配置从 .env 注入
│ ├── run.py # 开发入口
│ └── seed.py # 首次建库 + 默认管理员
├── frontend/src/
│ ├── views/ # 访客页面
│ ├── admin/ # 后台页面
│ ├── games/ # 小游戏(注册表 + 各游戏组件)
│ ├── components/ # 导航、灯箱、桌宠等
│ ├── composables/ # usePagedList / useMobile / useSeo
│ ├── stores/ # Pinia:auth / site
│ └── utils/ossUploader.js # 直传/中转统一上传入口
└── deploy/ # systemd、gunicorn、nginx、运维文档
后端的入口是一个很标准的应用工厂,启动时做四件事:装配置、初始化扩展、注册蓝图、建表:
def create_app():
app = Flask(__name__)
app.config.from_object(get_config()) # 按 FLASK_ENV 选 Dev/Prod
db.init_app(app)
cors.init_app(app, resources={r'/api/*': {'origins': app.config['CORS_ORIGINS']}},
supports_credentials=True)
from . import models # 先导入模型
from .api import register_blueprints
register_blueprints(app) # 再注册蓝图
with app.app_context():
db.create_all() # 启动即建表,无需迁移脚本
return app
所有敏感配置——数据库密码、OSS 密钥、AI key——一律走 .env,代码里只留 _env('OSS_ACCESS_KEY_ID') 这样的读取。.env 在 .gitignore 里,仓库里给的是 .env.example。
五、怎么跑起来:本地与生产的差异
本地开发
后端 python run.py 起在 5001(debug 热重载),前端 npm run dev 起在 5174,Vite 把 /api 代理到 5001,CORS 只放行这两个本地端口:
# config.py
class BaseConfig:
CORS_ORIGINS = ['http://127.0.0.1:5174', 'http://localhost:5174']
端口刻意避开 5000/5173 这两个"谁都可能占用"的默认值,省去大量本地端口冲突。
生产部署:同源 + 进程托管
生产环境前后端同源部署,不需要 CORS:Nginx 同一个域名下既托管 dist/ 又反代 /api/。后端进程交给 systemd:
# deploy/personal-site.service
[Service]
Type=simple
WorkingDirectory=/home/luhuan/project/self-web/backend
ExecStart=/home/luhuan/project/self-web/backend/venv/bin/gunicorn \
-w 2 -b 127.0.0.1:5001 --timeout 600 \
--access-logfile - --error-logfile - run:app
Restart=always
RestartSec=3
几个参数背后都是有故事的:
- 2 个 sync worker:个人站流量极小,2 个进程足够;sync 模型配 Gunicorn 默认的多线程心智最简单;
--timeout 600:为兼容早期的"大视频服务器中转上传 OSS",10 分钟超时,Nginx 的proxy_read_timeout必须同步设 600s,否则 Gunicorn 还在传,Nginx 先 504 了(现在已有直传,这个值保留作为回退链路的保障);Restart=always:进程崩了 3 秒后自动拉起,单机场景这就是最朴素有效的"高可用";- 只绑 127.0.0.1:外网打不到 5001。
日常更新代码的流程短到可以记在肌肉里:服务器 git pull → 有新依赖就 pip install → systemctl restart personal-site;前端改动则在本地 build 后 scp 覆盖 dist/。
为什么前端不在服务器上构建
这是一个真实的坑:服务器系统自带 Node 16.20.2,而 Vite 6 要求 Node 18/20+。我没有选择在服务器上折腾 nvm(生产机少装一套版本管理器就少一个变量),而是定了规矩:
本地 Windows(Node 24)构建,
scp -r dist/* 服务器:.../frontend/dist/,服务器不装 Node。
构建产物是纯静态文件,在"哪台机器"构建对运行没有任何影响,而本地构建还顺手能先点一遍验证。这也让 ECS 保持"只装运行时依赖"的干净状态。
六、这一路踩过的坑(开篇先列部署相关的)
- 服务器 Python 也是源码编译的 3.9.18,绑定的 OpenSSL 较老。因此密码哈希我固定用
pbkdf2:sha256,不用较新 Python 默认的 scrypt——后者在老 OpenSSL 上会出问题。 - Nginx 是源码安装在
/usr/local/nginx,不是 yum 版,重载命令是/usr/local/nginx/sbin/nginx -s reload。照着网上教程systemctl reload nginx是找不到服务的。 - HTTP 不强制跳 HTTPS。证书万一过期,
http://wulala.cc仍能完整访问,续期只需替换 pem/key 再 reload,网站不会在证书事故时整体宕掉。 - 上传体积上限要两处对齐:Flask
MAX_CONTENT_LENGTH = 300MB和 Nginxclient_max_body_size 300m,少配任何一边都会在另一头报错。 .env、venv/、uploads/、dist/全部不进 git。这样服务器上git pull永远不会冲掉线上配置和文件;代码源头永远是本地 Windows,服务器改挂了直接git checkout -- .。- 含密码的部署笔记也不要进仓库。我专门在
.gitignore里放了一条服务器配置文件名,敏感信息只留在本地。
七、小结
一个人做站,我的核心原则是三条:
- 数据源唯一:一份内容同时服务真人、爬虫和分享卡片,不做重复维护;
- 薄依赖、厚掌控:框架只选够用的,关键链路(上传、SEO、安全)全部自己掌握原理;
- 部署极简:一台机器、一个 systemd 服务、一份 Nginx 配置,更新流程短到不需要写脚本。
地基交代完了,接下来进入实现细节。下一篇我们钻进后端,看怎么用应用工厂、统一响应信封和配置分层,把 Flask 组织成一个虽然小但"有框架感"的项目——第 2 篇:后端骨架,把 Flask 写成"小框架"。
系列导航:① 开篇与总体架构(本文)→ ② Flask 后端骨架 → ③ 登录与安全 → ④ 博客内容模块 → ⑤ Markdown 编辑器 → ⑥ OSS 直传全攻略 → ⑦ SPA 的 SEO 与微信分享 → ⑧ 打卡热力图与小游戏 → ⑨ AI 网页宠物与收官


