19 KiB
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)
# 在项目目录 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 | HTTP(Nginx) | 0.0.0.0/0 |
| 443 | HTTPS(Nginx) | 0.0.0.0/0(配合 certbot) |
| 25565 | Minecraft(独立服务,与博客无关) | 按需开放 |
⚠️ 8080 端口千万不要开放:FastAPI 只监听 127.0.0.1:8080,公网即使访问 8080 也会被拒绝;
如果安全组放行 8080 而 Nginx 代理配置有误,等于把后端裸奔在公网上。
三、服务器初始化
SSH 登录服务器后执行:
# 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.js,root 必须指向 frontend)
ls /var/www/blog/frontend/index.html
四、配置 .env(服务器上手动创建)
.env 含密钥与授权码,必须到服务器上创建,不要从本地直接上传:
cd /var/www/blog
sudo -u blog cp .env.example .env
sudo -u blog nano .env # 或 vi
必填项(nano 里按 Ctrl+O 保存,Ctrl+X 退出):
# 强随机密钥:在服务器上执行下面命令生成,然后粘贴进来
# 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。
设置文件权限,防止他人读取密钥:
sudo chmod 600 /var/www/blog/.env
五、安装依赖 + 初始化数据库
# 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 配置
# 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 服务
# 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):
cd /var/www/blog
sudo -u blog /var/www/blog/venv/bin/uvicorn backend.main:app --host 127.0.0.1 --port 8080
八、开启 HTTPS(Let's Encrypt,免费证书)
# 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 跳转到 443(certbot 自动处理)
- 记得把安全组 443 放行(见第二节)
- 建议把
.env里的EMAIL_FROM等保持不变即可
九、上线验证清单
# 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 |
十一、日常运维
# 查看状态 / 重启 / 日志
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
# 升级代码流程:见下方「更新代码到服务器(增量同步 + 自动重启)」
更新代码到服务器(增量同步 + 自动重启)
修改完本地代码后,把改动同步到服务器并重启后端,一条命令完成:
# 在本地 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:
# 服务器上执行(username 换成你的登录用户)
sudo visudo -f /etc/sudoers.d/blog-update
# 在文件里加入下面一行后保存:
# username ALL=(ALL) NOPASSWD: /bin/systemctl restart myblog
之后本地执行:
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'
验证更新是否生效:
ssh root@你的公网IP 'systemctl status myblog'
curl -s http://你的公网IP/api/article/list # 应返回 {"success":true,...}
一键更新脚本(推荐,Windows 上直接用)
项目自带两个脚本,不需要 rsync / Git Bash,Windows 打开 PowerShell 即可:
| 文件 | 作用 |
|---|---|
deploy/update.ps1 |
本地一键脚本:打包代码 -> 上传 -> 触发服务器更新 |
deploy/update_server.sh |
服务器端脚本:备份 -> 解压 -> 数据库迁移 -> 重启 -> 验证 |
deploy/deploy.conf |
配置服务器地址与域名(SERVER= / DOMAIN=) |
用法:
# 在项目根目录 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)
服务器端配置(一次性):
- 在服务器
.env中追加以下变量(AccessKey 只放服务器,不要发给家庭端):
# 生成随机上报密钥: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
- RAM 子账号只授予该安全组的查询/增/删规则权限(
ecs:DescribeSecurityGroupAttribute、ecs:AuthorizeSecurityGroup、ecs:RevokeSecurityGroup),不要使用主账号密钥。 - 重新部署代码后重启服务:
sudo systemctl restart myblog - 验证服务状态:
curl -s http://127.0.0.1:8080/api/ipwatch/status,应返回configured: true。
家庭端安装(Windows,一次性):
cd deploy\ipwatch
Copy-Item ipwatch.conf.example ipwatch.conf
# 用记事本编辑 ipwatch.conf:SECRET 填与服务器 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 沙箱里运行, 隔离文件/网络/系统访问(符合“禁止直接运行用户上传代码”的安全规范)。
安装编译工具(一次性,服务器上执行):
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 约 300KB~1MB(内含 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 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,重启生效):
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)。