# 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 | 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 登录服务器后执行: ```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.js,root 必须指向 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 ``` --- ## 八、开启 HTTPS(Let'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 跳转到 443(certbot 自动处理) - 记得把安全组 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 Bash,Windows 打开 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.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 沙箱里运行, 隔离文件/网络/系统访问(符合“禁止直接运行用户上传代码”的安全规范)。 **安装编译工具(一次性,服务器上执行):** ```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 约 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 # 方式一:服务器上手动触发(推荐先用这个验证) bash deploy/precompile_wasm.sh 1 # 方式二:博主登录后调用接口 curl -X POST http://127.0.0.1:8080/api/project/1/precompile -H "Authorization: Bearer " # 查看最近一次编译报告(公开接口) 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)。