Files
guzhujushiBlog/deploy/DEPLOY.md
T

477 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)。