Files

19 KiB
Raw Permalink Blame History

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 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 登录服务器后执行:

# 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 含密钥与授权码,必须到服务器上创建,不要从本地直接上传:

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

八、开启 HTTPSLet'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 跳转到 443certbot 自动处理)
  • 记得把安全组 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 看后端日志;确认 .envJWT_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 BashWindows 打开 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 方案等价且更省心):

  • 打包时自动排除 .envbackend/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 只放服务器,不要发给家庭端):
# 生成随机上报密钥: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
  1. RAM 子账号只授予该安全组的查询/增/删规则权限(ecs:DescribeSecurityGroupAttributeecs:AuthorizeSecurityGroupecs:RevokeSecurityGroup),不要使用主账号密钥。
  2. 重新部署代码后重启服务:sudo systemctl restart myblog
  3. 验证服务状态: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.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 可调)。
  • 若上报密钥泄露:改服务器 .envIPWATCH_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 约 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)。
  • 总量上限默认 10MBWASM_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)。