博客部署与运维指南
从零开始的部署教程:服务器环境准备、前后端推送、systemd 服务配置、Nginx 反代、SSL 证书、日常更新、常见问题排查。
博客部署指南
服务器信息
| 项目 | 值 |
|---|---|
| SSH 端口 | 略 |
| 用户 | root(已配密钥免密登录) |
| 项目路径 | /root/blog |
一、快速部署(日常使用)
推荐使用部署脚本:
cd D:\Projects\blog
python deploy/deploy.py
菜单选项:
| 选项 | 功能 | 说明 |
|---|---|---|
| 1 | 完整部署 | 构建前端 → 打包前后端代码 → SCP 上传 → 服务器解压并重启服务 |
| 2 | 数据库同步 | 下载(服务器→本地)/ 上传(本地→服务器),包含数据库 + uploads 文件 |
部署前注意: 生产环境需设置环境变量
SITE_URL=https://blog.vue2.xyz(CORS 白名单依赖此变量)和SECRET_KEY(强随机字符串,见下方 systemd 配置)
数据库同步注意事项
- 下载前会暂停服务器 blog-api 服务,下载完成后自动重启
- 同步包含
blog.db和data/uploads/目录下的所有上传文件及子目录(bed / background / editor) - 不包含
data/pan/目录,需手动 scp 同步 - Windows 下本地 dev server 运行时 SQLite 文件被占用,脚本采用
copy2覆盖写入
二、首次部署(从头搭建)
1. 服务器环境准备
ssh root@43.108.33.153 -p 2894
apt update && apt install -y python3 python3-pip python3-venv nginx certbot python3-certbot-nginx
# Node.js 20+(Ubuntu 默认 v18 不够)
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
mkdir -p /root/blog/data/uploads
2. 推送后端代码
# 本地打包
cd /d/Projects/blog
tar czf backend.tar.gz backend/ --exclude=backend/venv --exclude=backend/__pycache__ --exclude='backend/**/__pycache__'
# 上传
scp -P 2894 backend.tar.gz root@43.108.33.153:/root/blog/
# 服务器解压安装
ssh root@43.108.33.153 -p 2894
cd /root/blog
tar xzf backend.tar.gz
cd backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
3. 推送前端代码
# 本地构建
cd /d/Projects/blog/frontend
npm install
npm run build
# 打包上传
tar czf frontend.tar.gz .output/
scp -P 2894 frontend.tar.gz root@43.108.33.153:/root/blog/
# 服务器解压
ssh root@43.108.33.153 -p 2894
cd /root/blog
tar xzf frontend.tar.gz
4. 配置后端 systemd 服务
ssh root@43.108.33.153 -p 2894
cp /root/blog/deploy/blog-api.service /etc/systemd/system/
# 编辑服务文件,替换 SECRET_KEY
sed -i "s/SECRET_KEY=change-this-in-production/SECRET_KEY=$(openssl rand -hex 32)/" /etc/systemd/system/blog-api.service
systemctl daemon-reload
systemctl enable blog-api
systemctl start blog-api
服务文件参考:
[Unit]
Description=Blog API (FastAPI)
After=network.target
[Service]
User=root
WorkingDirectory=/root/blog/backend
ExecStart=/root/blog/backend/venv/bin/gunicorn -w 2 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8001 main:app
Restart=always
RestartSec=5
Environment=SECRET_KEY=<your-generated-key> # 必填:openssl rand -hex 32 生成
Environment=SITE_URL=https://blog.vue2.xyz # 必填:CORS 白名单来源
[Install]
WantedBy=multi-user.target
注意:
SECRET_KEY默认为dev-secret-change-in-production,生产环境启动时若SITE_URL不是 localhost 会报错退出,强制要求设置环境变量。
5. 配置前端 systemd 服务
cp /root/blog/deploy/blog-web.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable blog-web
systemctl start blog-web
[Unit]
Description=Blog Web (Nuxt SSR)
After=network.target
[Service]
User=root
WorkingDirectory=/root/blog/frontend
ExecStart=/usr/bin/node .output/server/index.mjs
Restart=always
RestartSec=5
Environment=NODE_ENV=production
Environment=NUXT_API_BASE=http://127.0.0.1:8001
Environment=HOST=127.0.0.1
Environment=PORT=3000
[Install]
WantedBy=multi-user.target
6. 配置 Nginx
cp /root/blog/deploy/nginx-blog.conf /etc/nginx/sites-available/blog.vue2.xyz
ln -s /etc/nginx/sites-available/blog.vue2.xyz /etc/nginx/sites-enabled/
nginx -t && nginx -s reload
Nginx 配置要点:
- SSL 使用 vue2.xyz 的通配证书(多域名共用)
/api/*反代到 :8001/uploads/*Nginx 直出,缓存 3 天- 其他路径反代到 Nuxt SSR (:3000)
client_max_body_size 20m支持大图片上传
7. 申请 SSL 证书
certbot --nginx -d blog.vue2.xyz
会自动在现有 vue2.xyz 证书上追加 blog 子域名。如首次申请:
certbot --nginx --expand -d vue2.xyz -d www.vue2.xyz -d blog.vue2.xyz
8. 首次启动初始化
后端首次启动时自动完成:
- 创建 SQLite 数据库及所有表(含
card_styles卡片样式表) - 写入 26 行卡片样式默认值
- 卡片样式可在后台
/admin/settings→「🎴 卡片样式」面板可视化调节
9. 初始化管理员
curl -s -X POST http://127.0.0.1:8001/api/auth/init \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"<your-password>"}'
10. 验证
# API 健康检查
curl http://127.0.0.1:8001/api/health
# 预期: {"status":"ok"}
# 前端 SSR
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/
# 预期: 200
# 管理员登录
curl -s -X POST http://127.0.0.1:8001/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"<your-password>"}'
# 预期: {"access_token":"...","token_type":"bearer"}
# 也可不传 username,单用户自动匹配:
curl -s -X POST http://127.0.0.1:8001/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"password":"<your-password>"}'
# 预期: {"access_token":"...","token_type":"bearer"}
三、日常更新
后端更新(手动)
# 本地
cd /d/Projects/blog
git pull
tar czf backend.tar.gz backend/ --exclude=backend/venv --exclude=backend/__pycache__ --exclude='backend/**/__pycache__'
scp -P 2894 backend.tar.gz root@43.108.33.153:/root/blog/
# 服务器
ssh root@43.108.33.153 -p 2894
cd /root/blog
systemctl stop blog-api
tar xzf backend.tar.gz --overwrite
systemctl start blog-api
前端更新(手动)
# 本地构建打包
cd /d/Projects/blog/frontend
git pull
npm run build
tar czf frontend.tar.gz .output/
scp -P 2894 frontend.tar.gz root@43.108.33.153:/root/blog/
# 服务器
ssh root@43.108.33.153 -p 2894
cd /root/blog
systemctl stop blog-web
rm -rf frontend/.output
tar xzf frontend.tar.gz -C frontend/
systemctl start blog-web
数据库同步
cd D:\Projects\blog
python deploy/deploy.py
# 选择 2 → 1 下载 / 2 上传
下载流程:停止服务器 blog-api → ssh cat 传输 db 到 .tmp → copy2 覆盖本地 → scp 拉取 uploads(含子目录) → 重启 blog-api
上传流程:scp 上传 db → scp 上传 uploads(含子目录) → 重启 blog-api
四、服务器维护
查看服务状态
ssh root@43.108.33.153 -p 2894
systemctl status blog-api # 后端
systemctl status blog-web # 前端 SSR
systemctl status nginx # Nginx
查看日志
ssh root@43.108.33.153 -p 2894
journalctl -u blog-api -n 50 --no-pager -f # 后端日志(实时追踪加 -f)
journalctl -u blog-web -n 50 --no-pager # 前端日志
journalctl -u nginx -n 50 --no-pager # Nginx 日志
tail -f /var/log/nginx/access.log # Nginx 访问日志
tail -f /var/log/nginx/error.log # Nginx 错误日志
重启单个服务
ssh root@43.108.33.153 -p 2894
systemctl restart blog-api # 重启后端
systemctl restart blog-web # 重启前端
nginx -s reload # 重载 Nginx
五、服务器目录结构
/root/blog/
├── backend/ # 后端代码 + venv
│ ├── main.py
│ ├── routers/
│ ├── models.py
│ └── ...
├── frontend/
│ └── .output/ # 前端构建产物(public + server)
├── data/
│ ├── blog.db # SQLite 数据库
│ ├── uploads/
│ │ ├── bed/ # 图床——外链图片(不压缩,原文件)
│ │ ├── background/ # 背景图(自动压缩)
│ │ └── editor/ # 文章编辑器插图(自动压缩)
│ └── pan/ # 网盘文件(需密码验证)
├── deploy/
│ ├── blog-api.service
│ ├── blog-web.service
│ ├── nginx-blog.conf
│ ├── deploy.py # 一键部署脚本
│ └── sync-db.py # 旧版同步脚本
六、端口说明
| 服务 | 端口 | 绑定地址 | 说明 |
|---|---|---|---|
| 前端 SSR | 3000 | 127.0.0.1 | 仅 Nginx 访问 |
| 后端 API | 8001 | 127.0.0.1 | 仅 Nginx 访问 |
| Nginx | 443/80 | 0.0.0.0 | 对外公开 |
| SSH | 2894 | 0.0.0.0 | 非默认端口 |
七、Nginx 路由规则
| 路径 | 目标 | 说明 |
|---|---|---|
/api/* |
FastAPI :8001 | 所有 API 请求 |
/_nuxt/* |
Nuxt SSR :3000 | 前端构建资源 |
/uploads/* |
/root/blog/data/uploads/ |
图片/文件直出,缓存 3 天(/uploads/bed/ 图床、/uploads/editor/ 文章插图、/uploads/background/ 背景图) |
| 其他 | Nuxt SSR :3000 | 页面路由(含 SSR) |
八、常见问题
413 Request Entity Too Large
上传图片时出现此错误 → Nginx 默认 body 大小 1MB,配置中已设 client_max_body_size 20m。如仍有问题,检查配置是否生效。
500 Internal Server Error(文章详情页)
新建文章的 summary 或 cover_image 等字段可能为 NULL,Pydantic v2 严格模式不接受 NULL。解决:更新数据库将 NULL 改为空字符串,schema 加 = "" 默认值。
ssh: connect to host port 22: Connection refused
SSH 端口不是默认 22,需加 -p 2894。
数据库同步时报 Broken pipe
Windows 下本地 dev server 持有的 SQLite 文件句柄导致删除失败。修复:脚本已改为 shutil.copy2 覆盖写入。
登录不传 username 报错
登录接口支持不传 username 参数(单用户自动匹配第一个管理员)。如果请求仍然报错,检查后端是否为最新代码(schemas.py 中 LoginRequest.username 需有 = "" 默认值)。
部署后 SECRET_KEY 错误导致后端启动失败
config.py 在 SITE_URL 不是 localhost 且 SECRET_KEY 为默认值时会抛出 RuntimeError。解决:在 blog-api.service 中正确设置 Environment=SECRET_KEY=<随机字符串>,用 openssl rand -hex 32 生成。
评论
暂无评论,来写第一条吧
