From f63c0c55c91226ded7004c9763110318deff2592 Mon Sep 17 00:00:00 2001 From: gavindiaz Date: Sun, 25 Oct 2026 15:35:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Docker=20=E5=8C=96=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=EF=BC=88nginx-proxy-manager=20=E5=8F=8D=E4=BB=A3=EF=BC=8C?= =?UTF-8?q?=E5=8D=95=E5=9F=9F=E5=90=8D=20/api=20=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker-compose.yml: backend + frontend 两服务,挂 NPM 所在外部网络、不暴露宿主端口; data/uploads 用 bind mount 持久化(deploy/ 下,gitignored) - backend/Dockerfile: python:3.12-slim + 清华 PyPI 镜像(ARG 可覆盖) - frontend/Dockerfile: 多阶段 node:22 + npm ci + next build(NEXT_PUBLIC_API_BASE 构建期 注入,npmmirror 默认)→ next start - .env.example: 全量部署环境变量模板(compose 替换变量 + 后端密钥) - deploy/README.md: 服务器部署手册(NPM 配置 / DNS / 数据迁移 / 运维 / 大陆网络) Co-Authored-By: Claude Code --- .dockerignore | 30 +++++++++++ .env.example | 49 ++++++++++++++++++ .gitignore | 2 + backend/Dockerfile | 23 +++++++++ deploy/README.md | 120 ++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 35 +++++++++++++ frontend/Dockerfile | 34 +++++++++++++ 7 files changed, 293 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 backend/Dockerfile create mode 100644 deploy/README.md create mode 100644 docker-compose.yml create mode 100644 frontend/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a550947 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# 构建上下文精简(避免把巨型/敏感目录发给 docker daemon) +.git +.gitignore +**/.venv +**/__pycache__ +**/*.py[cod] +**/node_modules +**/.next +**/.turbo +**/tsconfig.tsbuildinfo +**/next-env.d.ts + +# 环境与数据(密钥不进镜像;数据由卷挂载) +.env +.env.* +!.env.example +backend/data +backend/uploads +deploy/data +deploy/uploads +deploy/README.md +*.db +*.db-* +*.epub +*.log + +# 本地工具 +.idea +.vscode +.DS_Store diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8e196c9 --- /dev/null +++ b/.env.example @@ -0,0 +1,49 @@ +# ===== BookMate 部署环境变量模板 ===== +# 复制为仓库根目录的 `.env` 后填写(.env 已被 gitignore,不会提交)。 +# 这个文件同时服务两件事: +# 1) docker compose 的 ${NEXT_PUBLIC_API_BASE} / ${NPM_NETWORK} 替换 +# 2) backend 容器的 env_file(注入后端所有环境变量) + +# —— compose 替换变量 —— +# 前端(浏览器端)调后端 API 的地址:改成你实际用的域名,必须是 https 且带 /api 后缀 +#(NPM 里 location /api 反代到 backend) +NEXT_PUBLIC_API_BASE=https://bookmate.example.com/api +# 服务器上 nginx-proxy-manager 容器所在的 docker 网络名: +# 在服务器执行 `docker network ls` 查看,通常是 npm_default 或 nginx-proxy-manager_default +NPM_NETWORK=npm_default + +# —— 后端运行与存储(容器内路径,由 docker-compose 卷挂载,一般不用改)—— +DATABASE_URL=sqlite+aiosqlite:////app/data/bookmate.db +UPLOAD_DIR=/app/uploads +DATA_DIR=/app/data +API_HOST=0.0.0.0 +API_PORT=8000 + +# 站点访问密码:部署必须设置!留空 = 任何人可访问 +SITE_PASSWORD= + +# ===== 模型 Provider(至少配一个;也可启动后在网页「管理模型」里添加)===== +DEEPSEEK_API_KEY= +DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 +DEEPSEEK_MODEL=deepseek-chat + +OPENAI_API_KEY= +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_MODEL=gpt-4o + +ANTHROPIC_API_KEY= +ANTHROPIC_MODEL=claude-sonnet-4-20250514 + +OPENROUTER_API_KEY= +OPENROUTER_MODEL=openrouter/auto + +# ===== 向量检索 & 重排(OpenAI 兼容 API,如硅基流动 / OpenRouter)===== +# embedding_base_url: 支持 OpenAI 兼容 /embeddings 的端点前缀 +EMBEDDING_BASE_URL=https://api.siliconflow.cn/v1 +EMBEDDING_API_KEY= +EMBEDDING_MODEL=BAAI/bge-m3 +# rerank: 硅基流动使用 /v1/rerank,OpenAI 不提供;留空则跳过重排 +RERANK_BASE_URL=https://api.siliconflow.cn/v1 +RERANK_API_KEY= +RERANK_MODEL=BAAI/bge-reranker-v2-m3 +EMBEDDING_BATCH_SIZE=64 diff --git a/.gitignore b/.gitignore index 1beff95..9fe4911 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ next-env.d.ts # Local application data (includes book files and saved model credentials) backend/data/ backend/uploads/ +deploy/data/ +deploy/uploads/ *.db *.db-* *.sqlite* diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..1158c38 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,23 @@ +# BookMate 后端镜像:FastAPI + SQLAlchemy async + SQLite +# 构建上下文是仓库根目录(docker-compose context: .),COPY 路径相对仓库根。 +FROM python:3.12-slim + +WORKDIR /app + +ENV PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=utf-8 \ + PIP_NO_CACHE_DIR=1 + +# 先装依赖(利用层缓存)。大陆网络默认走清华 PyPI 镜像;可在构建时用 --build-arg 覆盖: +# docker compose build --build-arg PIP_INDEX_URL=https://pypi.org/simple backend +ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple +COPY backend/requirements.txt /app/requirements.txt +RUN pip install -i "$PIP_INDEX_URL" -r /app/requirements.txt + +# 应用代码;密钥与数据不打包进镜像(.env 由 compose env_file 注入,data/uploads 由卷挂载) +COPY backend/app /app/app + +EXPOSE 8000 + +# 生产用:不挂 --reload +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..da6b533 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,120 @@ +# BookMate 云服务器部署手册(Docker + nginx-proxy-manager) + +> 服务器已装 Docker 与 nginx-proxy-manager(NPM)。BookMate 由 `backend`(FastAPI:8000)与 +> `frontend`(Next.js:3000)两个容器组成,挂到 NPM 所在网络,由 NPM 用域名反代 + TLS。 +> **不暴露任何宿主端口**,不会和你服务器上的其它应用冲突。 + +## 0. 准备 + +把仓库放到服务器(二选一): + +```bash +# A. git 克隆(推荐,后续更新方便) +git clone https://code.aifunny.ltd/gavindiaz/my_bookmate.git /opt/bookmate +cd /opt/bookmate + +# B. 或从本机拷贝(不含 .git 也行) +# scp -r ./bookmate user@server:/opt/ +``` + +## 1. 迁移现有数据(保留书库 / 对话 / 向量) + +> 只迁移 `backend/data`(含 `bookmate.db`)和 `backend/uploads`(EPUB 原文件)。 +> 先确认本机后端已停止(避免 db 锁),再拷贝。 + +```bash +# 在服务器上(目录结构照做) +mkdir -p /opt/bookmate/deploy/data /opt/bookmate/deploy/uploads + +# 本机执行(把路径换成你的) +scp backend/data/bookmate.db user@server:/opt/bookmate/deploy/data/ +scp -r backend/uploads/. user@server:/opt/bookmate/deploy/uploads/ +``` + +不迁移也能跑:服务器从空书库开始,之后在网页里重新导入即可。 + +## 2. 配置 .env + +```bash +cd /opt/bookmate +cp .env.example .env +# 编辑 .env,必改项: +# NEXT_PUBLIC_API_BASE=https://你的域名/api +# NPM_NETWORK= ← 服务器上 `docker network ls` 查到 NPM 所在网络名 +# SITE_PASSWORD= ← 站点访问密码(必须设,否则任何人可访问) +# DEEPSEEK_API_KEY / OPENROUTER_API_KEY / EMBEDDING_API_KEY / RERANK_API_KEY 等 +``` + +## 3. 构建并启动 + +```bash +cd /opt/bookmate +docker compose up -d --build +docker compose ps # 两个容器都应为 Up +# 无 NPM 时直连验证: +curl http://localhost:8000/api/health # {"status":"ok"} +curl http://localhost:3000 # 前端 HTML +``` + +## 4. NPM 反向代理 + TLS + +NPM 网页(通常 http://服务器IP:81)→ Hosts → Add Proxy Host: + +- **Domain Names**:`bookmate.example.com`(改成你的) +- **Forward Hostname / Port**: + - 根路径:`frontend` / `3000` +- **Custom Locations**(添加两段,最长前缀优先): + | Location | Forward Hostname | Port | + |---|---:|---:| + | `/api` | `backend` | `8000` | + | `/` | `frontend` | `3000` | +- **SSL**:Request a new SSL Certificate → Let's Encrypt → 勾选 Force SSL。 + +SSE 流式与文件上传都是普通 HTTP,NPM 无需 websocket 特殊配置。 + +## 5. DNS + +把 `bookmate.example.com` 的 **A 记录**指向服务器公网 IP,等解析生效即可访问 +`https://bookmate.example.com`。 + +## 6. 验证 + +1. 打开 `https://bookmate.example.com` → 出现登录页 → 输入 SITE_PASSWORD。 +2. 书库应显示迁移过来的书;打开一本书能看到迁移的对话。 +3. 上传一本新 EPUB → 对话提问 → 确认流式回答与引用正常。 +4. 发一条「第二大脑 / AI 助手」消息 → 确认工具(bm_*)能调用。 + +## 7. 日常运维 + +```bash +# 更新到最新代码并重建 +cd /opt/bookmate +git pull +docker compose up -d --build + +# 看日志 +docker compose logs -f backend +docker compose logs -f frontend + +# 备份(只备份数据即可,镜像可重建) +tar czf bookmate-data-backup.tgz deploy/data deploy/uploads +``` + +## 大陆网络 + +Dockerfile 已把国内源烘焙成默认值(可在构建时用 `--build-arg` 覆盖为官方源): + +- backend 的 pip:默认 `https://pypi.tuna.tsinghua.edu.cn/simple` +- frontend 的 npm:默认 `https://registry.npmmirror.com` + +基础镜像(`python:3.12-slim` / `node:22-alpine`)来自 Docker Hub,大陆直连可能很慢/失败: +- 临时用代理拉一次后即缓存(`docker pull python:3.12-slim && docker pull node:22-alpine`); +- 或给 Docker 配置 registry 镜像加速(如阿里云容器镜像服务),长期更稳。 + +## 常见问题 + +- **NPM 连不上容器**:`NPM_NETWORK` 没填对。`docker network ls` 确认 NPM 所在网络名, + 填到 `.env` 后 `docker compose down && docker compose up -d` 重建网络连接。 +- **页面能开但接口 404**:`/api` 的 Custom Location 没加,或 `NEXT_PUBLIC_API_BASE` 端口/域名 + 与 NPM 配置不一致(它内联在构建产物里,改了要重新 build)。 +- **向量没生效**:确认 `EMBEDDING_API_KEY` / `RERANK_API_KEY` 已填;上传新书后后台会自动生成向量。 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e047193 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +services: + backend: + build: + context: . + dockerfile: backend/Dockerfile + # 后端全部环境变量(含密钥)从根目录 .env 注入;pydantic 会忽略额外键 + env_file: ./.env + volumes: + - ./deploy/data:/app/data # SQLite 数据库(迁移时把本机 backend/data 拷到这里) + - ./deploy/uploads:/app/uploads # EPUB 原文件 + restart: unless-stopped + networks: + - bookmate + - npm-proxy + + frontend: + build: + context: . + dockerfile: frontend/Dockerfile + args: + # 构建期内联到浏览器代码;务必改成你实际使用的域名(带 /api 后缀) + NEXT_PUBLIC_API_BASE: ${NEXT_PUBLIC_API_BASE:-https://bookmate.example.com/api} + depends_on: + - backend + restart: unless-stopped + networks: + - bookmate + - npm-proxy + +networks: + bookmate: {} # 两服务互通(backend 内部调用,无宿主端口) + npm-proxy: + external: true + # 服务器上 nginx-proxy-manager 容器所在网络名,用 `docker network ls` 查到后填到 .env 的 NPM_NETWORK + name: ${NPM_NETWORK:-npm_default} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..36029d5 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,34 @@ +# BookMate 前端镜像:Next.js(经典 next start 方案,最稳妥) +# 构建上下文是仓库根目录,COPY 路径相对仓库根。 + +# —— 构建阶段:安装依赖 + 生产构建(NEXT_PUBLIC_API_BASE 在此时内联进浏览器代码)—— +FROM node:22-alpine AS builder +WORKDIR /app + +# 大陆网络默认走 npmmirror 镜像;可在构建时用 --build-arg 覆盖: +# docker compose build --build-arg NPM_REGISTRY=https://registry.npmjs.org frontend +ARG NPM_REGISTRY=https://registry.npmmirror.com +ENV npm_config_registry="$NPM_REGISTRY" + +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci + +COPY frontend/ . + +ARG NEXT_PUBLIC_API_BASE +ENV NEXT_PUBLIC_API_BASE=$NEXT_PUBLIC_API_BASE +RUN npm run build + +# —— 运行阶段:最小化拷贝出 .next / node_modules / public —— +FROM node:22-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production + +COPY --from=builder /app/package.json /app/package-lock.json ./ +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/next.config.ts ./ + +EXPOSE 3000 +CMD ["npm", "run", "start"]