mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-13 20:51:09 +00:00
chore(deploy): 成品部署链路、依赖分组与运维基建
部署方式:服务器不再接收源码、不执行构建,改为本地构建成品后交付
- deploy-artifacts.sh:构建→备份→校验→原子替换,支持回滚、镜像/备份轮转、
mysql 与 postgres 双变体镜像;后端以 docker save|ssh|docker load 交付
- 依赖分组:requirements/{base,storage}.txt + db/{mysql,postgres,sqlite}.txt,
镜像按所选数据库只装对应驱动(MySQL 变体 565MB → 521MB)
编排与运行时加固
- compose:MySQL/Redis/backend 端口仅绑 127.0.0.1;密钥(SECRET_KEY、
DATA_ENCRYPTION_KEY、DATA_ENCRYPTION_OLD_KEYS)、PROD_CORS_ORIGINS、
FORWARDED_ALLOW_IPS、DEMO_ENABLE 一律由 docker/.env 注入
- Dockerfile:对齐依赖分组、以非 root(uid=1000) 运行;.dockerignore 排除全部真实 .env.*
- nginx:静态资源长缓存、隐藏文件拒绝、接口文档仅内网可访问、放行 ACME http-01 校验路径
工程基建
- 新增 CI:backend(uv sync + ruff + pytest)与 frontend(web type-check+vitest、app type-check)
- 新增 DEPLOYMENT.md 运维手册(部署/回滚/密钥轮换/每日巡检/证书/排查)
This commit is contained in:
@@ -15,7 +15,12 @@ __pycache__
|
|||||||
# 虚拟环境
|
# 虚拟环境
|
||||||
venv
|
venv
|
||||||
.venv
|
.venv
|
||||||
|
# 环境变量文件:必须排除所有真实密钥文件(.env 只能匹配到名为 .env 的文件,
|
||||||
|
# 匹配不到 .env.prod / .env.dev / .env.test —— 它们含真实口令与 API Key)
|
||||||
.env
|
.env
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
!**/.env.example
|
||||||
|
|
||||||
# 编辑器/IDE
|
# 编辑器/IDE
|
||||||
.vscode
|
.vscode
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
name: backend
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "backend/**"
|
||||||
|
- ".github/workflows/backend-ci.yml"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "backend/**"
|
||||||
|
- ".github/workflows/backend-ci.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: backend
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 安装 uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: 安装依赖(含 sqlite 与 mysql 驱动;测试用 sqlite + fakeredis)
|
||||||
|
run: uv sync --extra sqlite --extra mysql
|
||||||
|
|
||||||
|
- name: 代码检查(ruff)
|
||||||
|
run: uv run ruff check
|
||||||
|
|
||||||
|
- name: 单元测试(pytest)
|
||||||
|
run: uv run pytest -q
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
name: frontend
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "frontend/**"
|
||||||
|
- ".github/workflows/frontend-ci.yml"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "frontend/**"
|
||||||
|
- ".github/workflows/frontend-ci.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
web:
|
||||||
|
name: web(类型检查 + 单元测试)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: frontend/web
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: frontend/web/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: 类型检查
|
||||||
|
run: pnpm run type-check
|
||||||
|
|
||||||
|
- name: 单元测试
|
||||||
|
run: pnpm run test
|
||||||
|
|
||||||
|
app:
|
||||||
|
name: app(类型检查)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: frontend/app
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: frontend/app/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: 类型检查
|
||||||
|
run: pnpm run type-check
|
||||||
+3
-1
@@ -73,4 +73,6 @@ CLAUDE.md
|
|||||||
AGENTS.md
|
AGENTS.md
|
||||||
.codebuddy
|
.codebuddy
|
||||||
.agent
|
.agent
|
||||||
.workbuddy
|
.workbuddy
|
||||||
|
# AgentTeams 协作运行期状态(本地,不入库)
|
||||||
|
.agent-teams/
|
||||||
|
|||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
# FastapiAdmin 部署运维手册
|
||||||
|
|
||||||
|
> 适用环境:阿里云单机 Docker 部署(`service.fastapiadmin.com`)
|
||||||
|
> 最近更新:2026-10-04(本轮密钥轮换与加固之后)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 架构与目录
|
||||||
|
|
||||||
|
| 组件 | 说明 | 对外端口 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| nginx | TLS 终止、静态站点、反向代理 `/api/v1` | 80 / 443 |
|
||||||
|
| backend | FastAPI(uvicorn,1 worker,非 root 运行 uid=1000) | 仅 127.0.0.1:8001 |
|
||||||
|
| mysql | MySQL 8.0,数据卷 `mysql_data` | 仅 127.0.0.1:3306 |
|
||||||
|
| redis | Redis 7(会话/缓存/限流) | 仅 127.0.0.1:6379 |
|
||||||
|
|
||||||
|
服务器布局(`/home/FastapiAdmin`):
|
||||||
|
|
||||||
|
```
|
||||||
|
docker/ # 唯一部署目录,不含业务源码
|
||||||
|
├── docker-compose.yaml # 服务编排(端口仅 127.0.0.1,密钥由 .env 注入)
|
||||||
|
├── docker-compose.override.yaml # nginx 版本固定 + 缓存配置挂载 + ACME 目录
|
||||||
|
├── .env # 真实凭据(600,不入库)
|
||||||
|
├── .env.example # 占位模板(入库)
|
||||||
|
├── backend/Dockerfile # 源码构建路径(正式部署不用它)
|
||||||
|
├── nginx/
|
||||||
|
│ ├── nginx.conf # 站点配置(含文档封堵、隐藏文件拒绝、ACME 放行)
|
||||||
|
│ ├── cache-headers.inc # 静态资源长缓存片段
|
||||||
|
│ ├── acme/ # ACME http-01 校验目录
|
||||||
|
│ ├── ssl/ # 证书与私钥(600,不入库)
|
||||||
|
│ └── {web,app,docs}/dist # 三端静态成品(由本地构建后上传)
|
||||||
|
└── mysql/ redis/ # 初始化 SQL 与 redis.conf
|
||||||
|
backups/ # 部署前自动备份(每类保留 5 份)
|
||||||
|
```
|
||||||
|
|
||||||
|
仓库侧:`deploy-artifacts.sh`(本机执行的成品部署脚本)、`backend/requirements/`(依赖分组)、`.github/workflows/`(CI)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 日常部署
|
||||||
|
|
||||||
|
在本机仓库根目录执行(需要本机可免密 SSH 到服务器、且本机 Docker 可用——`colima start`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy-artifacts.sh web # 构建并部署 Web 管理后台
|
||||||
|
./deploy-artifacts.sh app # 构建并部署移动端 H5
|
||||||
|
./deploy-artifacts.sh backend # 本地构建 MySQL 版镜像 → 传输 → 只重建 backend
|
||||||
|
./deploy-artifacts.sh all # 三样全发
|
||||||
|
./deploy-artifacts.sh verify # 公网四端验证
|
||||||
|
./deploy-artifacts.sh backups # 列出服务器备份
|
||||||
|
./deploy-artifacts.sh prune # 轮转清理旧备份与旧镜像
|
||||||
|
./deploy-artifacts.sh rollback web|app|backend
|
||||||
|
```
|
||||||
|
|
||||||
|
要点:
|
||||||
|
|
||||||
|
- **服务器不接收源码、不执行构建**:后端以 `docker save → ssh → docker load` 交付镜像,前端以 tar 管道交付成品。
|
||||||
|
- 每次部署前自动备份到 `backups/<标签>-<时间戳>`,备份失败即中止,不会带着假备份上线。
|
||||||
|
- 只重建变化的容器;MySQL/Redis/Nginx 不受影响。
|
||||||
|
- PostgreSQL 版本镜像:`./deploy-artifacts.sh backend-pg`(**只构建与自检,不切换线上**)。切换需人工把 `BACKEND_IMAGE_TAG` 指到该镜像并确保 compose 的数据库配置已改。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 密钥与口令
|
||||||
|
|
||||||
|
| 项 | 位置 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `SECRET_KEY` | 服务器 `docker/.env` | JWT 签名密钥,64 位 hex,随机生成 |
|
||||||
|
| `DATA_ENCRYPTION_KEY` | 服务器 `docker/.env` | 落库敏感数据加密主密钥 |
|
||||||
|
| `DATA_ENCRYPTION_OLD_KEYS` | 服务器 `docker/.env` | 轮换前的旧密钥,仅用于解密历史密文 |
|
||||||
|
| 种子账号口令 | 服务器 `/root/.fa-seed-passwords`(600) | `super`/`admin`/`user`,登录后请自行修改并删除该文件 |
|
||||||
|
|
||||||
|
**轮换密钥的正确步骤**(不可跳步,否则历史密文无法解密):
|
||||||
|
|
||||||
|
1. 把当前 `DATA_ENCRYPTION_KEY`(未设置时取当前 `SECRET_KEY`)追加到 `DATA_ENCRYPTION_OLD_KEYS`;
|
||||||
|
2. 生成新值:`openssl rand -hex 32`,写入 `SECRET_KEY` / `DATA_ENCRYPTION_KEY`;
|
||||||
|
3. `docker compose up -d --force-recreate backend`;
|
||||||
|
4. 验证历史密文仍可解密(例如存储源口令),再删除 `OLD_KEYS` 中不再需要的旧值。
|
||||||
|
|
||||||
|
> 注意:更换 `SECRET_KEY` 会使已签发的令牌全部失效,用户需重新登录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 运行状态与巡检
|
||||||
|
|
||||||
|
- **每日巡检**:`systemctl list-timers fa-health-check.timer`(每天 08:30,带 5 分钟随机延迟)
|
||||||
|
- 巡检内容:4 个容器健康、`/api/v1/monitor/health/check` 与 `/web/`、`/app/` 可用性、证书剩余天数、磁盘、swap、备份与镜像堆积
|
||||||
|
- 结果:`/var/log/fa-health.log`;有告警时写入 syslog(`journalctl -t fa-health`)且服务退出码非 0
|
||||||
|
- 手动执行:`systemctl start fa-health-check.service && tail -n 30 /var/log/fa-health.log`
|
||||||
|
|
||||||
|
常用排查命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/FastapiAdmin/docker
|
||||||
|
docker compose ps
|
||||||
|
docker logs --tail 100 backend
|
||||||
|
docker inspect backend --format '{{.State.Health.Status}}'
|
||||||
|
ss -tlnp | grep docker-proxy # 确认对外只有 80/443
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 证书
|
||||||
|
|
||||||
|
- 当前证书:`docker/nginx/ssl/server.pem`(`CN=service.fastapiadmin.com`),由巡检脚本监控到期天数
|
||||||
|
- 续期(重新签发后):替换 `server.pem` 与 `server.key` → `docker exec nginx nginx -s reload`
|
||||||
|
- 已放行 ACME http-01 校验路径(`/.well-known/acme-challenge/` → `docker/nginx/acme`),如需切换到 acme.sh 自动续期,把校验文件写入该目录即可,无需再改 nginx 配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 已知遗留问题
|
||||||
|
|
||||||
|
安全与质量债的完整清单见 `AUDIT-REPORT.md` 与各分报告(`backend/audit-*.md`、`frontend/audit-*.md`、`docker/audit-deploy.md`)。当前仍待处理的高优先级项:
|
||||||
|
|
||||||
|
1. 会话失效机制:停用/删除/改密后旧会话仍有效(最长 7 天)——需应用侧改造;
|
||||||
|
2. 数据权限注入不完整:`CRUDBase` 的删除/清空/改状态路径未注入数据权限;
|
||||||
|
3. Web i18n 与通知状态枚举:`85/101` 个页面未接入 i18n;通知状态枚举与后端语义冲突;
|
||||||
|
4. 测试覆盖:后端仅 2 个用例,夹具掩盖缺陷(见 `backend/audit-testing.md`);
|
||||||
|
5. 多租户需求与实现错位(见 `backend/audit-requirements.md`)。
|
||||||
+22
-8
@@ -32,11 +32,7 @@ dependencies = [
|
|||||||
"ua-parser==1.0.2", # 解析 User-Agent 获取 OS/浏览器
|
"ua-parser==1.0.2", # 解析 User-Agent 获取 OS/浏览器
|
||||||
"uvicorn==0.49.0", # uvicorn web 框架
|
"uvicorn==0.49.0", # uvicorn web 框架
|
||||||
"websockets>=16.0,<17.0", # websocket 通信
|
"websockets>=16.0,<17.0", # websocket 通信
|
||||||
"aiosqlite==0.22.1",
|
"aiosqlite==0.22.1", # 内嵌知识库(app/modules/ai/chat)固定使用 sqlite,故列入核心
|
||||||
"aiomysql>=0.2.0",
|
|
||||||
"asyncpg==0.31.0",
|
|
||||||
"psycopg[binary]==3.3.2",
|
|
||||||
"pymysql==1.2.0",
|
|
||||||
"openai==2.46.0",
|
"openai==2.46.0",
|
||||||
"pydantic[email]>=2.12.5",
|
"pydantic[email]>=2.12.5",
|
||||||
"cryptography==50.0.0", # Fernet 对称加密(存储源密码加密)
|
"cryptography==50.0.0", # Fernet 对称加密(存储源密码加密)
|
||||||
@@ -47,10 +43,28 @@ dependencies = [
|
|||||||
"cos-python-sdk-v5==1.9.44",
|
"cos-python-sdk-v5==1.9.44",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 数据库驱动按需安装:uv sync --extra mysql / --extra postgres / --extra sqlite
|
||||||
|
# 默认(不指定 extra)只装核心依赖,启动时会提示缺少对应驱动
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
mysql = ["aiomysql>=0.2.0"]
|
mysql = [
|
||||||
postgres = ["asyncpg==0.31.0"]
|
"aiomysql>=0.2.0", # 异步驱动(ASYNC_DB_URI)
|
||||||
sqlite = ["aiosqlite==0.22.1"]
|
"pymysql==1.2.0", # 同步驱动(DB_URI)
|
||||||
|
]
|
||||||
|
postgres = [
|
||||||
|
"asyncpg==0.31.0", # 异步驱动(ASYNC_DB_URI)
|
||||||
|
"psycopg[binary]==3.3.2", # 同步驱动(DB_URI,自带 libpq)
|
||||||
|
]
|
||||||
|
sqlite = [
|
||||||
|
"aiosqlite==0.22.1",
|
||||||
|
]
|
||||||
|
# 一个进程同时支持三种库(体积最大,适合本地开发)
|
||||||
|
all-db = [
|
||||||
|
"aiomysql>=0.2.0",
|
||||||
|
"pymysql==1.2.0",
|
||||||
|
"asyncpg==0.31.0",
|
||||||
|
"psycopg[binary]==3.3.2",
|
||||||
|
"aiosqlite==0.22.1",
|
||||||
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
+14
-41
@@ -1,41 +1,14 @@
|
|||||||
# 与 pyproject.toml [project] dependencies 一致(生产 pip;不含 dev 组:pytest / ruff / fakeredis)
|
# ============================================================================
|
||||||
agno==2.5.8 # AI agent框架
|
# 完整依赖入口(核心 + 全部数据库驱动 + 对象存储)
|
||||||
aiofiles==24.1.0 # 文件操作
|
# 适用于:源码部署、本机开发、以及需要「一个镜像同时支持 mysql/postgres/sqlite」的场景。
|
||||||
aiosqlite==0.22.1 # sqlite 异步操作数据库
|
#
|
||||||
alembic==1.18.4 # 数据库迁移
|
# 想要更小的生产镜像,请按需组合,例如:
|
||||||
apscheduler==3.11.0 # 定时任务
|
# MySQL : -r requirements/base.txt -r requirements/db/mysql.txt -r requirements/storage.txt
|
||||||
croniter==6.2.4 # cron 表达式校验
|
# Postgres : -r requirements/base.txt -r requirements/db/postgres.txt -r requirements/storage.txt
|
||||||
aiomysql>=0.2.0 # 异步mysql操作数据库
|
# SQLite : -r requirements/base.txt -r requirements/db/sqlite.txt
|
||||||
asyncpg==0.31.0 # 异步postgresql操作数据库
|
# ============================================================================
|
||||||
bleach==6.4.0 # 过滤 HTML 标签
|
-r requirements/base.txt
|
||||||
tinycss2==1.5.1 # bleach CSS 清洗依赖
|
-r requirements/db/mysql.txt
|
||||||
click==8.1.7 # 命令行参数解析
|
-r requirements/db/postgres.txt
|
||||||
fastapi==0.138.2 # fastapi 框架
|
-r requirements/db/sqlite.txt
|
||||||
greenlet==3.5.2 # 协程核心依赖(SQLAlchemy 异步)
|
-r requirements/storage.txt
|
||||||
httpx==0.28.1 # HTTP 客户端
|
|
||||||
jinja2==3.1.6 # 模板引擎
|
|
||||||
loguru==0.7.3 # 日志
|
|
||||||
openai==2.46.0 # OpenAI API
|
|
||||||
openpyxl==3.1.5 # Excel
|
|
||||||
psutil==7.2.2 # 系统信息
|
|
||||||
py-ip2region==3.0.4 # IP归属地离线查询(ip2region官方binding,内置xdb本地解析)
|
|
||||||
psycopg[binary]==3.3.2 # postgresql 同步驱动(含预编译 libpq,开箱即用)
|
|
||||||
pydantic[email]>=2.12.5 # 邮箱校验
|
|
||||||
pydantic-settings>=2.6.1,<3 # 配置设置
|
|
||||||
pyjwt==2.13.0 # OAuth2
|
|
||||||
pymysql==1.2.0 # MySQL 异步操作数据库
|
|
||||||
python-multipart==0.0.32 # request.form() 对表单进行「解析」时安装
|
|
||||||
redis==7.1.0 # Redis 异步操作数据库
|
|
||||||
rich==15.0.0 # 终端打印美化
|
|
||||||
sqlalchemy==2.0.51 # 数据库ORM
|
|
||||||
sqlglot[rs]==27.8.0 # sql 解析
|
|
||||||
typer==0.26.7 # 命令行工具
|
|
||||||
ua-parser==1.0.2 # 获取用户UA
|
|
||||||
uvicorn==0.49.0 # ASGI 框架
|
|
||||||
websockets>=16.0,<17.0 # websocket 通信
|
|
||||||
cryptography==50.0.0 # Fernet 对称加密(存储源密码加密)
|
|
||||||
paramiko==5.0.0 # SFTP 协议
|
|
||||||
boto3==1.43.72 # S3 协议
|
|
||||||
esdk-obs-python==3.26.6 # 华为云 OBS 协议
|
|
||||||
alibabacloud-oss-v2==1.3.2 # 阿里云 OSS 协议
|
|
||||||
cos-python-sdk-v5==1.9.44 # 腾讯云 COS 协议
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# 核心依赖(与数据库、对象存储无关,任何部署方式都需要)
|
||||||
|
# 用法:pip install -r requirements/base.txt -r requirements/db/<你的数据库>.txt
|
||||||
|
# ============================================================================
|
||||||
|
agno==2.5.8 # AI agent 框架
|
||||||
|
aiosqlite==0.22.1 # AI 模块内嵌知识库固定用 sqlite(与主库类型无关),故列入核心
|
||||||
|
aiofiles==24.1.0 # 异步文件操作
|
||||||
|
alembic==1.18.4 # 数据库迁移
|
||||||
|
apscheduler==3.11.0 # 定时任务
|
||||||
|
bleach==6.4.0 # HTML 过滤
|
||||||
|
click==8.1.7 # 命令行参数解析(typer 依赖)
|
||||||
|
croniter==6.2.4 # cron 表达式校验
|
||||||
|
fastapi==0.138.2 # Web 框架
|
||||||
|
greenlet==3.5.2 # SQLAlchemy 异步必需
|
||||||
|
httpx==0.28.1 # HTTP 客户端
|
||||||
|
jinja2==3.1.6 # 模板引擎(代码生成)
|
||||||
|
loguru==0.7.3 # 日志
|
||||||
|
openai==2.46.0 # OpenAI 兼容接口
|
||||||
|
openpyxl==3.1.5 # Excel 导入导出
|
||||||
|
psutil==7.2.2 # 系统信息采集
|
||||||
|
py-ip2region==3.0.4 # IP 归属地离线查询
|
||||||
|
pydantic[email]>=2.12.5 # 数据校验 + 邮箱
|
||||||
|
pydantic-settings>=2.6.1,<3 # 配置管理
|
||||||
|
pyjwt==2.13.0 # 令牌签发校验
|
||||||
|
python-multipart==0.0.32 # 表单解析
|
||||||
|
redis==7.1.0 # 缓存 / 会话
|
||||||
|
rich==15.0.0 # 终端输出美化
|
||||||
|
sqlalchemy==2.0.51 # ORM
|
||||||
|
sqlglot[rs]==27.8.0 # SQL 解析
|
||||||
|
tinycss2==1.5.1 # bleach 依赖
|
||||||
|
typer==0.26.7 # CLI 入口(main.py)
|
||||||
|
ua-parser==1.0.2 # UA 解析
|
||||||
|
uvicorn==0.49.0 # ASGI 服务器
|
||||||
|
websockets>=16.0,<17.0 # WebSocket
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# MySQL 驱动(DATABASE_TYPE=mysql 时必需)
|
||||||
|
# aiomysql / pymysql:异步与同步连接,分别对应 ASYNC_DB_URI 与 DB_URI
|
||||||
|
# 用法:pip install -r requirements/base.txt -r requirements/db/mysql.txt
|
||||||
|
# ============================================================================
|
||||||
|
aiomysql>=0.2.0 # 异步驱动(ASYNC_DB_URI: mysql+aiomysql://)
|
||||||
|
pymysql==1.2.0 # 同步驱动(DB_URI: mysql+pymysql://,迁移与后台任务用)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# PostgreSQL 驱动(DATABASE_TYPE=postgres 时必需)
|
||||||
|
# asyncpg:异步驱动,对应 ASYNC_DB_URI(postgresql+asyncpg://)
|
||||||
|
# psycopg[binary]:同步驱动(含预编译 libpq),对应 DB_URI(postgresql+psycopg://)
|
||||||
|
# 注意:psycopg[binary] 自带 libpq,无需在系统里额外安装 postgresql 客户端库
|
||||||
|
# 用法:pip install -r requirements/base.txt -r requirements/db/postgres.txt
|
||||||
|
# ============================================================================
|
||||||
|
asyncpg==0.31.0 # 异步驱动
|
||||||
|
psycopg[binary]==3.3.2 # 同步驱动
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# SQLite 驱动(DATABASE_TYPE=sqlite 时必需)
|
||||||
|
# 适用于本地开发与单机小规模部署;AINI 的 AI 模块内嵌知识库也复用该驱动
|
||||||
|
# 用法:pip install -r requirements/base.txt -r requirements/db/sqlite.txt
|
||||||
|
# ============================================================================
|
||||||
|
aiosqlite==0.22.1 # 异步驱动(sqlite+aiosqlite:///)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# 对象存储与文件传输(本地存储源之外的能力;存储源配置里选用 S3 / OSS / COS / OBS / SFTP 时必需)
|
||||||
|
# 仅使用「本地存储源」的部署可以不装这一组,镜像可再小约 45MB
|
||||||
|
# 用法:pip install -r requirements/base.txt -r requirements/db/mysql.txt -r requirements/storage.txt
|
||||||
|
# ============================================================================
|
||||||
|
boto3==1.43.72 # S3 及兼容协议
|
||||||
|
alibabacloud-oss-v2==1.3.2 # 阿里云 OSS
|
||||||
|
cos-python-sdk-v5==1.9.44 # 腾讯云 COS
|
||||||
|
esdk-obs-python==3.26.6 # 华为云 OBS
|
||||||
|
paramiko==5.0.0 # SFTP
|
||||||
|
cryptography==50.0.0 # 存储源密码的 Fernet 加密
|
||||||
Generated
+17
-9
@@ -184,12 +184,10 @@ source = { virtual = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "agno" },
|
{ name = "agno" },
|
||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
{ name = "aiomysql" },
|
|
||||||
{ name = "aiosqlite" },
|
{ name = "aiosqlite" },
|
||||||
{ name = "alembic" },
|
{ name = "alembic" },
|
||||||
{ name = "alibabacloud-oss-v2" },
|
{ name = "alibabacloud-oss-v2" },
|
||||||
{ name = "apscheduler" },
|
{ name = "apscheduler" },
|
||||||
{ name = "asyncpg" },
|
|
||||||
{ name = "bleach" },
|
{ name = "bleach" },
|
||||||
{ name = "boto3" },
|
{ name = "boto3" },
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
@@ -206,12 +204,10 @@ dependencies = [
|
|||||||
{ name = "openpyxl" },
|
{ name = "openpyxl" },
|
||||||
{ name = "paramiko" },
|
{ name = "paramiko" },
|
||||||
{ name = "psutil" },
|
{ name = "psutil" },
|
||||||
{ name = "psycopg", extra = ["binary"] },
|
|
||||||
{ name = "py-ip2region" },
|
{ name = "py-ip2region" },
|
||||||
{ name = "pydantic", extra = ["email"] },
|
{ name = "pydantic", extra = ["email"] },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "pyjwt" },
|
{ name = "pyjwt" },
|
||||||
{ name = "pymysql" },
|
|
||||||
{ name = "python-multipart" },
|
{ name = "python-multipart" },
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
@@ -225,11 +221,20 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
|
all-db = [
|
||||||
|
{ name = "aiomysql" },
|
||||||
|
{ name = "aiosqlite" },
|
||||||
|
{ name = "asyncpg" },
|
||||||
|
{ name = "psycopg", extra = ["binary"] },
|
||||||
|
{ name = "pymysql" },
|
||||||
|
]
|
||||||
mysql = [
|
mysql = [
|
||||||
{ name = "aiomysql" },
|
{ name = "aiomysql" },
|
||||||
|
{ name = "pymysql" },
|
||||||
]
|
]
|
||||||
postgres = [
|
postgres = [
|
||||||
{ name = "asyncpg" },
|
{ name = "asyncpg" },
|
||||||
|
{ name = "psycopg", extra = ["binary"] },
|
||||||
]
|
]
|
||||||
sqlite = [
|
sqlite = [
|
||||||
{ name = "aiosqlite" },
|
{ name = "aiosqlite" },
|
||||||
@@ -247,14 +252,15 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "agno", specifier = "==2.5.8" },
|
{ name = "agno", specifier = "==2.5.8" },
|
||||||
{ name = "aiofiles", specifier = "==24.1.0" },
|
{ name = "aiofiles", specifier = "==24.1.0" },
|
||||||
{ name = "aiomysql", specifier = ">=0.2.0" },
|
{ name = "aiomysql", marker = "extra == 'all-db'", specifier = ">=0.2.0" },
|
||||||
{ name = "aiomysql", marker = "extra == 'mysql'", specifier = ">=0.2.0" },
|
{ name = "aiomysql", marker = "extra == 'mysql'", specifier = ">=0.2.0" },
|
||||||
{ name = "aiosqlite", specifier = "==0.22.1" },
|
{ name = "aiosqlite", specifier = "==0.22.1" },
|
||||||
|
{ name = "aiosqlite", marker = "extra == 'all-db'", specifier = "==0.22.1" },
|
||||||
{ name = "aiosqlite", marker = "extra == 'sqlite'", specifier = "==0.22.1" },
|
{ name = "aiosqlite", marker = "extra == 'sqlite'", specifier = "==0.22.1" },
|
||||||
{ name = "alembic", specifier = "==1.18.4" },
|
{ name = "alembic", specifier = "==1.18.4" },
|
||||||
{ name = "alibabacloud-oss-v2", specifier = "==1.3.2" },
|
{ name = "alibabacloud-oss-v2", specifier = "==1.3.2" },
|
||||||
{ name = "apscheduler", specifier = "==3.11.0" },
|
{ name = "apscheduler", specifier = "==3.11.0" },
|
||||||
{ name = "asyncpg", specifier = "==0.31.0" },
|
{ name = "asyncpg", marker = "extra == 'all-db'", specifier = "==0.31.0" },
|
||||||
{ name = "asyncpg", marker = "extra == 'postgres'", specifier = "==0.31.0" },
|
{ name = "asyncpg", marker = "extra == 'postgres'", specifier = "==0.31.0" },
|
||||||
{ name = "bleach", specifier = "==6.4.0" },
|
{ name = "bleach", specifier = "==6.4.0" },
|
||||||
{ name = "boto3", specifier = "==1.43.72" },
|
{ name = "boto3", specifier = "==1.43.72" },
|
||||||
@@ -272,12 +278,14 @@ requires-dist = [
|
|||||||
{ name = "openpyxl", specifier = "==3.1.5" },
|
{ name = "openpyxl", specifier = "==3.1.5" },
|
||||||
{ name = "paramiko", specifier = "==5.0.0" },
|
{ name = "paramiko", specifier = "==5.0.0" },
|
||||||
{ name = "psutil", specifier = "==7.2.2" },
|
{ name = "psutil", specifier = "==7.2.2" },
|
||||||
{ name = "psycopg", extras = ["binary"], specifier = "==3.3.2" },
|
{ name = "psycopg", extras = ["binary"], marker = "extra == 'all-db'", specifier = "==3.3.2" },
|
||||||
|
{ name = "psycopg", extras = ["binary"], marker = "extra == 'postgres'", specifier = "==3.3.2" },
|
||||||
{ name = "py-ip2region", specifier = "==3.0.4" },
|
{ name = "py-ip2region", specifier = "==3.0.4" },
|
||||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.12.5" },
|
{ name = "pydantic", extras = ["email"], specifier = ">=2.12.5" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.6.1,<3" },
|
{ name = "pydantic-settings", specifier = ">=2.6.1,<3" },
|
||||||
{ name = "pyjwt", specifier = "==2.13.0" },
|
{ name = "pyjwt", specifier = "==2.13.0" },
|
||||||
{ name = "pymysql", specifier = "==1.2.0" },
|
{ name = "pymysql", marker = "extra == 'all-db'", specifier = "==1.2.0" },
|
||||||
|
{ name = "pymysql", marker = "extra == 'mysql'", specifier = "==1.2.0" },
|
||||||
{ name = "python-multipart", specifier = "==0.0.32" },
|
{ name = "python-multipart", specifier = "==0.0.32" },
|
||||||
{ name = "redis", specifier = "==7.1.0" },
|
{ name = "redis", specifier = "==7.1.0" },
|
||||||
{ name = "rich", specifier = "==15.0.0" },
|
{ name = "rich", specifier = "==15.0.0" },
|
||||||
@@ -289,7 +297,7 @@ requires-dist = [
|
|||||||
{ name = "uvicorn", specifier = "==0.49.0" },
|
{ name = "uvicorn", specifier = "==0.49.0" },
|
||||||
{ name = "websockets", specifier = ">=16.0,<17.0" },
|
{ name = "websockets", specifier = ">=16.0,<17.0" },
|
||||||
]
|
]
|
||||||
provides-extras = ["mysql", "postgres", "sqlite"]
|
provides-extras = ["mysql", "postgres", "sqlite", "all-db"]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
Executable
+369
@@ -0,0 +1,369 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# FastapiAdmin 成品部署脚本(本地构建 → 服务器只接收成品)
|
||||||
|
#
|
||||||
|
# 设计目标:
|
||||||
|
# 1. 服务器上不放业务源码、不跑构建,只接收构建好的成品;
|
||||||
|
# 2. 后端用「镜像」交付(docker save | ssh docker load),前端用 tar 管道交付;
|
||||||
|
# 3. 每次上线前先在服务器 backups/ 留一份备份,可一键回滚;
|
||||||
|
# 4. 只重建变化的那一个容器,MySQL/Redis/Nginx 不重启。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# ./deploy-artifacts.sh web # Web 管理后台(nginx /web)
|
||||||
|
# ./deploy-artifacts.sh app # 移动端 H5(nginx /app)
|
||||||
|
# ./deploy-artifacts.sh backend # 构建并部署 MySQL 版后端镜像(切换线上)
|
||||||
|
# ./deploy-artifacts.sh backend-pg # 只构建并自检 PostgreSQL 版镜像,不切换线上
|
||||||
|
# ./deploy-artifacts.sh all # 三样全发(后端默认 MySQL 版)
|
||||||
|
# ./deploy-artifacts.sh verify # 公网四端验证
|
||||||
|
# ./deploy-artifacts.sh backups # 列出服务器上的备份
|
||||||
|
# ./deploy-artifacts.sh rollback web # 回滚 Web 到最近一次备份
|
||||||
|
# ./deploy-artifacts.sh rollback app # 回滚 H5 到最近一次备份
|
||||||
|
#
|
||||||
|
# 前置条件:
|
||||||
|
# - 本机 SSH 密钥可免密登录 root@$FA_SERVER
|
||||||
|
# - backend 目标需要本机 docker(Colima / Docker Desktop 均可)
|
||||||
|
# - frontend/web、frontend/app 依赖已安装(pnpm install)
|
||||||
|
#
|
||||||
|
# 环境变量覆盖:FA_SERVER、FA_REMOTE_ROOT、FA_BACKUP_DIR
|
||||||
|
# ============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SERVER="${FA_SERVER:-8.137.99.5}"
|
||||||
|
REMOTE_ROOT="${FA_REMOTE_ROOT:-/home/FastapiAdmin}"
|
||||||
|
BACKUP_DIR="${FA_BACKUP_DIR:-/home/FastapiAdmin/backups}"
|
||||||
|
BACKUP_KEEP="${FA_BACKUP_KEEP:-5}" # 每个标签最多保留的备份份数
|
||||||
|
IMAGE_KEEP="${FA_IMAGE_KEEP:-2}" # 每类镜像最多保留的版本数(当前 + 回滚点)
|
||||||
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
KEY="$HOME/.ssh/id_ed25519"
|
||||||
|
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
|
||||||
|
|
||||||
|
log() { printf '\033[0;34m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*"; }
|
||||||
|
ok() { printf '\033[0;32m ✅ %s\033[0m\n' "$*"; }
|
||||||
|
die() { printf '\033[0;31m ❌ %s\033[0m\n' "$*"; exit 1; }
|
||||||
|
|
||||||
|
ssh_do() { ssh "${SSH_OPTS[@]}" -i "$KEY" "root@${SERVER}" "$@"; }
|
||||||
|
|
||||||
|
# 把本地目录内容流式发到服务器目标目录。
|
||||||
|
# 服务器侧流程:解包到暂存区 → 校验入口文件 → 备份旧目录到 backups/<label>-<时间> →
|
||||||
|
# 原子替换 → 修正属主。任何一步失败都中止,绝不留下半成品。
|
||||||
|
# 用法: push_dir <本地目录> <远端目录> <备份标签>
|
||||||
|
push_dir() {
|
||||||
|
local src="$1" dst="$2" label="$3"
|
||||||
|
[ -d "$src" ] || die "本地目录不存在: $src"
|
||||||
|
local ts tar_cmd ssh_cmd
|
||||||
|
ts="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
|
||||||
|
tar_cmd="COPYFILE_DISABLE=1 tar -czf - -C $(printf '%q' "$src") ."
|
||||||
|
ssh_cmd="
|
||||||
|
set -e
|
||||||
|
dst=$(printf '%q' "$dst")
|
||||||
|
bakdir=$(printf '%q' "$BACKUP_DIR")
|
||||||
|
bak=\"\$bakdir/$(printf '%q' "$label")-\$(date +%Y%m%d-%H%M%S)\"
|
||||||
|
|
||||||
|
rm -rf /tmp/fa-stage && mkdir -p /tmp/fa-stage
|
||||||
|
tar -xzf - --no-same-owner -C /tmp/fa-stage
|
||||||
|
find /tmp/fa-stage \( -name '._*' -o -name '.DS_Store' \) -delete
|
||||||
|
[ -f /tmp/fa-stage/index.html ] || { echo 'ERROR: 缺少 index.html,中止'; exit 1; }
|
||||||
|
new=\$(find /tmp/fa-stage -type f | wc -l)
|
||||||
|
[ \"\$new\" -gt 0 ] || { echo 'ERROR: 上传内容为空,中止'; exit 1; }
|
||||||
|
|
||||||
|
if [ -e \"\$dst\" ]; then
|
||||||
|
mkdir -p \"\$bakdir\"
|
||||||
|
rm -rf \"\$bak\"
|
||||||
|
cp -a \"\$dst\" \"\$bak\"
|
||||||
|
old=\$(find \"\$bak\" -type f | wc -l)
|
||||||
|
[ \"\$old\" -gt 0 ] || { echo 'ERROR: 备份为空,中止'; exit 1; }
|
||||||
|
echo \" 备份 OK: \${bak} (\${old} 文件)\"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf \"\$dst\" && mkdir -p \"\$dst\"
|
||||||
|
for e in \$(ls -A /tmp/fa-stage); do mv \"/tmp/fa-stage/\$e\" \"\$dst\"/; done
|
||||||
|
rm -rf /tmp/fa-stage
|
||||||
|
chown -R root:root \"\$dst\"
|
||||||
|
echo \" 上线 OK: \$new 文件, \$(du -sh \"\$dst\" | cut -f1), 属主 \$(stat -c %U:%G \"\$dst\")\"
|
||||||
|
"
|
||||||
|
log "上传 $(basename "$src") → $dst"
|
||||||
|
if ! eval "$tar_cmd" 2>/dev/null | ssh_do "$ssh_cmd"; then
|
||||||
|
die "部署失败:远端未做替换,备份保留,可安全重试"
|
||||||
|
fi
|
||||||
|
_rotate_backups "$label"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 每个标签只保留最近 BACKUP_KEEP 份备份(web 一份 23MB,不轮转会持续吃磁盘)
|
||||||
|
_rotate_backups() {
|
||||||
|
local label="$1" keep="$BACKUP_KEEP"
|
||||||
|
ssh_do "
|
||||||
|
bakdir=${BACKUP_DIR}
|
||||||
|
ls -1dt \"\$bakdir/${label}-\"* 2>/dev/null | tail -n +\$(( ${keep} + 1 )) | while read -r d; do
|
||||||
|
rm -rf \"\$d\" && echo \" 轮转删除旧备份: \$(basename \$d)\"
|
||||||
|
done
|
||||||
|
true
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- Web 管理后台 ----------
|
||||||
|
deploy_web() {
|
||||||
|
log "构建 Web 管理后台(vue-tsc 类型检查由 type-check 单独负责,这里只出产物)"
|
||||||
|
( cd frontend/web && pnpm run build:prod >/tmp/fa-web-build.log 2>&1 ) \
|
||||||
|
|| { tail -20 /tmp/fa-web-build.log; die "web 构建失败"; }
|
||||||
|
ok "web 构建完成:$(du -sh frontend/web/dist | cut -f1)"
|
||||||
|
push_dir frontend/web/dist "${REMOTE_ROOT}/docker/nginx/web/dist" "web"
|
||||||
|
ok "Web 已上线"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 移动端 H5 ----------
|
||||||
|
deploy_app() {
|
||||||
|
log "构建移动端 H5"
|
||||||
|
( cd frontend/app && pnpm run build:h5 >/tmp/fa-app-build.log 2>&1 ) \
|
||||||
|
|| { tail -20 /tmp/fa-app-build.log; die "app 构建失败"; }
|
||||||
|
ok "H5 构建完成:$(du -sh frontend/app/dist/build/h5 | cut -f1)"
|
||||||
|
# nginx /app 用 alias 指向 app/dist,真实入口在 build/h5 下;
|
||||||
|
# try_files 回退到 /app/index.html,少一层就 500 内部重定向死循环
|
||||||
|
push_dir frontend/app/dist/build/h5 \
|
||||||
|
"${REMOTE_ROOT}/docker/nginx/app/dist/build/h5" "app-h5"
|
||||||
|
ok "H5 已上线"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 后端镜像 ----------
|
||||||
|
# deploy_backend <mysql|postgres|sqlite> [switch]
|
||||||
|
# switch=1(默认)构建并切换线上;switch=0 只构建+装载+自检,不动生产
|
||||||
|
deploy_backend() {
|
||||||
|
local db="${1:-mysql}" switch="${2:-1}"
|
||||||
|
case "$db" in mysql|postgres|sqlite) ;; *) die "数据库类型只能是 mysql / postgres / sqlite" ;; esac
|
||||||
|
local tag="backend:${db}-${STAMP}"
|
||||||
|
command -v docker >/dev/null || die "本机没有 docker,无法构建镜像"
|
||||||
|
|
||||||
|
log "准备构建上下文"
|
||||||
|
rm -rf /tmp/fa-ctx && mkdir -p /tmp/fa-ctx/backend
|
||||||
|
( cd backend && tar -cf - \
|
||||||
|
--exclude='.mypy_cache' --exclude='.pytest_cache' --exclude='__pycache__' \
|
||||||
|
--exclude='logs' --exclude='tests' --exclude='dist' --exclude='.venv' \
|
||||||
|
--exclude='.DS_Store' --exclude='.ruff_cache' --exclude='.codebuddy' \
|
||||||
|
--exclude='.vscode' --exclude='.python-version' \
|
||||||
|
--exclude='env/.env.dev' --exclude='env/.env.test' \
|
||||||
|
. ) | tar -xf - -C /tmp/fa-ctx/backend/
|
||||||
|
# 依赖清单目录化:核心 + 数据库驱动分离,镜像里只装所选数据库的驱动
|
||||||
|
rm -rf /tmp/fa-ctx/requirements && mkdir -p /tmp/fa-ctx/requirements
|
||||||
|
cp -R backend/requirements/. /tmp/fa-ctx/requirements/
|
||||||
|
|
||||||
|
# 镜像内的 .env.prod 只是兜底:数据库/Redis 真实地址由 compose 运行时注入。
|
||||||
|
# 这里抹掉本地私网地址与所有密钥,避免把开发凭据烤进镜像。
|
||||||
|
python3 - "$db" <<'PY'
|
||||||
|
import re, pathlib, sys
|
||||||
|
db = sys.argv[1]
|
||||||
|
p = pathlib.Path("/tmp/fa-ctx/backend/env/.env.prod")
|
||||||
|
force = {"DATABASE_HOST": "localhost", "REDIS_HOST": "localhost"}
|
||||||
|
if db == "postgres":
|
||||||
|
force.update({"DATABASE_TYPE": "postgres", "DATABASE_PORT": "5432", "DATABASE_USER": "postgres"})
|
||||||
|
drop_prefixes = ("OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_MODEL")
|
||||||
|
out = []
|
||||||
|
for line in p.read_text().splitlines():
|
||||||
|
m = re.match(r"^(\s*)([A-Z_]+)(\s*=\s*)(.*)$", line)
|
||||||
|
if m and m.group(2) in force:
|
||||||
|
out.append(f"{m.group(1)}{m.group(2)}{m.group(3)}{force[m.group(2)]}")
|
||||||
|
elif m and m.group(2).startswith(drop_prefixes):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
out.append(line)
|
||||||
|
p.write_text("\n".join(out) + "\n")
|
||||||
|
PY
|
||||||
|
|
||||||
|
# 只装所选数据库的驱动:mysql 变体不装 asyncpg/psycopg,postgres 变体不装 aiomysql/pymysql
|
||||||
|
# 运行用户为非 root(UID 1000):容器逃逸后的影响面更小
|
||||||
|
cat > /tmp/fa-ctx/Dockerfile <<EOF
|
||||||
|
FROM python:3.12-slim
|
||||||
|
ARG DB=${db}
|
||||||
|
ENV TZ=Asia/Shanghai PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 LANG=C.UTF-8 DB=\${DB}
|
||||||
|
WORKDIR /home
|
||||||
|
COPY requirements/ /home/requirements/
|
||||||
|
RUN pip install --no-cache-dir \\
|
||||||
|
-r /home/requirements/base.txt \\
|
||||||
|
-r /home/requirements/db/\${DB}.txt \\
|
||||||
|
-r /home/requirements/storage.txt \\
|
||||||
|
-i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
COPY backend/ /home/
|
||||||
|
# 运行时需要写入的目录(日志、上传、迁移版本目录)预先建好并交给运行用户
|
||||||
|
RUN useradd --uid 1000 --create-home --shell /usr/sbin/nologin app \\
|
||||||
|
&& mkdir -p /home/logs /home/static/upload /home/alembic/versions \\
|
||||||
|
&& chown -R app:app /home
|
||||||
|
USER app
|
||||||
|
EXPOSE 8001
|
||||||
|
CMD ["python", "main.py", "run", "--env=prod"]
|
||||||
|
EOF
|
||||||
|
cat > /tmp/fa-ctx/.dockerignore <<'EOF'
|
||||||
|
**/__pycache__
|
||||||
|
**/*.pyc
|
||||||
|
**/.mypy_cache
|
||||||
|
**/.pytest_cache
|
||||||
|
**/logs
|
||||||
|
**/tests
|
||||||
|
**/.DS_Store
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "构建镜像 ${tag} (linux/amd64,依赖层命中缓存时约 5 秒)"
|
||||||
|
docker build --platform linux/amd64 -t "$tag" /tmp/fa-ctx >/tmp/fa-build.log 2>&1 \
|
||||||
|
|| { tail -25 /tmp/fa-build.log; die "镜像构建失败"; }
|
||||||
|
ok "镜像构建完成:$(docker images "$tag" --format '{{.Size}}')"
|
||||||
|
|
||||||
|
log "传输镜像(docker save | gzip | ssh docker load)"
|
||||||
|
docker save "$tag" | gzip -1 | ssh_do 'gunzip | docker load' >/dev/null
|
||||||
|
ok "镜像已加载到服务器"
|
||||||
|
|
||||||
|
if [ "$switch" != "1" ]; then
|
||||||
|
# 只构建与装载,不切换线上。用于生产数据库与镜像变体不一致时做验证。
|
||||||
|
ok "镜像 ${tag} 已就绪但未切换(避免把 ${db} 变体切到当前生产环境)"
|
||||||
|
local drivers
|
||||||
|
case "$db" in
|
||||||
|
postgres) drivers="asyncpg psycopg" ;;
|
||||||
|
mysql) drivers="aiomysql pymysql" ;;
|
||||||
|
sqlite) drivers="aiosqlite" ;;
|
||||||
|
esac
|
||||||
|
if ssh_do "
|
||||||
|
set -e
|
||||||
|
docker run --rm --platform linux/amd64 --entrypoint python ${tag} -c '
|
||||||
|
import importlib, sys
|
||||||
|
mods = sys.argv[1:]
|
||||||
|
for m in mods:
|
||||||
|
importlib.import_module(m)
|
||||||
|
print(\" 驱动可用:\", m)
|
||||||
|
print(\" 自检通过:\", len(mods), \"个 ${db} 驱动\")
|
||||||
|
' ${drivers}
|
||||||
|
" 2>/dev/null; then
|
||||||
|
ok "镜像自检通过:${db} 驱动可正常导入"
|
||||||
|
else
|
||||||
|
die "镜像自检失败:${db} 驱动无法导入"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "切换并只重建 backend(MySQL/Redis/Nginx 不动)"
|
||||||
|
ssh_do "
|
||||||
|
set -e
|
||||||
|
cd ${REMOTE_ROOT}/docker
|
||||||
|
# 容器以非 root(UID 1000)运行;宿主绑定挂载的上传目录必须属于该 UID,否则无法写入
|
||||||
|
mkdir -p backend/static/upload
|
||||||
|
chown -R 1000:1000 backend/static/upload
|
||||||
|
docker tag backend:3.0.0 backend:prev 2>/dev/null || true
|
||||||
|
docker tag ${tag} backend:3.0.0
|
||||||
|
docker compose up -d --force-recreate backend
|
||||||
|
for i in 1 2 3 4 5 6 7; do
|
||||||
|
sleep 15
|
||||||
|
st=\$(docker inspect backend --format '{{.State.Health.Status}}')
|
||||||
|
echo \" 健康检查: \$st\"
|
||||||
|
[ \"\$st\" = healthy ] && break
|
||||||
|
done
|
||||||
|
echo \" 运行用户: \$(docker inspect backend --format '{{.Config.User}}')\"
|
||||||
|
docker inspect backend --format ' 镜像={{.Config.Image}} 状态={{.State.Status}}'
|
||||||
|
"
|
||||||
|
ok "后端已上线(回滚:docker tag backend:prev backend:3.0.0 && docker compose up -d backend)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 验证 ----------
|
||||||
|
verify() {
|
||||||
|
log "公网四端验证"
|
||||||
|
local fail=0
|
||||||
|
for p in "/" "/web/" "/app/" "/api/v1/monitor/health/check"; do
|
||||||
|
printf ' %-42s ' "$p"
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "https://service.fastapiadmin.com${p}")
|
||||||
|
printf 'HTTP %s\n' "$code"
|
||||||
|
[ "$code" = "200" ] || fail=1
|
||||||
|
done
|
||||||
|
[ "$fail" -eq 0 ] && ok "四端全部 200" || die "存在非 200 端点"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 轮转清理:备份目录与镜像仓库,避免磁盘无限增长 ----------
|
||||||
|
prune() {
|
||||||
|
log "轮转清理(备份每标签保留 ${BACKUP_KEEP} 份,镜像每类保留 ${IMAGE_KEEP} 个)"
|
||||||
|
ssh_do "
|
||||||
|
bakdir=${BACKUP_DIR}
|
||||||
|
keep=${BACKUP_KEEP}
|
||||||
|
if [ -d \"\$bakdir\" ]; then
|
||||||
|
for label in \$(ls -1 \"\$bakdir\" | sed 's/-[0-9]\{8\}-[0-9]\{6\}\$//' | sort -u); do
|
||||||
|
n=0
|
||||||
|
for d in \$(ls -1dt \"\$bakdir\"/\$label-* 2>/dev/null); do
|
||||||
|
n=\$((n+1))
|
||||||
|
if [ \"\$n\" -gt \"\$keep\" ]; then rm -rf \"\$d\"; echo \" 清理备份: \$(basename \$d)\"; fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
true # 显式成功,避免末端条件表达式在 set -e 下打断后续清理
|
||||||
|
"
|
||||||
|
# 镜像 tag 形如 <db>-YYYYmmdd-HHMMSS,按 tag 排序即时间序(比 CreatedAt 可靠:秒级相同会乱序)
|
||||||
|
local prefix t
|
||||||
|
for prefix in "mysql-" "postgres-" "sqlite-" "release-" "deploy-"; do
|
||||||
|
while IFS= read -r t; do
|
||||||
|
[ -n "$t" ] || continue
|
||||||
|
docker rmi "backend:${t}" >/dev/null 2>&1 && ok "清理本机镜像 backend:${t}"
|
||||||
|
done < <(docker images "backend:${prefix}*" --format '{{.Tag}}' 2>/dev/null \
|
||||||
|
| sort -r | tail -n +$((IMAGE_KEEP + 1)))
|
||||||
|
done
|
||||||
|
for prefix in "mysql-" "postgres-" "sqlite-" "release-" "deploy-"; do
|
||||||
|
for t in $(ssh_do "docker images 'backend:${prefix}*' --format '{{.Tag}}' 2>/dev/null | sort -r | tail -n +$((IMAGE_KEEP + 1))"); do
|
||||||
|
[ -n "$t" ] || continue
|
||||||
|
ssh_do "docker rmi 'backend:${t}' >/dev/null 2>&1 && echo ' 清理服务器镜像 backend:${t}'"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
ssh_do "df -h / | tail -1 | sed 's/^/ 文件系统: /'"
|
||||||
|
ok "清理完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 备份与回滚 ----------
|
||||||
|
list_backups() {
|
||||||
|
log "服务器备份(${BACKUP_DIR})"
|
||||||
|
ssh_do "ls -1t ${BACKUP_DIR} 2>/dev/null | head -20 || echo ' (还没有备份)'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# rollback <web|app|backend>
|
||||||
|
rollback() {
|
||||||
|
local what="${1:-}"
|
||||||
|
case "$what" in
|
||||||
|
web) _rollback_dir web "${REMOTE_ROOT}/docker/nginx/web/dist" ;;
|
||||||
|
app) _rollback_dir app-h5 "${REMOTE_ROOT}/docker/nginx/app/dist/build/h5" ;;
|
||||||
|
backend) _rollback_image ;;
|
||||||
|
*) die "用法: $0 rollback <web|app|backend>" ;;
|
||||||
|
esac
|
||||||
|
verify
|
||||||
|
}
|
||||||
|
|
||||||
|
_rollback_dir() {
|
||||||
|
local label="$1" dst="$2"
|
||||||
|
log "回滚 $label → $dst"
|
||||||
|
ssh_do "
|
||||||
|
set -e
|
||||||
|
bak=\$(ls -1dt ${BACKUP_DIR}/${label}-* 2>/dev/null | head -1)
|
||||||
|
[ -n \"\$bak\" ] || { echo ' ERROR: 没有可用备份'; exit 1; }
|
||||||
|
[ -f \"\$bak/index.html\" ] || { echo \" ERROR: 备份 \$bak 缺少 index.html\"; exit 1; }
|
||||||
|
rm -rf '${dst}.broken' && mv '${dst}' '${dst}.broken'
|
||||||
|
mkdir -p '${dst}'
|
||||||
|
for e in \$(ls -A \"\$bak\"); do cp -a \"\$bak/\$e\" '${dst}'/; done
|
||||||
|
rm -rf '${dst}.broken'
|
||||||
|
chown -R root:root '${dst}'
|
||||||
|
echo \" 已回滚自: \${bak} (\$(find '${dst}' -type f | wc -l) 文件)\"
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
_rollback_image() {
|
||||||
|
log "回滚后端到 backend:prev"
|
||||||
|
ssh_do "
|
||||||
|
set -e
|
||||||
|
cd ${REMOTE_ROOT}/docker
|
||||||
|
docker image inspect backend:prev >/dev/null 2>&1 || { echo ' ERROR: backend:prev 不存在'; exit 1; }
|
||||||
|
docker tag backend:prev backend:3.0.0
|
||||||
|
docker compose up -d --force-recreate backend
|
||||||
|
sleep 30
|
||||||
|
docker inspect backend --format ' 已回滚: 镜像={{.Config.Image}} 健康={{.State.Health.Status}}'
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
web) deploy_web; verify; prune ;;
|
||||||
|
app) deploy_app; verify; prune ;;
|
||||||
|
backend) deploy_backend mysql 1; verify; prune ;;
|
||||||
|
backend-pg) deploy_backend postgres 0 ;; # 只构建+自检,绝不自动切换生产
|
||||||
|
all) deploy_web; deploy_app; deploy_backend mysql 1; verify; prune ;;
|
||||||
|
verify) verify ;;
|
||||||
|
prune) prune ;;
|
||||||
|
backups) list_backups ;;
|
||||||
|
rollback) rollback "${2:-}" ;;
|
||||||
|
*) sed -n '2,30p' "$0"; exit 1 ;;
|
||||||
|
esac
|
||||||
+28
-12
@@ -1,28 +1,44 @@
|
|||||||
# Docker 部署环境变量(案例)
|
# ============================================================================
|
||||||
|
# Docker 部署环境变量示例
|
||||||
|
# 用法:cp docker/.env.example docker/.env 然后改成真实值(.env 不会被提交)
|
||||||
|
# 注意:本文件会被提交到仓库,只放占位符,不要写真实口令
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
# 数据库配置
|
# ---------- 数据库(MySQL)----------
|
||||||
MYSQL_ROOT_PASSWORD=your_mysql_root_password
|
MYSQL_ROOT_PASSWORD=change_me_root_password
|
||||||
MYSQL_DATABASE=fastapiadmin
|
MYSQL_DATABASE=fastapiadmin
|
||||||
MYSQL_USER=fastapiadmin
|
MYSQL_USER=fastapiadmin
|
||||||
MYSQL_PASSWORD=your_mysql_password
|
MYSQL_PASSWORD=change_me_app_password
|
||||||
MYSQL_PORT=3306
|
MYSQL_PORT=3306
|
||||||
|
# 说明:compose 把 MySQL 端口绑定在 127.0.0.1(只对本机开放),
|
||||||
|
# 容器之间通过 app_network 互联,不依赖宿主机端口。
|
||||||
|
|
||||||
# Redis配置
|
# ---------- Redis ----------
|
||||||
REDIS_PASSWORD=your_redis_password
|
REDIS_PASSWORD=change_me_redis_password
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
# 后端配置
|
# ---------- 后端 ----------
|
||||||
BACKEND_PORT=8001
|
BACKEND_PORT=8001
|
||||||
|
# 使用的后端镜像 tag:
|
||||||
|
# 3.0.0 表示「当前线上版本」(deploy-artifacts.sh 部署时写入)
|
||||||
|
# mysql-<时间戳> MySQL 驱动变体
|
||||||
|
# postgres-<时间戳> PostgreSQL 驱动变体(切换数据库时手动指定)
|
||||||
BACKEND_IMAGE_TAG=3.0.0
|
BACKEND_IMAGE_TAG=3.0.0
|
||||||
# 日志级别(DEBUG/INFO/WARNING/ERROR)
|
|
||||||
LOGGER_LEVEL=INFO
|
LOGGER_LEVEL=INFO
|
||||||
|
# 运行环境,对应 backend/env/.env.{dev,prod}
|
||||||
|
DEPLOY_ENV=prod
|
||||||
|
# 数据库类型(仅当使用外部数据库、不靠 compose 注入时才有意义)
|
||||||
|
# DATABASE_TYPE=mysql
|
||||||
|
|
||||||
# Nginx配置
|
# ---------- Nginx ----------
|
||||||
HTTP_PORT=80
|
HTTP_PORT=80
|
||||||
HTTPS_PORT=443
|
HTTPS_PORT=443
|
||||||
|
# nginx 镜像版本(由 docker-compose.override.yaml 读取;留空用 1.27-alpine)
|
||||||
|
NGINX_TAG=1.27-alpine
|
||||||
|
|
||||||
# 项目配置
|
# ---------- 项目 ----------
|
||||||
PROJECT_NAME=FastapiAdmin
|
PROJECT_NAME=FastapiAdmin
|
||||||
|
|
||||||
# 部署环境
|
# ---------- 部署方式 ----------
|
||||||
DEPLOY_ENV=prod
|
# 健康检查宽限期(秒):容器首次启动需要跑迁移,慢机器上可调大
|
||||||
|
HEALTHCHECK_START_PERIOD=40s
|
||||||
|
|||||||
+33
-13
@@ -1,26 +1,46 @@
|
|||||||
# 使用 python:3.12-slim 镜像作为基础镜像
|
# ============================================================================
|
||||||
|
# FastapiAdmin 后端镜像(源码构建路径)
|
||||||
|
#
|
||||||
|
# 构建参数 DB 决定安装哪套数据库驱动:
|
||||||
|
# docker build --build-arg DB=mysql -f docker/backend/Dockerfile . # 默认
|
||||||
|
# docker build --build-arg DB=postgres -f docker/backend/Dockerfile .
|
||||||
|
#
|
||||||
|
# 注意:正式部署建议用 deploy-artifacts.sh(本地构建镜像 → docker save/load),
|
||||||
|
# 这样服务器上不需要源码,也不需要构建工具链。
|
||||||
|
# ============================================================================
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
# 使用 LABEL 替代 MAINTAINER
|
|
||||||
LABEL maintainer="948080782@qq.com"
|
LABEL maintainer="948080782@qq.com"
|
||||||
|
|
||||||
# 设置时区
|
ARG DB=mysql
|
||||||
ENV TZ Asia/Shanghai
|
|
||||||
|
ENV TZ=Asia/Shanghai \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
LANG=C.UTF-8 \
|
||||||
|
DB=${DB}
|
||||||
|
|
||||||
# 设置容器内工作目录
|
|
||||||
WORKDIR /home
|
WORKDIR /home
|
||||||
|
|
||||||
# 先复制依赖清单并安装(利用 layer 缓存:代码变更时不重装依赖)
|
# 依赖清单先复制(layer 缓存:只改代码时不重装依赖)
|
||||||
COPY ./backend/requirements.txt .
|
COPY ./backend/requirements/ /home/requirements/
|
||||||
|
|
||||||
# 安装依赖
|
# 只装所选数据库的驱动 + 存储与加密依赖
|
||||||
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
RUN pip install --no-cache-dir \
|
||||||
|
-r /home/requirements/base.txt \
|
||||||
|
-r /home/requirements/db/${DB}.txt \
|
||||||
|
-r /home/requirements/storage.txt \
|
||||||
|
-i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
|
||||||
# 复制应用代码(backend/.venv、logs、缓存等由根 .dockerignore 排除)
|
# 应用代码(backend/.venv、logs、缓存、真实 .env.* 由根 .dockerignore 排除)
|
||||||
COPY ./backend/ .
|
COPY ./backend/ /home/
|
||||||
|
|
||||||
|
# 以非 root 用户运行;运行时需要写的目录预先建好并交给该用户
|
||||||
|
RUN useradd --uid 1000 --create-home --shell /usr/sbin/nologin app \
|
||||||
|
&& mkdir -p /home/logs /home/static/upload /home/alembic/versions \
|
||||||
|
&& chown -R app:app /home
|
||||||
|
USER app
|
||||||
|
|
||||||
# 暴露端口
|
|
||||||
EXPOSE 8001
|
EXPOSE 8001
|
||||||
|
|
||||||
# 运行应用
|
|
||||||
CMD ["python", "main.py", "run", "--env=prod"]
|
CMD ["python", "main.py", "run", "--env=prod"]
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# 优化项:静态资源缓存 + nginx 版本固定。撤销:删除本文件并重建 nginx。
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:${NGINX_TAG:-1.27-alpine}
|
||||||
|
volumes:
|
||||||
|
- ./nginx/cache-headers.inc:/etc/nginx/cache-headers.inc:ro
|
||||||
|
- ./nginx/acme:/usr/share/nginx/html/acme:ro
|
||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
MYSQL_USER: "${MYSQL_USER:-fastapiadmin}"
|
MYSQL_USER: "${MYSQL_USER:-fastapiadmin}"
|
||||||
MYSQL_PASSWORD: "${MYSQL_PASSWORD:?错误: 请设置 MYSQL_PASSWORD 环境变量}"
|
MYSQL_PASSWORD: "${MYSQL_PASSWORD:?错误: 请设置 MYSQL_PASSWORD 环境变量}"
|
||||||
ports:
|
ports:
|
||||||
- "${MYSQL_PORT:-3306}:3306"
|
- "127.0.0.1:${MYSQL_PORT:-3306}:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- mysql_data:/var/lib/mysql
|
- mysql_data:/var/lib/mysql
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
TZ: "Asia/Shanghai"
|
TZ: "Asia/Shanghai"
|
||||||
ports:
|
ports:
|
||||||
- "${REDIS_PORT:-6379}:6379"
|
- "127.0.0.1:${REDIS_PORT:-6379}:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
# 挂载自定义 redis.conf(若存在则覆盖默认配置)
|
# 挂载自定义 redis.conf(若存在则覆盖默认配置)
|
||||||
@@ -115,10 +115,22 @@ services:
|
|||||||
# 日志级别
|
# 日志级别
|
||||||
LOGGER_LEVEL: "${LOGGER_LEVEL:-INFO}"
|
LOGGER_LEVEL: "${LOGGER_LEVEL:-INFO}"
|
||||||
# 信任反向代理(nginx)传来的 X-Forwarded-Proto/For:否则 prod 的
|
# 信任反向代理(nginx)传来的 X-Forwarded-Proto/For:否则 prod 的
|
||||||
# HTTPSRedirectMiddleware 会把 https 请求误判为 http 造成 301 循环
|
# HTTPSRedirectMiddleware 会把 https 请求误判为 http 造成 301 循环。
|
||||||
FORWARDED_ALLOW_IPS: "*"
|
# 只信任应用网络子网(默认 10.255.178.0/24),不要用 "*"——否则客户端可伪造
|
||||||
|
# X-Forwarded-For 绕过登录限流与来源 IP 记录。
|
||||||
|
FORWARDED_ALLOW_IPS: "${FORWARDED_ALLOW_IPS:-10.255.178.0/24}"
|
||||||
|
# 令牌签名密钥与数据加密密钥(必须显式提供,禁止使用源码默认值)
|
||||||
|
SECRET_KEY: "${SECRET_KEY:?错误: 请设置 SECRET_KEY 环境变量(openssl rand -hex 32)}"
|
||||||
|
DATA_ENCRYPTION_KEY: "${DATA_ENCRYPTION_KEY:-}"
|
||||||
|
# 轮换前的旧主密钥(逗号分隔),仅用于解密历史密文
|
||||||
|
DATA_ENCRYPTION_OLD_KEYS: "${DATA_ENCRYPTION_OLD_KEYS:-}"
|
||||||
|
# 生产跨域白名单(逗号分隔)。未配置时应用不再回落通配
|
||||||
|
PROD_CORS_ORIGINS: "${PROD_CORS_ORIGINS:-}"
|
||||||
|
# 演示模式:为 true 时禁止写操作。当前线上为 true,如需可写请设为 false
|
||||||
|
DEMO_ENABLE: "${DEMO_ENABLE:-true}"
|
||||||
ports:
|
ports:
|
||||||
- "${BACKEND_PORT:-8001}:8001"
|
# 仅绑定本机:数据面不对公网开放,容器之间经 app_network 互联
|
||||||
|
- "127.0.0.1:${BACKEND_PORT:-8001}:8001"
|
||||||
volumes:
|
volumes:
|
||||||
# ── 开发热更新(可选)─────────────────────
|
# ── 开发热更新(可选)─────────────────────
|
||||||
# 取消注释将宿主机代码覆盖容器内代码。生产环境保持注释,
|
# 取消注释将宿主机代码覆盖容器内代码。生产环境保持注释,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# 静态资源长缓存 + 隐藏文件防护。被 nginx.conf 的 /web、/app location include。
|
||||||
|
# 撤销:删除本文件 + 移除 nginx.conf 中对应 include 行,然后重建 nginx。
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
location ~* ^/(web|app|docs)/(js|css|assets|static)/ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
location ~* ^/(web|app|docs)/[^/]*\.(?:png|jpe?g|gif|svg|webp|ico|woff2?|ttf|eot)$ {
|
||||||
|
add_header Cache-Control "public, max-age=2592000";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
+33
-2
@@ -69,11 +69,22 @@ http {
|
|||||||
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
|
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
|
||||||
|
|
||||||
# ==================== HTTP -> HTTPS 重定向 ====================
|
# ==================== HTTP -> HTTPS 重定向 ====================
|
||||||
# HTTP server块 - 重定向到HTTPS
|
# HTTP server块 - 重定向到HTTPS(放行 ACME 校验路径,供证书自动续期使用)
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name service.fastapiadmin.com;
|
server_name service.fastapiadmin.com;
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
|
# 证书续期校验路径(ACME http-01):必须在 301 之前放行,
|
||||||
|
# 否则签发/续期校验请求会被重定向到 https 而导致验证失败。
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /usr/share/nginx/html/acme;
|
||||||
|
default_type "text/plain";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==================== HTTPS Server ====================
|
# ==================== HTTPS Server ====================
|
||||||
@@ -101,17 +112,37 @@ http {
|
|||||||
|
|
||||||
# ==================== 前端(/web) ====================
|
# ==================== 前端(/web) ====================
|
||||||
location /web {
|
location /web {
|
||||||
|
include cache-headers.inc; # 静态资源长缓存(优化项,可删除)
|
||||||
alias /usr/share/nginx/html/web/dist;
|
alias /usr/share/nginx/html/web/dist;
|
||||||
try_files $uri $uri/ /web/index.html; #解决页面刷新404问题
|
try_files $uri $uri/ /web/index.html; #解决页面刷新404问题
|
||||||
}
|
}
|
||||||
|
location ~ /\. { deny all; access_log off; log_not_found off; }
|
||||||
|
|
||||||
|
|
||||||
# ==================== 小程序 H5(/app) ====================
|
# ==================== 小程序 H5(/app) ====================
|
||||||
# 已禁用:app 不开源,如需使用请取消注释
|
# 已禁用:app 不开源,如需使用请取消注释
|
||||||
location /app {
|
location /app {
|
||||||
|
include cache-headers.inc; # 静态资源长缓存(优化项,可删除)
|
||||||
alias /usr/share/nginx/html/app/dist/build/h5;
|
alias /usr/share/nginx/html/app/dist/build/h5;
|
||||||
try_files $uri $uri/ /app/index.html; #解决页面刷新404问题
|
try_files $uri $uri/ /app/index.html; #解决页面刷新404问题
|
||||||
}
|
}
|
||||||
|
location ~ /\. { deny all; access_log off; log_not_found off; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 封堵接口文档(仅本机/内网可访问)====================
|
||||||
|
location ~ ^/api/v1/(docs|redoc|openapi\.json)$ {
|
||||||
|
allow 127.0.0.1;
|
||||||
|
allow 10.0.0.0/8;
|
||||||
|
allow 172.16.0.0/12;
|
||||||
|
allow 192.168.0.0/16;
|
||||||
|
deny all;
|
||||||
|
proxy_pass http://backend:8001;
|
||||||
|
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 代理 ====================
|
# ==================== 后端 API 代理 ====================
|
||||||
location /api/v1 {
|
location /api/v1 {
|
||||||
limit_req zone=api_limit burst=60 nodelay;
|
limit_req zone=api_limit burst=60 nodelay;
|
||||||
|
|||||||
Reference in New Issue
Block a user