From 7ed55c8a8d6a951952178f511cf2613879ab6df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 15 Sep 2026 21:32:20 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20Docker/Nginx=20=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E4=BA=A7=E7=89=A9=E4=B8=8E=201Panel=20=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- .gitignore | 1 + deploy/.env.example | 27 ++++++++ deploy/README-deploy.md | 128 ++++++++++++++++++++++++++++++++++++++ deploy/docker-compose.yml | 28 +++++++++ deploy/nginx.conf | 40 ++++++++++++ server/.dockerignore | 6 ++ server/Dockerfile | 11 ++++ 7 files changed, 241 insertions(+) create mode 100644 deploy/.env.example create mode 100644 deploy/README-deploy.md create mode 100644 deploy/docker-compose.yml create mode 100644 deploy/nginx.conf create mode 100644 server/.dockerignore create mode 100644 server/Dockerfile diff --git a/.gitignore b/.gitignore index b94c2f0..6188760 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ server/.cache/ server/tracker/data/*.xdb sdk/node_modules/ sdk/dist/ +deploy/data/ diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..033d551 --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,27 @@ +# ============ Django 运行环境 ============ +# 生产固定 prod。设为 prod 后 SECRET_KEY/ALLOWED_HOSTS 必填,缺失启动即崩(fail-fast 设计,见 README 第 3 节) +DJANGO_ENV=prod +# 调试开关:生产保持 0 +DJANGO_DEBUG=0 + +# Django 密钥:JWT 签名与密码哈希加盐都依赖它。生成命令: +# python -c "import secrets; print(secrets.token_urlsafe(48))" +# 注意:token 泄露时唯一吊销手段就是换新 SECRET_KEY(全体登录态失效,见 README 第 9 节) +DJANGO_SECRET_KEY= + +# 允许访问的 Host,多个用英文逗号分隔,填实际域名,例:track.example.com +# 生产必填(fail-fast),不要填 * +DJANGO_ALLOWED_HOSTS= + +# ============ 数据库 ============ +# mysql(生产)/ sqlite(仅本地无 MySQL 的开发模式) +DB_ENGINE=mysql +# MySQL 地址,三条连接路径三选一(详细说明见 README 第 2 节): +# a) DB_HOST=host.docker.internal —— 需 MySQL 端口以 0.0.0.0 发布、账号 host 为 % +# b) 推荐:compose 加入 1panel-network 外部网络后 DB_HOST=,如 1Panel-mysql-xxxx +# c) DB_HOST=172.17.0.1 —— 走 docker0 网桥访问宿主机 +DB_HOST= +DB_NAME=tracker +DB_USER=tracker +DB_PASSWORD= +DB_PORT=3306 diff --git a/deploy/README-deploy.md b/deploy/README-deploy.md new file mode 100644 index 0000000..ba127dd --- /dev/null +++ b/deploy/README-deploy.md @@ -0,0 +1,128 @@ +# 部署指南(1Panel + Docker Compose) + +面向 1Panel 用户的生产部署。产物为 `docker compose` 两个服务:`web`(Django + gunicorn)与 `nginx`(admin SPA / SDK 静态 + API 反代),数据库复用 1Panel 已装的 MySQL。 + +## 1. 前置构建(仓库内完成,产出 dist 与 xdb) + +```bash +# 管理后台:产出 admin/dist/ +cd admin && pnpm install && pnpm build && cd .. +# 采集 SDK:产出 sdk/dist/v1.js +cd sdk && pnpm install && pnpm build && cd .. +``` + +下载 ip2region 离线库到 `deploy/data/ip2region.xdb`(compose 以只读卷挂载进容器,重建容器不丢): + +- GitHub:https://github.com/lionsoul2014/ip2region (`data/ip2region_v4.xdb`) +- 大陆备用:https://gitee.com/lionsoul/ip2region (同名路径) + +```bash +mkdir -p deploy/data +cp /path/to/ip2region_v4.xdb deploy/data/ip2region.xdb +``` + +## 2. MySQL 准备(1Panel 已有容器) + +在 1Panel 的 MySQL 里建库建账号(utf8mb4): + +```sql +CREATE DATABASE tracker CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER 'tracker'@'%' IDENTIFIED BY '<强密码>'; +GRANT ALL PRIVILEGES ON tracker.* TO 'tracker'@'%'; +FLUSH PRIVILEGES; +``` + +**容器连宿主 MySQL 的三条路径**(`.env` 的 `DB_HOST` 三选一): + +| 路径 | 做法 | DB_HOST | 备注 | +|---|---|---|---| +| a | 走宿主机发布端口(compose 已配 `extra_hosts: host.docker.internal:host-gateway`) | `host.docker.internal` | MySQL 端口需以 `0.0.0.0` 发布(1Panel 默认如此),账号 host 为 `%` | +| b **推荐** | compose 追加外部网络,与 MySQL 容器同网 | ``(如 `1Panel-mysql-xxxx`) | 不暴露额外端口,容器名在 1Panel 容器列表可见 | +| c | 走 docker0 网桥网关 | `172.17.0.1` | 网桥地址随环境可能变化,兜底用 | + +路径 b 的 compose 改法(文件末尾已有注释模板,取消注释即生效): + +```yaml +networks: + default: + name: 1panel-network + external: true +``` + +## 3. 配置 .env + +```bash +cd deploy +cp .env.example .env +``` + +填必填项:`DJANGO_SECRET_KEY`(生成命令见文件内注释)、`DJANGO_ALLOWED_HOSTS`(实际域名)、`DB_*`。 + +**fail-fast 设计**:`DJANGO_ENV=prod` 时缺 `SECRET_KEY` / `ALLOWED_HOSTS` 启动即崩(`RuntimeError`),不带病上线——这是有意为之的护栏,不是 bug。漏配时看容器日志 `docker compose logs web` 即可定位。 + +## 4. 部署 + +```bash +docker compose up -d --build +# 建管理员账号(JWT 登录用) +docker compose exec web python manage.py createsuperuser +``` + +浏览器访问 `http://<服务器IP>/`(有域名先做第 5 步 HTTPS)。 + +## 5. HTTPS(1Panel 二选一) + +- **做法一(推荐)**:1Panel「网站」→ 创建网站 → 反向代理到 `http://127.0.0.1:80`,在网站设置里申请 Let's Encrypt 证书并开启 HTTPS。本 compose 的 nginx 只听 80,TLS 由 1Panel 的 OpenResty 终结。 +- **做法二**:域名 DNS 直接解析到服务器,1Panel「网站」创建站点直接挂证书(证书路径填入 1Panel 证书管理),也能达到同样效果。 + +任选其一即可,务实为准。HTTPS 后 SDK endpoint 与后台访问地址同步换成 `https://`。 + +## 6. 建站点拿 site_id + +登录后台 → 站点管理 → 新建站点(填域名) → 列表里复制 `site_id`(形如 `site_xxxxxxxx`),接入目标网站时用它。 + +## 7. 目标网站接入 + +目标站 `` 里加两行(endpoint 与 SDK 地址换成正式域名): + +```html + + +``` + +## 8. 定时任务(不做则趋势图历史全零) + +1Panel「计划任务」→ 每日 00:10 执行(Shell 脚本): + +```bash +docker exec tracker-web python manage.py refresh_stats --days 2 +``` + +`refresh_stats` 聚合昨日明细为每日统计快照(`--days 2` 幂等重算近两天,补齐漏跑)。 + +## 9. JWT 应急 + +token 泄露时**唯一**吊销手段是更换 `DJANGO_SECRET_KEY`(JWT 由它签名,换钥即全体登出):重新生成 → 改 `.env` → `docker compose up -d`。后台没有"注销某 token"的接口,这是无状态 JWT 的设计取舍。 + +## 10. CDN / WAF 注意 + +目标网站套 CDN/WAF 后,到达采集 API 的 `X-Forwarded-For` 层数会变化(CDN 一层 + 本站 nginx 一层 = 2 层起)。取真实客户端 IP 依赖 DRF `NUM_PROXIES`(当前为 1,对应"仅前置一层可信代理"取末段)。CDN 层数变化后需同步调整 `server/config/settings.py` 的 `NUM_PROXIES` 并重建镜像,否则取到的是 CDN 节点 IP,地域与去重统计失真。 + +## 11. 大陆网络备用源 + +| 资源 | 主源 | 备用源 | +|---|---|---| +| admin 模板(Vite 等 npm 依赖) | npm registry | `pnpm config set registry https://registry.npmmirror.com` | +| ip2region xdb | GitHub lionsoul2014/ip2region | gitee.com/lionsoul/ip2region | +| pip 依赖 | pypi.org | 清华源(**Dockerfile 已内置** `-i https://pypi.tuna.tsinghua.edu.cn/simple`) | + +--- + +### 附:常用运维命令 + +```bash +docker compose logs -f web # 看后端日志 +docker compose up -d --build # 改代码后重建 +docker compose exec web python manage.py createsuperuser # 再建账号 +``` diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..dd6ad6b --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,28 @@ +# 埋点后端 + 前端 Nginx 一键部署(面向 1Panel,操作步骤见 README-deploy.md) +services: + web: + build: ../server + container_name: tracker-web # 计划任务/运维命令引用此名(README 第 8 节) + env_file: .env # 必含 DJANGO_ENV=prod DJANGO_DEBUG=0 DJANGO_SECRET_KEY DJANGO_ALLOWED_HOSTS DB_*(见 .env.example) + volumes: + - ./data/ip2region.xdb:/app/tracker/data/ip2region.xdb:ro # xdb 挂载(不是 docker cp,重建不丢) + extra_hosts: + - "host.docker.internal:host-gateway" # MySQL 连接路径 a 用(README 第 2 节) + restart: unless-stopped + nginx: + image: nginx:alpine + container_name: tracker-nginx + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ../admin/dist:/var/www/admin:ro + - ../sdk/dist:/var/www/sdk:ro + depends_on: + - web + restart: unless-stopped +# MySQL 走 1Panel 已有容器时,推荐追加以下外部网络并用 DB_HOST=(README 第 2 节路径 b): +# networks: +# default: +# name: 1panel-network +# external: true diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..c56244f --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,40 @@ +# 前端 Nginx:托管 admin SPA 与采集 SDK,/api/ 反代 Django(compose 服务名 web) +# HTTPS 不在此终结:由 1Panel「网站」功能(OpenResty)反代本 80 端口或直接挂证书, +# 二选一即可,见 README-deploy.md 第 5 节。 +server { + listen 80; + server_name _; + + # gzip 压缩 js/css/json(SDK v1.js 体积小,不设 min_length 阈值) + gzip on; + gzip_comp_level 6; + gzip_types text/css application/javascript text/javascript application/json; + + # Django API。client_max_body_size 与 settings DATA_UPLOAD_MAX_MEMORY_SIZE(256KB)对齐 + location /api/ { + client_max_body_size 256k; + proxy_pass http://web:8000; + # X-Forwarded-For 必传:DRF NUM_PROXIES=1 取末段还原真实客户端 IP + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + } + + # 采集 SDK(sdk/dist/v1.js)。no-cache:SDK 更新后及时生效 + location /sdk/ { + alias /var/www/sdk/; + add_header Cache-Control "no-cache"; + } + + # 管理后台 SPA:路由 fallback 到 index.html + location / { + root /var/www/admin; + try_files $uri $uri/ /index.html; + } + + # admin 构建产物带 hash 文件名,可长缓存 + location /assets/ { + root /var/www/admin; + add_header Cache-Control "public, max-age=2592000, immutable"; + } +} diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..860de44 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,6 @@ +.venv/ +db.sqlite3 +__pycache__/ +.cache/ +.pytest_cache/ +tracker/data/*.xdb diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..4e7e25a --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12-slim +WORKDIR /app +# 先只拷 requirements 加装依赖层,业务代码改动不触发重装 +COPY requirements.txt . +# 大陆网络备用源:清华 pypi 镜像 +RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple +COPY . . +EXPOSE 8000 +# 启动即 migrate(单副本场景简单可靠),随后 gunicorn 3 worker +# --forwarded-allow-ips=*:信任前置 Nginx 传入的 X-Forwarded-For(DRF NUM_PROXIES=1 取末段还原客户端 IP) +CMD ["sh", "-c", "python manage.py migrate && gunicorn config.wsgi:application -b 0.0.0.0:8000 -w 3 --forwarded-allow-ips=*"]