准备部署
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# ============================================================
|
||||
# 青叶 (QingYe) —— 生产环境变量模板
|
||||
# ------------------------------------------------------------
|
||||
# 用法:
|
||||
# cp .env.prod.example .env.prod
|
||||
# 然后把下面所有「请替换…」的值改为真实强随机值。
|
||||
#
|
||||
# 生成强随机密钥(在 VPS 上执行):
|
||||
# openssl rand -hex 32
|
||||
#
|
||||
# 注意:
|
||||
# * .env.prod 已在 .gitignore 中,不会被提交。
|
||||
# * POSTGRES_PASSWORD 与 ADMIN_PASSWORD 仅在「首次启动」时写入:
|
||||
# - PostgreSQL 只在数据卷为空时初始化口令,首次启动后修改本文件无效。
|
||||
# - 管理员密码同样只在首次启动写入数据库。
|
||||
# 因此请务必在第一次 docker compose up 之前就把它们设好!
|
||||
# ============================================================
|
||||
|
||||
# ---------- PostgreSQL ----------
|
||||
POSTGRES_DB=qingye
|
||||
POSTGRES_USER=qingye
|
||||
# 必填:用 openssl rand -hex 32 生成
|
||||
# ⚠️ 重要:此口令会被拼入 DATABASE_URL,必须「URL 安全」——只能含字母和数字,
|
||||
# 不能包含 : / @ # ? % & + 空格 等特殊字符(否则连接串解析失败,后端连不上库)。
|
||||
# 所以务必使用 openssl rand -hex 32(纯十六进制)这类无特殊字符的随机串。
|
||||
POSTGRES_PASSWORD=__替换为强随机密码_请用_openssl_rand_hex_32__
|
||||
|
||||
# ---------- 后端密钥 ----------
|
||||
# 必填:用 openssl rand -hex 32 生成(两个用不同的值)
|
||||
JWT_SECRET_KEY=__替换为强随机密钥_请用_openssl_rand_hex_32__
|
||||
JWT_REFRESH_SECRET_KEY=__替换为强随机密钥_请用_openssl_rand_hex_32__
|
||||
|
||||
# CORS 允许来源(生产同源,填你的域名;多个用英文逗号分隔)
|
||||
CORS_ORIGINS=https://www.e4s.world
|
||||
|
||||
# 管理员初始密码(首次启动写入数据库,之后在管理后台修改)
|
||||
ADMIN_PASSWORD=__替换为强密码__
|
||||
|
||||
# ---------- 无需修改 ----------
|
||||
# DATABASE_URL 由 docker-compose.prod.yml 用上面的 POSTGRES_* 自动拼接
|
||||
# REDIS_URL 由 compose 固定为 redis://redis:6379/0
|
||||
@@ -27,6 +27,9 @@ build/
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.production
|
||||
# 生产密钥文件(真实值,绝不提交;.env.prod.example 模板仍可提交)
|
||||
.env.prod
|
||||
.env.prod.local
|
||||
|
||||
# ---------------------
|
||||
# Python 虚拟环境
|
||||
|
||||
@@ -20,6 +20,19 @@ docker compose restart backend # Restart a service
|
||||
|
||||
**Backend** runs on `localhost:8000`, **Frontend** on `localhost:5173`. Both have hot-reload via volume mounts. Database changes requiring new columns need manual `ALTER TABLE` or `docker compose restart backend` (which triggers `Base.metadata.create_all`).
|
||||
|
||||
### Production (dev/prod separation)
|
||||
|
||||
Two **independent, self-contained** Compose files:
|
||||
|
||||
- `docker-compose.yml` — **dev** (hot-reload, `docker compose up`). Leave untouched for local dev.
|
||||
- `docker-compose.prod.yml` — **prod**. Run with:
|
||||
```bash
|
||||
docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
Secrets come from `.env.prod` (gitignored; copy from `.env.prod.example`). **Never run bare `docker compose up` on the server.**
|
||||
|
||||
CRITICAL prod invariant: the backend **must run single-process**. The WebSocket `ConnectionManager` (`backend/app/websocket/manager.py`) is an in-memory singleton — `--workers N` / gunicorn multi-worker splits connections and silently breaks real-time messaging. `backend/Dockerfile.prod` is pinned to single-process; do not change. The frontend prod build is domain-agnostic (same-origin `/api/v1`, auto `wss://`). See [DEPLOYMENT.md](DEPLOYMENT.md) for the full VPS (Ubuntu 20.04 + host Nginx + certbot) walkthrough.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend (FastAPI + SQLAlchemy 2.0 async)
|
||||
|
||||
+416
@@ -0,0 +1,416 @@
|
||||
# 青叶 (QingYe) 生产环境部署指南
|
||||
|
||||
> 目标:把青叶部署到 VPS(Ubuntu 20.04,IP `103.170.72.162`),域名 `www.e4s.world`,使用 **宿主机 Nginx** 作反向代理 + SSL,**Docker Compose** 编排后端容器。
|
||||
|
||||
---
|
||||
|
||||
## 0. 架构总览
|
||||
|
||||
```
|
||||
Internet (HTTPS)
|
||||
│
|
||||
┌─────────▼──────────┐
|
||||
│ 宿主机 Nginx :443 │ ← SSL 证书(Let's Encrypt / certbot)
|
||||
│ www.e4s.world │
|
||||
└───┬──────┬─────┬───┘
|
||||
│ │ │
|
||||
/ 与静态资源 │ /api │ /ws (WebSocket) + /uploads
|
||||
│ │ │
|
||||
┌──────────▼┐ ┌───▼─────▼──────────┐
|
||||
│ frontend │ │ backend │ 仅绑定 127.0.0.1
|
||||
│ 容器:8080 │ │ 容器:8000 │ 不对外公开
|
||||
│ (SPA静态) │ │ (FastAPI 单进程) │
|
||||
└───────────┘ └───┬────────────┬───┘
|
||||
│ │
|
||||
┌───────▼───┐ ┌─────▼─────┐
|
||||
│ postgres │ │ redis │ 仅容器内网,不映射端口
|
||||
└───────────┘ └───────────┘
|
||||
```
|
||||
|
||||
**关键约束(务必读懂一条)**:
|
||||
|
||||
- 后端必须 **单进程** 运行。WebSocket 连接管理器(`backend/app/websocket/manager.py`)是「进程内存」单例;若用 `--workers N` 或 gunicorn 多 worker,连接会被拆分到不同进程,导致聊天、撤回、好友请求等实时消息无法投递。生产镜像 `backend/Dockerfile.prod` 已固定为单进程,**不要改动**。
|
||||
- postgres / redis **不对外映射端口**;backend / frontend 只绑定 `127.0.0.1`。所有外部流量经宿主机 Nginx 进入。
|
||||
- 前端生产构建 **域名无关**:不硬编码域名,API 走同源相对路径 `/api/v1`,WebSocket 自动按页面协议推导为 `wss://<当前域名>`。同一份镜像可部署到任意域名。
|
||||
|
||||
---
|
||||
|
||||
## 1. 前置条件检查
|
||||
|
||||
VPS:Ubuntu 20.04,已安装 Nginx,但 Docker / Docker Compose / Certbot 未装。
|
||||
|
||||
先升级系统并装基础工具:
|
||||
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade -y && sudo apt install -y ca-certificates curl gnupg lsb-release software-properties-common git ufw
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 配置 DNS(先做这一步,证书依赖它)
|
||||
|
||||
在域名服务商为 `e4s.world` 添加解析:
|
||||
|
||||
| 类型 | 主机记录 | 记录值 |
|
||||
|------|---------|--------|
|
||||
| A | www | 103.170.72.162 |
|
||||
|
||||
等待解析生效并**验证**(必须返回你的 VPS IP 才能继续):
|
||||
|
||||
```bash
|
||||
dig +short www.e4s.world
|
||||
# 期望输出:103.170.72.162
|
||||
```
|
||||
|
||||
> 若没有 `dig`:`sudo apt install -y dnsutils`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置防火墙(ufw)
|
||||
|
||||
```bash
|
||||
# 配置放行端口:22(SSH) / 80(HTTP,含 certbot 验证与跳转) / 443(HTTPS)
|
||||
sudo ufw default deny incoming && sudo ufw default allow outgoing && sudo ufw allow 22/tcp && sudo ufw allow 80/tcp && sudo ufw allow 443/tcp
|
||||
|
||||
# 启用防火墙(首次会提示确认,输入 y)并查看状态
|
||||
sudo ufw enable && sudo ufw status verbose
|
||||
```
|
||||
|
||||
> ⚠️ **ufw 与 Docker 的关系**:Docker 会自行写入 iptables NAT 规则,**可能绕过 ufw**。本指南的安全不依赖 ufw,而是靠容器**只绑定 127.0.0.1**。部署完成后请用 `ss -tlnp` 复核 8000 / 8080 仅监听 127.0.0.1(见 [第 9 节](#9-验证清单))。如需对 Docker 做真正的端口过滤,可另外安装 `ufw-docker` 工具(可选)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 安装 Docker 与 Compose 插件(Ubuntu 20.04)
|
||||
|
||||
```bash
|
||||
# 1) 添加 Docker 官方 GPG key 与 apt 仓库(整行执行,无需反斜杠续行)
|
||||
sudo install -m 0755 -d /etc/apt/keyrings && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg && sudo chmod a+r /etc/apt/keyrings/docker.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
|
||||
# 2) 安装 Docker 引擎与 Compose 插件
|
||||
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
# 3) 验证(看到版本号即成功)
|
||||
docker --version && docker compose version
|
||||
```
|
||||
|
||||
> 若你的 VPS 在中国大陆、拉取 Docker 仓库很慢,可配置镜像加速器(编辑 `/etc/docker/daemon.json` 添加 `registry-mirrors`),然后 `sudo systemctl restart docker`。此处从略。
|
||||
|
||||
把当前用户加入 docker 组(免 sudo,需重新登录生效):
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
# 退出重新 SSH 登录后生效;验证:groups 应包含 docker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 获取代码并生成生产密钥
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /opt/qingye && sudo chown $USER:$USER /opt/qingye && cd /opt/qingye
|
||||
|
||||
# 方式一:git clone(替换为你的仓库地址)
|
||||
git clone <你的仓库地址> .
|
||||
|
||||
# 方式二:从本地打包上传
|
||||
# 本地: tar --exclude=node_modules --exclude=.git -czf qingye.tar.gz .
|
||||
# VPS: scp qingye.tar.gz user@103.170.72.162:/tmp/ 然后 tar -xzf /tmp/qingye.tar.gz -C /opt/qingye
|
||||
```
|
||||
|
||||
生成生产环境变量文件(**所有密钥必填**):
|
||||
|
||||
```bash
|
||||
# 复制模板并生成密钥(下面三项各需一个不同的随机值,可重复执行 openssl rand -hex 32)
|
||||
cp .env.prod.example .env.prod && openssl rand -hex 32
|
||||
```
|
||||
|
||||
编辑 `.env.prod`,把所有 `__替换…__` 改为真实值:
|
||||
|
||||
```bash
|
||||
nano .env.prod
|
||||
```
|
||||
|
||||
需要设置的项:
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `POSTGRES_DB` / `POSTGRES_USER` | 一般保持默认 `qingye` 即可 |
|
||||
| `POSTGRES_PASSWORD` | **必填** 强随机密码 |
|
||||
| `JWT_SECRET_KEY` | **必填** `openssl rand -hex 32` |
|
||||
| `JWT_REFRESH_SECRET_KEY` | **必填** 用**另一个**随机值 |
|
||||
| `CORS_ORIGINS` | `https://www.e4s.world`(同源) |
|
||||
| `ADMIN_PASSWORD` | **必填** 管理员初始密码 |
|
||||
|
||||
> 🚨 `POSTGRES_PASSWORD` 与 `ADMIN_PASSWORD` **只在首次启动时写入**:
|
||||
> - PostgreSQL 仅在数据卷为空时初始化口令;首次启动后再改此文件**无效**。
|
||||
> - 管理员密码同样只在首次启动写入数据库。
|
||||
>
|
||||
> 所以务必在**第一次 `docker compose up` 之前**就把它们设好。万一设错了,只能在**没有用户数据**时 `docker compose down -v`(**会清空数据库**)重来。
|
||||
>
|
||||
> 💡 若只是**管理员密码**设错,无需 `down -v`,可参见[第 12 节「轮换管理员密码」](#12-更新--重新部署)用 `DELETE` + 重启的方式无损轮换(保留全部数据)。
|
||||
|
||||
确认 `.env.prod` 不会被提交(应为「ignored」):
|
||||
|
||||
```bash
|
||||
git check-ignore -v .env.prod
|
||||
# 应输出匹配规则,例如:.gitignore:31:.env.prod .env.prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 构建并启动生产容器
|
||||
|
||||
```bash
|
||||
cd /opt/qingye && docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
首次构建会拉取镜像、安装依赖,耗时几分钟。完成后查看状态:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml ps
|
||||
# 四个容器应均为 healthy / running
|
||||
```
|
||||
|
||||
确认后端启动成功(看到「🚀 青叶后端启动完成!」即建表成功):
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml logs --tail=50 backend
|
||||
```
|
||||
|
||||
本地自测(不经域名,直接验证容器):
|
||||
|
||||
```bash
|
||||
curl -I http://127.0.0.1:8080 && curl http://127.0.0.1:8000/
|
||||
# 前端容器应返回 200;后端应返回 {"name":"青叶 QingYe",...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 配置宿主机 Nginx 反向代理
|
||||
|
||||
```bash
|
||||
# 复制站点配置、启用(软链)、移除默认站点、测试并重载
|
||||
sudo cp deploy/nginx.conf /etc/nginx/sites-available/www.e4s.world.conf && sudo ln -sf /etc/nginx/sites-available/www.e4s.world.conf /etc/nginx/sites-enabled/ && sudo rm -f /etc/nginx/sites-enabled/default && sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
此时 `http://www.e4s.world` 应已能打开前端(但仍是 HTTP,WebSocket 在 HTTPS 页面下会被浏览器拦截为 mixed content —— 下一步解决)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 申请 SSL 证书(certbot)
|
||||
|
||||
```bash
|
||||
sudo apt install -y certbot python3-certbot-nginx && sudo certbot --nginx -d www.e4s.world
|
||||
```
|
||||
|
||||
按提示:填邮箱、同意条款、选择是否重定向 HTTP→HTTPS(**选是**)。
|
||||
|
||||
certbot 会自动:
|
||||
|
||||
1. 用 HTTP-01 验证域名;
|
||||
2. 下载证书到 `/etc/letsencrypt/live/www.e4s.world/`;
|
||||
3. 把 `listen 80` 的 server 块改为 `return 301 https://...`;
|
||||
4. 新增 `listen 443 ssl` server 块,并把所有 `location`(含 `/ws`)复制进去。
|
||||
|
||||
certbot 已自动注册 systemd 定时器续期。手动验证续期逻辑:
|
||||
|
||||
```bash
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
重载 Nginx:
|
||||
|
||||
```bash
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### 补充安全响应头(certbot 之后)
|
||||
|
||||
certbot 生成的 443 块不含安全头。编辑 `/etc/nginx/sites-available/www.e4s.world.conf`,在 **443 server 块**内补上:
|
||||
|
||||
```nginx
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
```
|
||||
|
||||
> 注意 Nginx 的 `add_header` 继承规则:若某个 `location` 内已存在 `add_header`/`expires`,则**不会**继承 server 级的头(本配置的 `/uploads/` 即如此)。若需对所有路径生效,需在该 location 内重复声明。
|
||||
|
||||
```bash
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
验证:浏览器打开 `https://www.e4s.world`,开发者工具 → Network → 任一文档请求 → Response Headers,应能看到 `strict-transport-security`、`x-frame-options`、`x-content-type-options`。
|
||||
|
||||
---
|
||||
|
||||
## 9. 验证清单
|
||||
|
||||
部署完成后逐项检查:
|
||||
|
||||
```bash
|
||||
# 1) 端口绑定:8000 / 8080 必须只监听 127.0.0.1(不能是 0.0.0.0 /::*)
|
||||
sudo ss -tlnp | grep -E ':(8000|8080)\b'
|
||||
# 期望看到 127.0.0.1:8000 与 127.0.0.1:8080
|
||||
|
||||
# 2) 从外部无法直连后端(在另一台机器或手机网络执行)
|
||||
curl -m 5 http://103.170.72.162:8000/ # 应失败(连接超时/拒绝)
|
||||
|
||||
# 3) HTTPS 前端
|
||||
curl -I https://www.e4s.world # 200 + 证书有效
|
||||
|
||||
# 4) HTTP 自动跳转 HTTPS
|
||||
curl -I http://www.e4s.world # 301 → https
|
||||
|
||||
# 5) 后端健康
|
||||
curl https://www.e4s.world/ # 前端页面
|
||||
curl https://www.e4s.world/api/v1/ # 经 Nginx 转发到后端(404 也算正常,说明路由通了)
|
||||
|
||||
# 6) WebSocket:浏览器打开 https://www.e4s.world,登录后
|
||||
# 开发者工具 → Network → WS,应看到到 wss://www.e4s.world/ws 的连接(101 Switching Protocols)
|
||||
|
||||
# 7) 数据库表已建好
|
||||
docker compose -f docker-compose.prod.yml exec postgres psql -U qingye -d qingye -c '\dt'
|
||||
# 应列出 users / conversations / messages / moments ... 等表
|
||||
|
||||
# 8) Redis 健康
|
||||
docker compose -f docker-compose.prod.yml exec redis redis-cli ping # PONG
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10.(可选)开机自启 + 崩溃重启
|
||||
|
||||
```bash
|
||||
# 安装单元、重载定义、设置开机自启、立即启动
|
||||
sudo cp deploy/qingye.service /etc/systemd/system/qingye.service && sudo systemctl daemon-reload && sudo systemctl enable qingye && sudo systemctl start qingye
|
||||
|
||||
# 查看状态与日志(journalctl -f 会持续输出,Ctrl+C 退出)
|
||||
sudo systemctl status qingye && sudo journalctl -u qingye -f
|
||||
```
|
||||
|
||||
> 该单元的 `ExecStart` 已显式带 `-f docker-compose.prod.yml` 与 `--env-file .env.prod`,**绝不会误用开发配置**。
|
||||
|
||||
---
|
||||
|
||||
## 11. 备份与恢复(重要)
|
||||
|
||||
数据库数据存放在 Docker 命名卷 `pgdata`,**`docker compose down -v` 会清空全部用户数据**,请勿在生产使用 `-v`。
|
||||
|
||||
### 定时备份(建议加入 cron)
|
||||
|
||||
使用 PostgreSQL 自定义格式(`-Fc`),便于用 `pg_restore --clean` 干净恢复:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /opt/qingye/backups
|
||||
|
||||
# 手动备份一次:
|
||||
docker compose -f docker-compose.prod.yml exec -T postgres pg_dump -Fc -U qingye qingye > /opt/qingye/backups/qingye-$(date +%F).dump
|
||||
|
||||
# 加入 crontab(每天 03:17 备份,保留 14 天):
|
||||
( crontab -l 2>/dev/null; echo "17 3 * * * docker compose -f /opt/qingye/docker-compose.prod.yml exec -T postgres pg_dump -Fc -U qingye qingye > /opt/qingye/backups/qingye-\$(date +\%F).dump && find /opt/qingye/backups -name 'qingye-*.dump' -mtime +14 -delete" ) | crontab -
|
||||
```
|
||||
|
||||
> 同时建议定期备份 `redisdata` 卷(草稿等数据存于 Redis)。
|
||||
|
||||
### 恢复
|
||||
|
||||
`--clean --if-exists` 会先删除已有对象再导入,避免「relation already exists」报错。恢复前**先停后端**,避免恢复期间有写入:
|
||||
|
||||
```bash
|
||||
cd /opt/qingye && docker compose -f docker-compose.prod.yml stop backend && docker compose -f docker-compose.prod.yml exec -T postgres pg_restore --clean --if-exists -U qingye -d qingye < /opt/qingye/backups/qingye-YYYY-MM-DD.dump && docker compose -f docker-compose.prod.yml start backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 更新 / 重新部署
|
||||
|
||||
```bash
|
||||
# 拉取最新代码并重新构建启动(不删数据)
|
||||
cd /opt/qingye && git pull && docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --build
|
||||
|
||||
# 若只改了后端代码,可只重建 backend:
|
||||
docker compose -f docker-compose.prod.yml up -d --build backend
|
||||
```
|
||||
|
||||
> ⚠️ **数据库结构变更**:后端在启动时用 `Base.metadata.create_all` **只创建不存在的表,不会给已有表加列**。若新版后端给某张表新增了字段,必须**在新后端启动前**手动执行 SQL:
|
||||
> ```bash
|
||||
> docker compose -f docker-compose.prod.yml exec postgres psql -U qingye -d qingye -c "ALTER TABLE xxx ADD COLUMN yyy TYPE;"
|
||||
> ```
|
||||
> 否则新后端读不到新列,运行时报错。(后续可引入 Alembic 迁移自动化。)
|
||||
|
||||
### 轮换管理员密码
|
||||
|
||||
管理员密码仅首次启动写入数据库。之后改 `.env.prod` 里的 `ADMIN_PASSWORD` 并重启**不会**生效。要改密码:登录管理后台用接口修改,或:
|
||||
|
||||
```bash
|
||||
# 删除已存的哈希,重启后会用 .env.prod 里的 ADMIN_PASSWORD 重新写入
|
||||
docker compose -f docker-compose.prod.yml exec postgres psql -U qingye -d qingye -c "DELETE FROM system_config WHERE key='admin_password_hash';" && docker compose -f docker-compose.prod.yml restart backend
|
||||
```
|
||||
|
||||
### 轮换 PostgreSQL 口令
|
||||
|
||||
`.env.prod` 里的 `POSTGRES_PASSWORD` 首次启动后改了无效。要在已有实例上改口令:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml exec postgres psql -U qingye -d qingye -c "ALTER USER qingye PASSWORD '新口令';"
|
||||
# 同步修改 .env.prod 里的 POSTGRES_PASSWORD,再重启 backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. 常用运维命令
|
||||
|
||||
```bash
|
||||
# 查看状态
|
||||
docker compose -f docker-compose.prod.yml ps
|
||||
|
||||
# 查看实时日志
|
||||
docker compose -f docker-compose.prod.yml logs -f --tail=200 backend
|
||||
docker compose -f docker-compose.prod.yml logs -f --tail=200 frontend
|
||||
|
||||
# 重启某服务
|
||||
docker compose -f docker-compose.prod.yml restart backend
|
||||
|
||||
# 停止(保留数据)
|
||||
docker compose -f docker-compose.prod.yml down
|
||||
|
||||
# 进入后端容器排查
|
||||
docker compose -f docker-compose.prod.yml exec backend bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 常见问题排查
|
||||
|
||||
| 现象 | 排查方向 |
|
||||
|------|---------|
|
||||
| 浏览器白屏 / 404 | 前端容器是否运行:`curl -I http://127.0.0.1:8080`;Nginx `location /` 是否转发到 8080 |
|
||||
| 刷新 `/chat` 等子路由 404 | 前端容器 `nginx.conf` 是否有 `try_files $uri $uri/ /index.html`(已内置) |
|
||||
| 登录后无法收消息 / 一直转圈 | WebSocket 未通:检查 `wss://www.e4s.world/ws`;确认 Nginx `/ws` 块有 `proxy_http_version 1.1` + `Upgrade`/`Connection` 头 |
|
||||
| 上传头像失败 (413) | `client_max_body_size`(Nginx)与后端 `MAX_UPLOAD_SIZE_MB` 不匹配,调大前者 |
|
||||
| 上传后图片不显示 | Nginx `/uploads/` 的 `proxy_pass` 不能带尾部斜杠(否则前缀被剥,404) |
|
||||
| certbot 失败 | DNS 未生效(`dig +short www.e4s.world`);80 端口被防火墙挡;触发了 Let's Encrypt 限频(5 次/小时) |
|
||||
| `compose` 报「请在 .env.prod 中设置…」 | 漏了 `--env-file .env.prod`,或 `.env.prod` 里对应变量为空 |
|
||||
| 改了 `.env.prod` 没生效 | 见 [第 12 节](#12-更新--重新部署) 关于「首次启动写入」的说明 |
|
||||
|
||||
---
|
||||
|
||||
## 15. 已知限制(架构层面)
|
||||
|
||||
1. **单进程后端**:当前实时消息依赖进程内存连接管理器,无法横向扩展(加机器 / 多 worker)。扩展前需先把 `ConnectionManager` 改为基于 Redis pub/sub。
|
||||
2. **无数据库迁移工具**:`create_all` 只建新表,结构变更需手动 SQL(见第 12 节)。后续建议引入 Alembic。
|
||||
3. **上传文件无鉴权读取**:`/uploads/<文件名>` 可被任何知道路径的人访问(沿用现有设计)。生产 Nginx 已加 `X-Content-Type-Options: nosniff` 缓解存储型 XSS;如需严格鉴权,需改造后端。
|
||||
|
||||
---
|
||||
|
||||
## 附:开发环境(本机)
|
||||
|
||||
开发环境与本指南的生产环境**完全独立**,命令不变:
|
||||
|
||||
```bash
|
||||
docker compose up --build # 走 docker-compose.yml(开发),含热重载
|
||||
docker compose down
|
||||
```
|
||||
|
||||
开发用前端变量见 `frontend/.env.development`,生产构建域名无关、无需配置。
|
||||
@@ -0,0 +1,34 @@
|
||||
# 排除无需进入镜像的文件,减小体积、避免泄密
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
.Python
|
||||
|
||||
# 环境变量(含密钥,绝不能打进镜像)
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# 运行时数据
|
||||
uploads/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# 测试与缓存
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# 版本控制与文档(镜像不需要)
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Dockerfile 本身
|
||||
Dockerfile
|
||||
Dockerfile.prod
|
||||
@@ -0,0 +1,37 @@
|
||||
# ============================================================
|
||||
# 青叶 —— 生产环境后端镜像
|
||||
# ------------------------------------------------------------
|
||||
# 与开发用 Dockerfile 的区别:
|
||||
# 1. 不切换国内镜像源(使用 Debian / PyPI 官方源,适合全球部署)
|
||||
# 2. 运行命令固定为「单进程 uvicorn + proxy-headers」,不带 --reload
|
||||
# 3. 不挂载源码(运行镜像内 COPY 进来的代码)
|
||||
#
|
||||
# ⚠️ 关键约束:必须单进程!WebSocket 连接管理器(ConnectionManager)是
|
||||
# 进程内存单例。若改用 --workers N 或 gunicorn 多 worker,连接会被拆分到
|
||||
# 不同进程,导致跨用户实时消息(聊天、撤回、好友请求等)无法投递。
|
||||
# 水平扩展前需先把 manager 迁移到 Redis pub/sub。
|
||||
# ============================================================
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖(使用官方源,便于全球部署)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 Python 依赖
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制项目代码(backend/.dockerignore 已排除 __pycache__/.env/uploads 等)
|
||||
COPY . .
|
||||
|
||||
# 创建上传目录
|
||||
RUN mkdir -p /app/uploads
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# 生产运行:单 worker + proxy-headers(信任 Nginx 转发的 X-Forwarded-*)
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]
|
||||
@@ -73,4 +73,9 @@ class ConnectionManager:
|
||||
|
||||
|
||||
# 全局单例
|
||||
# ⚠️ 重要约束:本管理器为「进程内存」单例 —— active_connections 只存在于
|
||||
# 当前进程。因此后端必须以「单进程」方式运行(生产镜像 Dockerfile.prod
|
||||
# 已固定为单 worker uvicorn)。若使用 --workers N 或 gunicorn 多 worker,
|
||||
# 连接会分散到不同进程,跨用户 / 跨标签页的实时消息(聊天、撤回、好友请求、
|
||||
# 互动通知等)将无法投递。水平扩展前需先将其迁移到基于 Redis 的 pub/sub。
|
||||
manager = ConnectionManager()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
计划将这个项目部署到生产环境,域名为www.e4s.world,使用Nginx作为反向代理,使用Docker Compose作为容器编排。VPS的IP为:103.170.72.162。请修改相关代码,以便项目能区分生产环境和开发环境。
|
||||
之后,生成一个md文件,告知如何在VPS中执行部署命令。VPS的OS是Ubuntu 20.04。安装了nginx,但其他必要的软件包未安装。
|
||||
@@ -0,0 +1,93 @@
|
||||
# ============================================================
|
||||
# 青叶 (QingYe) —— 宿主机 Nginx 反向代理配置
|
||||
# ------------------------------------------------------------
|
||||
# 目标域名:www.e4s.world
|
||||
# 部署步骤:
|
||||
# 1) 复制到 /etc/nginx/sites-available/www.e4s.world.conf
|
||||
# 2) ln -s /etc/nginx/sites-available/www.e4s.world.conf /etc/nginx/sites-enabled/
|
||||
# 3) 删除默认站点(如有冲突):rm /etc/nginx/sites-enabled/default
|
||||
# 4) nginx -t && systemctl reload nginx
|
||||
# 5) 确认 DNS 已生效且 http://www.e4s.world 能打开前端
|
||||
# 6) 申请 SSL 证书:certbot --nginx -d www.e4s.world
|
||||
# certbot 会自动把 80 端口改为 301 跳转到 443,并将下列 location
|
||||
# 复制到新增的 443 server 块(WebSocket 也由 443 处理)。
|
||||
# ------------------------------------------------------------
|
||||
# 流量走向:
|
||||
# https://www.e4s.world/ → 127.0.0.1:8080 (前端容器, SPA)
|
||||
# https://www.e4s.world/api/... → 127.0.0.1:8000 (后端容器)
|
||||
# https://www.e4s.world/uploads/ → 127.0.0.1:8000 (后端 StaticFiles)
|
||||
# wss://www.e4s.world/ws → 127.0.0.1:8000 (后端 WebSocket)
|
||||
# ============================================================
|
||||
|
||||
upstream qingye_frontend {
|
||||
server 127.0.0.1:8080;
|
||||
}
|
||||
|
||||
upstream qingye_backend {
|
||||
server 127.0.0.1:8000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name www.e4s.world;
|
||||
|
||||
# 上传体积上限:须 >= 后端 MAX_UPLOAD_SIZE_MB(默认 10MB)。
|
||||
# 这里设 12MB 为 multipart 编码留余量;若调大后端限制需同步调大此值。
|
||||
client_max_body_size 12M;
|
||||
|
||||
# 基础安全响应头(HSTS 仅在 HTTPS 下有意义,须在 certbot 生成 443 块后补上,见 DEPLOYMENT.md)
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
|
||||
# ---- 前端 SPA(默认路由,最低优先级)----
|
||||
location / {
|
||||
proxy_pass http://qingye_frontend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# ---- API 接口(proxy_pass 不带尾部斜杠,保留 /api/ 前缀)----
|
||||
location /api/ {
|
||||
proxy_pass http://qingye_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# ---- 上传文件(后端 StaticFiles 挂载于 /uploads)----
|
||||
# nosniff:防止用户上传的 .html/.svg 在同源执行(避免读取 localStorage token 的存储型 XSS)
|
||||
location /uploads/ {
|
||||
proxy_pass http://qingye_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
expires 7d;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ---- WebSocket(精确匹配 /ws,优先级高于上面的 location /)----
|
||||
# 注意:必须 proxy_http_version 1.1 + Upgrade/Connection,否则握手失败。
|
||||
location = /ws {
|
||||
proxy_pass http://qingye_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# 长连接超时(秒),避免空闲被切断
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
# 不记录含 token 的查询串,避免 JWT 落入访问日志
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# ============================================================
|
||||
# 青叶 (QingYe) —— systemd 服务单元(开机自启 + 崩溃重启)
|
||||
# ------------------------------------------------------------
|
||||
# 安装:
|
||||
# sudo cp deploy/qingye.service /etc/systemd/system/qingye.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable qingye # 开机自启
|
||||
# sudo systemctl start qingye
|
||||
#
|
||||
# 查看状态: sudo systemctl status qingye
|
||||
# 查看日志: sudo journalctl -u qingye -f
|
||||
#
|
||||
# ⚠️ ExecStart 必须显式带 -f docker-compose.prod.yml,
|
||||
# 绝不能运行裸 docker compose up(否则可能误用开发配置)。
|
||||
# ============================================================
|
||||
[Unit]
|
||||
Description=QingYe (青叶) Production Stack (Docker Compose)
|
||||
Requires=docker.service
|
||||
After=docker.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=/opt/qingye
|
||||
# 注意:--env-file 让 ${VAR} 插值来自 .env.prod;-f 固定使用生产编排文件
|
||||
ExecStart=/usr/bin/docker compose --env-file .env.prod -f docker-compose.prod.yml up -d
|
||||
ExecStop=/usr/bin/docker compose --env-file .env.prod -f docker-compose.prod.yml down
|
||||
TimeoutStartSec=0
|
||||
User=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,115 @@
|
||||
# ============================================================
|
||||
# 青叶 (QingYe) —— 生产环境 Docker Compose(自包含)
|
||||
# ------------------------------------------------------------
|
||||
# 启动命令:
|
||||
# docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --build
|
||||
#
|
||||
# 说明:
|
||||
# * 此文件为「生产专用」,与开发用的 docker-compose.yml 完全独立。
|
||||
# 生产服务器上请始终带 -f docker-compose.prod.yml,切勿运行裸 docker compose up。
|
||||
# * 所有密钥通过 --env-file .env.prod 注入(密钥文件已在 .gitignore 中)。
|
||||
# * 后端必须「单进程」运行:WebSocket 连接管理器为进程内存单例
|
||||
# (见 backend/app/websocket/manager.py),多 worker / gunicorn 会导致
|
||||
# 跨用户、跨标签页的实时消息丢失。镜像 Dockerfile.prod 已固定为单进程。
|
||||
# * postgres / redis 不向主机暴露端口,仅容器内网互通。
|
||||
# * backend / frontend 仅绑定 127.0.0.1,由宿主机 Nginx 反向代理对外。
|
||||
# ============================================================
|
||||
|
||||
services:
|
||||
# ==================== PostgreSQL ====================
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: qingye-postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-qingye}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-qingye}
|
||||
# 密钥必填:缺失时 compose 会直接报错,绝不回退到弱口令
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?请在 .env.prod 中设置 POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-qingye} -d ${POSTGRES_DB:-qingye}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- qingye-network
|
||||
# 生产环境不向宿主机映射 5432 端口(安全)
|
||||
|
||||
# ==================== Redis ====================
|
||||
# Redis 非可选:flash_service 原子计数 / draft_service 草稿等依赖它
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: qingye-redis
|
||||
restart: always
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- qingye-network
|
||||
# 生产环境不向宿主机映射 6379 端口(安全)
|
||||
|
||||
# ==================== Backend (FastAPI) ====================
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.prod
|
||||
container_name: qingye-backend
|
||||
restart: always
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000" # 仅本机可达,由宿主机 Nginx 反向代理
|
||||
environment:
|
||||
# DATABASE_URL 由上面的 POSTGRES_* 变量自动拼接,保持口令一致
|
||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-qingye}:${POSTGRES_PASSWORD:?请在 .env.prod 中设置 POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-qingye}
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
JWT_SECRET_KEY: ${JWT_SECRET_KEY:?请在 .env.prod 中设置 JWT_SECRET_KEY}
|
||||
JWT_REFRESH_SECRET_KEY: ${JWT_REFRESH_SECRET_KEY:?请在 .env.prod 中设置 JWT_REFRESH_SECRET_KEY}
|
||||
CORS_ORIGINS: ${CORS_ORIGINS:-https://www.e4s.world}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?请在 .env.prod 中设置 ADMIN_PASSWORD}
|
||||
APP_ENV: production
|
||||
volumes:
|
||||
- upload_data:/app/uploads # 仅持久化用户上传文件,不挂载源码(运行镜像内已打包的代码)
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- qingye-network
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
# ==================== Frontend (Vue 3 构建产物 + Nginx) ====================
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.prod
|
||||
# 生产构建为「域名无关」:不传入任何 VITE_* 变量,
|
||||
# 前端代码会自动使用页面同源地址(/api/v1 与 wss://当前域名)。
|
||||
container_name: qingye-frontend
|
||||
restart: always
|
||||
ports:
|
||||
- "127.0.0.1:8080:80" # 仅本机可达,由宿主机 Nginx 反向代理
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- qingye-network
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
# ==================== 数据卷 ====================
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
upload_data:
|
||||
|
||||
# ==================== 网络 ====================
|
||||
networks:
|
||||
qingye-network:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,23 @@
|
||||
# 排除无需进入构建上下文的文件
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
# 环境变量(含密钥/本地配置,不应进入镜像)—— 与 backend/.dockerignore 保持一致
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# 日志
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# 版本控制与编辑器
|
||||
.git/
|
||||
.gitignore
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Dockerfile 本身
|
||||
Dockerfile
|
||||
Dockerfile.prod
|
||||
@@ -0,0 +1,4 @@
|
||||
# 开发环境前端变量(vite dev 在 development 模式下自动加载此文件)
|
||||
# 生产构建(vite build)不会读取此文件 —— 生产环境前端使用页面同源地址。
|
||||
VITE_API_BASE_URL=http://localhost:8000
|
||||
VITE_WS_BASE_URL=ws://localhost:8000
|
||||
@@ -0,0 +1,41 @@
|
||||
# ============================================================
|
||||
# 青叶 —— 生产环境前端镜像(多阶段构建)
|
||||
# Stage 1: Node 构建 Vite 生产包
|
||||
# Stage 2: Nginx 提供静态文件 + SPA 路由回退
|
||||
# ------------------------------------------------------------
|
||||
# 构建「域名无关」:不设置任何 VITE_* 变量。前端代码在没有 VITE_API_BASE_URL /
|
||||
# VITE_WS_BASE_URL 时,会自动使用页面同源地址(API 走相对路径 /api/v1,
|
||||
# WebSocket 走 wss://<当前域名>)。因此同一份镜像可部署到任意域名。
|
||||
# ============================================================
|
||||
|
||||
# ===== Stage 1: 构建 =====
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制依赖描述(package-lock.json 可选 —— 仓库未提交锁文件,用 npm install)
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
# 安装依赖(使用 npm 官方源)
|
||||
RUN npm install
|
||||
|
||||
# 复制源码(frontend/.dockerignore 已排除 node_modules / dist / .env 等)
|
||||
COPY . .
|
||||
|
||||
# 直接调用 vite build,跳过 vue-tsc 类型检查
|
||||
# (避免仓库中既有的类型错误阻断生产部署;类型检查请在 CI / 发布前单独执行)
|
||||
# 如需严格的类型门禁,可改为 `npm run build`。
|
||||
RUN npx vite build
|
||||
|
||||
# ===== Stage 2: 运行(Nginx 静态服务) =====
|
||||
FROM nginx:alpine
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# 复制 SPA 专用 Nginx 配置(createWebHistory 需要 try_files 回退)
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,28 @@
|
||||
# 青叶前端容器 Nginx 配置(提供 SPA 静态文件)
|
||||
# 仅负责静态资源与前端路由回退;API / WebSocket / 上传文件由「宿主机 Nginx」转发到后端。
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# gzip 压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css application/json application/javascript application/xml text/xml application/xml+rss text/javascript image/svg+xml;
|
||||
|
||||
# SPA 路由回退:Vue Router 使用 createWebHistory(HTML5 history 模式),
|
||||
# 刷新 /chat、/moments 等前端路由时需回退到 index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 静态资源长缓存(Vite 产物带 hash 文件名,可安全强缓存)
|
||||
location ~* \.(?:js|css|woff2?|ttf|eot|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import axios, { type AxiosInstance } from 'axios'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
|
||||
// 域名无关:未显式配置 VITE_API_BASE_URL 时使用「同源相对地址」,
|
||||
// 即请求发往当前页面所在的域名(生产环境与前端同源)。
|
||||
// 开发时由 frontend/.env.development 提供 http://localhost:8000。
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || ''
|
||||
|
||||
const api: AxiosInstance = axios.create({
|
||||
baseURL: `${API_BASE}/api/v1`,
|
||||
|
||||
@@ -2,7 +2,11 @@ import { ref } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
|
||||
const WS_BASE = import.meta.env.VITE_WS_BASE_URL || 'ws://localhost:8000'
|
||||
// 域名无关:未显式配置时根据当前页面协议自动推导(https→wss, http→ws)。
|
||||
// 开发时由 frontend/.env.development 提供 ws://localhost:8000。
|
||||
const WS_BASE =
|
||||
import.meta.env.VITE_WS_BASE_URL ||
|
||||
`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}`
|
||||
|
||||
let ws: WebSocket | null = null
|
||||
let reconnectAttempts = 0
|
||||
|
||||
Reference in New Issue
Block a user