Initial commit: MyBlog full stack blog

This commit is contained in:
2026-08-22 22:28:41 +08:00
commit c61193f2af
64 changed files with 7290 additions and 0 deletions
+476
View File
@@ -0,0 +1,476 @@
# MyBlog 部署指南(Ubuntu + Nginx + FastAPI
本指南覆盖:服务器初始化 → 上传项目 → 安装依赖 → 配置 Nginx / systemd → 开启 HTTPS → 验证上线。
假设你的阿里云服务器为 **Ubuntu 22.04/24.04**,已有公网 IP 与可登录的账号(root 或 sudo 用户)。
> 架构速览:Nginx 提供前端静态文件与 `/uploads/`,并把 `/api/*` 反代到
> `127.0.0.1:8080` 的 FastAPI(只监听本机,公网无法直连);后端使用 SQLite,数据在
> `/var/www/blog/backend/blog.db`。
---
## 一、本地准备:确认要上传的内容
项目根目录(`D:\MyBlog`)需要上传:
| 路径 | 说明 | 是否上传 |
| --- | --- | --- |
| `frontend/` | 前端全部文件(index.html / *.js / style.css | ✅ |
| `backend/` | 后端代码(不含 `blog.db``__pycache__` | ✅ |
| `uploads/` | 上传目录(可传已有文件,如头像 favicon.png) | ✅(可为空) |
| `requirements.txt` | Python 依赖清单 | ✅ |
| `.env.example` | 环境变量模板 | ✅ |
| `README.md` | 项目说明 | ✅ |
| `deploy/` | 部署文件(nginx.conf / myblog.service / 本指南) | ✅ |
| `.env` | 含密钥与邮箱授权码,**不要上传**,到服务器上手动创建 | ❌ |
| `backend/blog.db` | 本地测试数据库,**不要上传**,服务器上重新初始化 | ❌ |
### 1.1 Windows 打包并上传(自带 tar 与 scp)
```powershell
# 在项目目录 D:\MyBlog 下执行:打包(排除本地环境文件)
tar -czf myblog.tar.gz frontend backend uploads deploy requirements.txt .env.example README.md
# 上传到服务器 /tmp(把 root@IP 换成你的实际账号和公网 IP)
scp myblog.tar.gz root@你的公网IP:/tmp/
```
> 提示:Windows 10 1803+ 自带 `tar` / `scp`。也可用 WinSCP / Xftp 直接拖拽,
> 或使用 Git Bash / WSL 里的 `rsync -avz --exclude='.env' --exclude='backend/blog.db' ./ root@IP:/var/www/blog/`。
---
## 二、阿里云安全组开放端口
登录阿里云控制台 → ECS 实例 → 安全组 → 配置规则 → 入方向,**仅开放以下端口**:
| 端口 | 用途 | 建议 |
| --- | --- | --- |
| 22 | SSH 远程管理 | 建议“指定源”只放行你的家庭/办公 IP |
| 80 | HTTPNginx | 0.0.0.0/0 |
| 443 | HTTPSNginx | 0.0.0.0/0(配合 certbot |
| 25565 | Minecraft(独立服务,与博客无关) | 按需开放 |
⚠️ **8080 端口千万不要开放**FastAPI 只监听 `127.0.0.1:8080`,公网即使访问 8080 也会被拒绝;
如果安全组放行 8080 而 Nginx 代理配置有误,等于把后端裸奔在公网上。
---
## 三、服务器初始化
SSH 登录服务器后执行:
```bash
# 1. 系统更新 + 安装 Nginx / Python3 / venv / pip
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx python3 python3-venv python3-pip
# 2. 创建博客专用系统账号(禁止登录,只用于运行后端)
sudo useradd -r -m -s /usr/sbin/nologin blog
# 3. 创建部署目录
sudo mkdir -p /var/www/blog
sudo chown -R blog:blog /var/www/blog
# 4. 解压上传的项目包(由 /tmp 解压到 /var/www/blog
sudo mkdir -p /var/www/blog
cd /tmp && sudo tar -xzf myblog.tar.gz -C /var/www/blog/
sudo chown -R blog:blog /var/www/blog
# 5. 准备证书校验目录(Nginx 配置里用到)
sudo mkdir -p /var/www/blog/.well-known/acme-challenge
sudo chown -R blog:blog /var/www/blog/.well-known
# 6. 确认前端文件就位(页面引用绝对路径 /main.jsroot 必须指向 frontend
ls /var/www/blog/frontend/index.html
```
---
## 四、配置 .env(服务器上手动创建)
`.env` 含密钥与授权码,必须到服务器上创建,不要从本地直接上传:
```bash
cd /var/www/blog
sudo -u blog cp .env.example .env
sudo -u blog nano .env # 或 vi
```
必填项(`nano` 里按 `Ctrl+O` 保存,`Ctrl+X` 退出):
```ini
# 强随机密钥:在服务器上执行下面命令生成,然后粘贴进来
# python3 -c "import secrets; print(secrets.token_urlsafe(48))"
JWT_SECRET=这里粘贴生成的随机密钥
# 博主账号(首次执行 python -m backend.seed 时创建)
BLOGGER_USERNAME=孤竹居士
BLOGGER_EMAIL=guzhujushi2008@163.com
BLOGGER_PASSWORD=你的登录密码
# SMTP 邮箱验证码(163 邮箱 + 授权码)
SMTP_HOST=smtp.163.com
SMTP_PORT=465
SMTP_USER=guzhujushi2008@163.com
SMTP_AUTH_CODE=你的163授权码
EMAIL_FROM=guzhujushi2008@163.com
```
其余可选变量(限流、验证码、评论长度等)不填会使用默认值,说明见 `.env.example`
设置文件权限,防止他人读取密钥:
```bash
sudo chmod 600 /var/www/blog/.env
```
---
## 五、安装依赖 + 初始化数据库
```bash
# 1. 创建虚拟环境并安装依赖(建议先换成国内 pip 镜像,速度更快)
cd /var/www/blog
sudo -u blog python3 -m venv venv
sudo -u blog venv/bin/pip install -r requirements.txt
# 可选加速:-i https://pypi.tuna.tsinghua.edu.cn/simple
# 2. 初始化数据库表(自动创建 backend/blog.db
sudo -u blog venv/bin/python -m backend.database
# 3. 创建唯一博主账号(读取 .env 里的 BLOGGER_*,重复执行会跳过)
sudo -u blog venv/bin/python -m backend.seed
# 4. 检查上传目录可写
sudo -u blog mkdir -p uploads/avatar uploads/article uploads/project
sudo chown -R blog:blog /var/www/blog/uploads
```
---
## 六、部署 Nginx 配置
```bash
# 1. 复制配置文件(内容见 deploy/nginx.conf
sudo cp /var/www/blog/deploy/nginx.conf /etc/nginx/sites-available/myblog
# 2. 把 server_name 改成你的域名(或公网 IP)
sudo nano /etc/nginx/sites-available/myblog
# 3. 启用站点(删除默认站点,避免冲突)
sudo rm -f /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/myblog /etc/nginx/sites-enabled/
# 4. 校验配置并重载
sudo nginx -t
sudo systemctl reload nginx
```
---
## 七、部署 systemd 服务
```bash
# 1. 复制服务文件(内容见 deploy/myblog.service
sudo cp /var/www/blog/deploy/myblog.service /etc/systemd/system/
# 2. 加载并启动
sudo systemctl daemon-reload
sudo systemctl enable --now myblog
# 3. 确认状态:Active: active (running) 表示成功
sudo systemctl status myblog
```
**手动启动方式(调试用,生产请用 systemd)**
```bash
cd /var/www/blog
sudo -u blog /var/www/blog/venv/bin/uvicorn backend.main:app --host 127.0.0.1 --port 8080
```
---
## 八、开启 HTTPSLet's Encrypt,免费证书)
```bash
# 1. 安装 certbot 的 Nginx 插件
sudo apt install -y certbot python3-certbot-nginx
# 2. 一键签发并自动改写 Nginx 配置(会自动加 443 监听与自动续期)
sudo certbot --nginx -d your-domain.com
# 3. 证书自动续期(certbot 已内置定时任务,可验证)
sudo certbot renew --dry-run
```
HTTPS 配置完成后:
- 80 端口会 301 跳转到 443certbot 自动处理)
- 记得把安全组 443 放行(见第二节)
- 建议把 `.env` 里的 `EMAIL_FROM` 等保持不变即可
---
## 九、上线验证清单
```bash
# 1. 后端接口是否通(应返回 {"success":true,...}
curl http://127.0.0.1:8080/api/article/list
# 2. 前端首页是否可访问(应返回 index.html)
curl -I http://127.0.0.1:8080/ # 本机访问后端正常(证明服务活着)
curl -I http://你的公网IP/ # 公网走 Nginx 访问首页
# 公网直接访问 http://公网IP:8080 应超时/拒绝(8080 未放行且只监听本机)
# 3. SPA 刷新是否正常(直接访问子路由应回退到 index.html)
curl -I http://你的公网IP/partition/4
# 4. 上传目录是否可访问(favicon 应为 200
curl -I http://你的公网IP/uploads/avatar/favicon.png
# 5. 查看后端日志是否有报错
journalctl -u myblog -f
# 6. 浏览器验证全流程:
# 打开首页 → 注册/登录 → 文章列表 → 文章详情 → 评论/点赞 → 好友申请
# (博主)登录 → 发文管理 → 上传头像/封面/项目 → 审批好友申请
```
---
## 十、常见问题
| 现象 | 排查方向 |
| --- | --- |
| 页面能打开但接口报错 | `journalctl -u myblog` 看后端日志;确认 `.env``JWT_SECRET` 已配置 |
| 上传图片 404 | `uploads/` 目录权限:`sudo chown -R blog:blog /var/www/blog/uploads` |
| 刷新子页面 404 | Nginx `try_files $uri $uri/ /index.html;` 是否在 `location /` 中 |
| 博主登录失败多次被锁 | 安全组限流策略(默认 5 次/15 分钟),等窗口过期或调大 `.env` 阈值 |
| 验证码发不出去 | 检查 `.env` 的 SMTP 配置与 163 授权码;服务器 465 端口出方向是否被云安全组限制 |
| 修改代码后不生效 | 后端:`sudo systemctl restart myblog`;前端:用 `deploy/update.ps1` 部署(自动换版本号,浏览器强制拉新文件);若仍异常,Ctrl+F5 强刷一次 |
| 8080 被公网扫到 | 确认 uvicorn 只监听 `127.0.0.1`,且安全组未放行 8080 |
---
## 十一、日常运维
```bash
# 查看状态 / 重启 / 日志
sudo systemctl status myblog
sudo systemctl restart myblog
journalctl -u myblog -n 100
# 备份数据库与上传文件(SQLite 单文件 + 目录)
sudo tar -czf backup_$(date +%F).tar.gz -C /var/www/blog backend/blog.db uploads
# 升级代码流程:见下方「更新代码到服务器(增量同步 + 自动重启)」
```
### 更新代码到服务器(增量同步 + 自动重启)
修改完本地代码后,把改动同步到服务器并重启后端,**一条命令完成**:
```bash
# 在本地 Git Bash / WSL 的项目目录执行(root 登录时无需 sudo)
rsync -avz \
--exclude='.env' \
--exclude='backend/blog.db' \
--exclude='venv' \
--exclude='__pycache__' \
--exclude='uploads/' \
./ root@你的公网IP:/var/www/blog/ \
&& ssh root@你的公网IP 'systemctl restart myblog'
```
命令做了什么:
- `rsync` 只把**本地改动过的文件**增量同步到服务器,未变化的自动跳过;
`&&` 表示同步成功后继续通过 SSH 执行重启 → 改完代码跑这一条,就是一次完整更新。
- 只改前端(`frontend/`)时后端无需重启;重启约 1 秒、无副作用,想省心可每次都执行。
注意:
- **不要加 `--delete`**:会删除服务器上本地没有的文件(例如用户通过网页上传的图片)。
- 排除 `uploads/`:它是服务器上的运行时数据(网页上传的文件只存在于服务器)。
若本地新增了静态资源(如头像 favicon)需要上传,去掉 `--exclude='uploads/'` 再执行。
- 非 root 用户:把 `root@` 换成你的用户名,并先在服务器上配置该命令的免密 sudo:
```bash
# 服务器上执行(username 换成你的登录用户)
sudo visudo -f /etc/sudoers.d/blog-update
# 在文件里加入下面一行后保存:
# username ALL=(ALL) NOPASSWD: /bin/systemctl restart myblog
```
之后本地执行:
```bash
rsync -avz --exclude='.env' --exclude='backend/blog.db' --exclude='venv' --exclude='__pycache__' --exclude='uploads/' ./ username@你的公网IP:/var/www/blog/ && ssh username@你的公网IP 'sudo systemctl restart myblog'
```
验证更新是否生效:
```bash
ssh root@你的公网IP 'systemctl status myblog'
curl -s http://你的公网IP/api/article/list # 应返回 {"success":true,...}
```
### 一键更新脚本(推荐,Windows 上直接用)
项目自带两个脚本,不需要 rsync / Git BashWindows 打开 PowerShell 即可:
| 文件 | 作用 |
| --- | --- |
| `deploy/update.ps1` | 本地一键脚本:打包代码 -> 上传 -> 触发服务器更新 |
| `deploy/update_server.sh` | 服务器端脚本:备份 -> 解压 -> 数据库迁移 -> 重启 -> 验证 |
| `deploy/deploy.conf` | 配置服务器地址与域名(`SERVER=` / `DOMAIN=` |
用法:
```powershell
# 在项目根目录 D:\MyBlog 下执行
powershell -ExecutionPolicy Bypass -File .\deploy\update.ps1
# 或指定服务器(覆盖 deploy.conf
powershell -ExecutionPolicy Bypass -File .\deploy\update.ps1 -Server root@8.145.36.108
# 需要把本地 uploads/ 一并同步时(首次部署 / 新增静态资源)
powershell -ExecutionPolicy Bypass -File .\deploy\update.ps1 -IncludeUploads
```
脚本会做的事(与上面 rsync 方案等价且更省心):
- 打包时自动排除 `.env``backend/blog.db``__pycache__``venv`,默认也排除 `uploads/`
- 每次更新前在服务器上自动备份到 `/root/blog-backups/`(含数据库与上传文件),可回滚
- 自动执行幂等数据库迁移(补齐 `articles.category` 等新列),并重启 `myblog` 服务
- 结束后自动验证后端接口与线上 HTTPS 是否正常
### 前端缓存自动清理(更新后无需手动 Ctrl+F5)
页面引用的 `style.css` / `main.js` 已带版本号 `?v=__VERSION__`
- 每次执行 `deploy/update.ps1` 部署时,服务器端会自动把 `__VERSION__` 替换成**当前时间戳**
- 版本号一变化,浏览器就会重新下载最新 JS/CSS,旧缓存自动失效,无需手动清理
- Nginx 同时对前端文件返回 `Cache-Control: no-cache`(每次重新校验),双保险
- 如需手动验证:`curl -s https://你的域名/ | grep main.js` 应能看到带版本号的引用
### SSH 白名单自动更新(家庭公网 IP 变化不锁死)
家庭宽带公网 IP 经常变化,若安全组 22 端口只放行固定 IP,换 IP 后 SSH 会被自己挡在门外。
本项目提供“家庭端定时上报 + 服务器端自动更新安全组”的完整方案,文件均在 `deploy/ipwatch/`
```
家庭电脑 report.ps1(每 30 分钟)---> 博客 /myip 获取当前公网 IP
| IP 有变化时
└--> POST /api/ipwatch/report(携带密钥)
服务器调用阿里云 ECS API:先新增新 IP 规则,成功后再删除旧规则(绝不锁死 SSH)
```
**服务器端配置(一次性):**
1. 在服务器 `.env` 中追加以下变量(AccessKey 只放服务器,不要发给家庭端):
```ini
# 生成随机上报密钥:python3 -c "import secrets; print(secrets.token_urlsafe(24))"
IPWATCH_SECRET=这里粘贴随机密钥
ALIYUN_AK_ID=你的RAM子账号AK
ALIYUN_AK_SECRET=你的RAM子账号SK
IPWATCH_SECURITY_GROUP_ID=sg-0jl65y8luej10xz8neli
IPWATCH_REGION=cn-wulanchabu
IPWATCH_PORT=22
```
2. RAM 子账号只授予该安全组的查询/增/删规则权限(`ecs:DescribeSecurityGroupAttribute`
`ecs:AuthorizeSecurityGroup``ecs:RevokeSecurityGroup`),不要使用主账号密钥。
3. 重新部署代码后重启服务:`sudo systemctl restart myblog`
4. 验证服务状态:`curl -s http://127.0.0.1:8080/api/ipwatch/status`,应返回 `configured: true`
**家庭端安装(Windows,一次性):**
```powershell
cd deploy\ipwatch
Copy-Item ipwatch.conf.example ipwatch.conf
# 用记事本编辑 ipwatch.confSECRET 填与服务器 IPWATCH_SECRET 相同的值
powershell -ExecutionPolicy Bypass -File .\install.ps1 # 注册每 30 分钟的计划任务
Get-Content .\ipwatch.log # 查看首次运行结果
```
**验证方法:**
- 手动模拟一次 IP 变化:调用 `POST /api/ipwatch/report` 上报一个测试 IP,再到阿里云控制台
`deploy/ipwatch` 检查安全组 22 端口规则,确认“先加后删”;随后再上报回真实 IP。
- 说明:测试期间新规则替换旧规则,SSH 会短暂不可用,测试后必须恢复真实 IP。
**注意事项:**
- 安全组中其它端口(80/443/25565 等)和 `0.0.0.0/0` 规则不会被脚本触碰,
它只操作“tcp 22/22 且来源为单个 IP(/32)”的规则。
- 上报接口按来源 IP 限流(默认 60 秒一次,`IPWATCH_REPORT_INTERVAL_SECONDS` 可调)。
- 若上报密钥泄露:改服务器 `.env``IPWATCH_SECRET` → 重启服务 → 同步改家庭端 `ipwatch.conf`
- 阿里云 AccessKey 建议定期在 RAM 控制台轮换,轮换后同步更新服务器 `.env`
### JS→WASM 预编译(把上传项目的 JS 编译为 WASM)
后端提供“把项目演示目录里的 .js 预编译为 .wasm”的功能(基于 Javy / QuickJS),
编译产物与报告存放在 `uploads/demos/{项目id}/wasm/`
**重要说明(先看这里):**
- 浏览器页面**仍然运行原始 JS**。Javy 产物是“QuickJS 解释器 + 字节码”,不能操作
DOM,速度也不如浏览器自带的 V8 JIT,所以它**不能替代**演示页里的 JS。
- 这个功能的真正价值是**服务端沙箱执行**:把不可信的 JS 关进 WASM 沙箱里运行,
隔离文件/网络/系统访问(符合“禁止直接运行用户上传代码”的安全规范)。
**安装编译工具(一次性,服务器上执行):**
```bash
cd /var/www/blog
bash deploy/install_javy.sh # 下载约 14MB,安装到 /usr/local/bin/javy
javy --version # 验证:应输出 javy 9.1.0
```
体积与性能分析:
- Javy 官方 Linux 工具包:约 14MB(下载)/ 约 40MB(解压后),磁盘足够即可。
- 编译产物大小:每个 .wasm 约 300KB1MB(内含 QuickJS 运行时快照 + 你的 JS 字节码)。
- 编译速度:普通几十 KB 的 JS 文件 1 秒内完成;总耗时与文件数成正比。
- 运行性能:QuickJS 是解释器,同类计算基准通常比 V8 JIT 慢数倍到十几倍;
用于沙箱隔离/短任务没问题,不适合做重计算。
**下载压缩包也会被一起打包为 wasm:**
- 每次预编译时,除了演示目录里的 .js,还会把项目的下载压缩包(uploads/project/{uuid}.zip
内的全部 JS 合并打包为单个 wasm,输出到同目录 `uploads/project/{uuid}.wasm`(可直接下载)。
- 说明:zip 本身不是 JS,无法直接编译;这里是把 zip 内的 .js 按文件名排序拼接成一个
临时 bundle 再交给 Javy,产物是“压缩包的 wasm 版本”(演示页仍运行原始 JS)。
- 总量上限默认 10MB`WASM_BUNDLE_MAX_SIZE_MB` 可调),超过会跳过打包并在报告中注明。
**使用方式(任选其一):**
```bash
# 方式一:服务器上手动触发(推荐先用这个验证)
bash deploy/precompile_wasm.sh 1
# 方式二:博主登录后调用接口
curl -X POST http://127.0.0.1:8080/api/project/1/precompile -H "Authorization: Bearer <token>"
# 查看最近一次编译报告(公开接口)
curl -s http://127.0.0.1:8080/api/project/1/wasm-report
```
**可选配置(追加到服务器 .env,重启生效):**
```ini
WASM_COMPILE_ENABLED=true # 总开关
JAVY_PATH=/usr/local/bin/javy # 编译工具路径
WASM_MAX_JS_SIZE_MB=3 # 单个 JS 超过 3MB 跳过
WASM_TIMEOUT_SECONDS=60 # 单文件编译超时
```
运行验证(可选):编译产物可用 wasmtime 执行(服务器已装 v47.0.3,路径 /usr/local/bin/wasmtime):
`ash
wasmtime run uploads/demos/1/wasm/data.wasm # 纯数据脚本可正常退出
`
注意:Javy 下载源是 GitHub Releases,服务器需能访问 GitHub;若失败可在本机下载后
`scp` 到服务器再解压安装(包名:javy-x86_64-linux-v9.1.0.gz)。