diff --git a/.dockerignore b/.dockerignore index 716de06..fb2b7dc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,11 +1,64 @@ -# 构建的相关文件 -README.md +# ===================================================== +# Pear Admin Flask - Docker 构建期排除 +# ===================================================== +# 防止 .git / 缓存 / 测试截图 / 一次性脚本进镜像 +# 注意:运行时数据(pear.db)通过 docker-compose volume 挂载, +# 这里排除 *.db 只是防止源代码里的 db 文件被误 COPY -# Python 相关文件 -migrations/ +# ---- VCS ---- +.git/ +.gitignore +.github/ + +# ---- Python 缓存 ---- +__pycache__/ +*.py[cod] +*$py.class +*.so + +# ---- 虚拟环境 ---- +venv/ +.venv/ +env/ + +# ---- 数据库 / 运行时数据 ---- +# 注意:pear.db 不在源码里(运行时由容器创建 + 卷挂载),这里排除只是兜底 +*.db +*.db.bak +pear.db.before_dedup.* + +# ---- 日志 ---- +*.log +logs/ + +# ---- Flask ---- instance/ flask_session/ -venv/ + +# ---- IDE / 编辑器 ---- .idea/ -*.log -*.db +.vscode/ +*.swp + +# ---- 构建 / 测试产物 ---- +build/ +dist/ +*.egg-info/ +.pytest_cache/ +htmlcov/ + +# ---- 测试截图 / 探针脚本输出 ---- +scripts/_verify_*.png +scripts/screenshots/ +output/ + +# ---- 开发用 docker 配置(不进 NAS 镜像)---- +deploy/dev/ + +# ---- 一次性工具 ---- +nav_extracted.js +dedup_nav.py + +# ---- 环境变量文件(不进镜像)---- +.env +.env.*.local \ No newline at end of file diff --git a/.env.example b/.env.example index c37547f..a0f075c 100644 --- a/.env.example +++ b/.env.example @@ -1,40 +1,44 @@ -# ============================================================ -# Pear Admin Flask - 环境变量示例 -# ============================================================ -# 用法: -# 1) 复制本文件:cp .env.example .env -# 2) 按需修改 .env 中的值(不要提交 .env 到 git) -# 3) 应用启动时由 applications/__init__.py 自动 load_dotenv 读取 -# -# 加载优先级:系统环境变量 > .env > 代码默认值 -# ============================================================ +# ===================================================== +# Pear Admin Flask - 生产环境变量模板 +# ===================================================== +# 使用方式: +# 1. 复制本文件为 .env: cp .env.example .env +# 2. 修改 SECRET_KEY 为强随机值(必填) +# 3. 邮件 / 数据库如不需要可保持注释状态 +# 4. docker compose 会自动读取同级目录的 .env +# ===================================================== # ---- 运行模式 ---- -# dev: 开发模式(DEBUG=True,自动生成 SECRET_KEY,无需外部传入) -# production: 生产模式(DEBUG=False,强制要求 SECRET_KEY) -PEAR_ENV=dev +# 必填:dev / pro(pro 会强制校验 SECRET_KEY) +PEAR_ENV=pro -# ---- 安全 ---- -# 生产模式必填。可用以下命令生成: -# python -c "import secrets; print(secrets.token_urlsafe(48))" -SECRET_KEY= +# ---- Flask 应用入口 ---- +FLASK_APP=app.py + +# ---- 安全密钥(生产必填,强随机)---- +# 生成命令:python -c "import secrets; print(secrets.token_urlsafe(48))" +SECRET_KEY=PLEASE_REPLACE_WITH_RANDOM_STRING # ---- 数据库 ---- -# 默认是项目根目录下的 SQLite pear.db;生产推荐 MySQL。 -# MySQL 示例: -# SQLALCHEMY_DATABASE_URI=mysql+pymysql://user:pass@127.0.0.1:3306/pear?charset=utf8mb4 -# SQLALCHEMY_DATABASE_URI= +# 默认 SQLite,存到容器内 /app/pear.db(已挂载为命名卷 pear_db,重启不丢) +# 如需切换 MySQL: +# SQLALCHEMY_DATABASE_URI=mysql+pymysql://user:pass@db:3306/pear?charset=utf8mb4 +# SQLALCHEMY_DATABASE_URI=sqlite:///../pear.db -# ---- 邮件(Flask-Mail)---- -# QQ 邮箱示例 -MAIL_SERVER=smtp.qq.com -MAIL_USE_TLS=false -MAIL_USE_SSL=true -MAIL_PORT=465 -MAIL_USERNAME=your_account@qq.com -MAIL_PASSWORD=your_authorization_code_here -MAIL_DEFAULT_SENDER=your_account@qq.com +# ---- 时区 ---- +TZ=Asia/Shanghai -# ---- Flask ---- -# FLASK_APP=app.py -# FLASK_DEBUG=1 +# ---- 邮件(可选,未启用找回密码等功能可不填)---- +# MAIL_SERVER=smtp.qq.com +# MAIL_PORT=465 +# MAIL_USE_SSL=true +# MAIL_USERNAME=yourname@qq.com +# MAIL_PASSWORD=authorization_code_here +# MAIL_DEFAULT_SENDER=yourname@qq.com + +# ---- 监听端口(容器内部)---- +# 仅在 docker-compose.yaml 的 ports 段同步修改时使用;默认 5000 +APP_PORT=5000 + +# ---- Gunicorn worker 数量(NAS 单容器推荐 2-4)---- +# GUNICORN_WORKERS=2 \ No newline at end of file diff --git a/applications/view/public/__init__.py b/applications/view/public/__init__.py index 8bcf779..93faa28 100644 --- a/applications/view/public/__init__.py +++ b/applications/view/public/__init__.py @@ -18,4 +18,5 @@ bp = nav_bp def register_public_bp(app): """注册 public 子蓝图""" app.register_blueprint(about_bp) - app.register_blueprint(friend_bp) \ No newline at end of file + app.register_blueprint(friend_bp) + # /healthz 已在 applications/view/health.py 注册(位于 applications/view/__init__.py),不在这里重复 \ No newline at end of file diff --git a/deploy/nas/Dockerfile b/deploy/nas/Dockerfile new file mode 100644 index 0000000..1bba123 --- /dev/null +++ b/deploy/nas/Dockerfile @@ -0,0 +1,54 @@ +# ===================================================== +# Pear Admin Flask - 个人 NAS / 单机部署 Dockerfile +# ===================================================== +# 适用场景:内网访问 / 少量并发 / 数据量小 +# 基础:python:3.11-slim(~150 MB),无 MySQL,纯 SQLite +# +# 构建: docker build -f deploy/nas/Dockerfile -t pear-admin-nas . +# 运行: docker compose -f deploy/nas/docker-compose.yaml up -d +# ===================================================== + +FROM python:3.11-slim + +# 容器内时区与 Python 输出 +ENV TZ=Asia/Shanghai \ + PYTHONUNBUFFERED=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_NO_CACHE_DIR=1 \ + PEAR_ENV=pro \ + FLASK_APP=app.py + +# 安装时区数据 + 健康检查用的 curl +# (curl 在 slim 里已有,但保持显式声明便于以后切到 alpine) +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl tzdata \ + && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ + && echo $TZ > /etc/timezone \ + && rm -rf /var/lib/apt/lists/* + +# 非 root 运行(生产最佳实践) +RUN useradd -m -u 1001 appuser + +WORKDIR /app + +# 先只 copy 依赖文件 → 利用 Docker 缓存;requirements 不变则跳过 pip install +COPY requirements.txt ./ +RUN pip install -r requirements.txt \ + && pip install gunicorn==21.2.0 + +# 再 copy 全部源码 +COPY --chown=appuser:appuser . . + +# 启动脚本加执行权限 +RUN chmod +x /app/deploy/nas/start.sh + +# 切换到非 root +USER appuser + +EXPOSE 5000 + +# 容器健康检查:访问 /healthz 接口(applications/view/health.py 提供,DB 不可达返回 503) +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -fsS http://localhost:5000/healthz || exit 1 + +CMD ["/app/deploy/nas/start.sh"] \ No newline at end of file diff --git a/deploy/nas/docker-compose.yaml b/deploy/nas/docker-compose.yaml new file mode 100644 index 0000000..616ac1c --- /dev/null +++ b/deploy/nas/docker-compose.yaml @@ -0,0 +1,81 @@ +# ===================================================== +# Pear Admin Flask - 个人 NAS 单机部署 +# ===================================================== +# 使用: +# 1) cp .env.example .env 并修改 SECRET_KEY +# 2) docker compose -f deploy/nas/docker-compose.yaml up -d +# 3) 访问 http://NAS_IP:5000 +# +# 数据持久化: +# - pear_db:SQLite 数据库文件 +# - flask_logs:容器内 /app/logs(gunicorn + Flask 日志) +# - flask_session:Flask-Session 文件会话 +# - flask_upload:上传的图片/文件 +# ===================================================== + +services: + flask: + build: + context: ../.. # 项目根目录(含 requirements.txt) + dockerfile: deploy/nas/Dockerfile + image: pear-admin-nas:latest + container_name: pear-admin-nas + restart: unless-stopped + ports: + - "5000:5000" # 容器 5000 → NAS 5000;可改为 127.0.0.1:5000:5000 仅本地访问 + + # 环境变量注入(生产环境用 .env 自动读取) + env_file: + - ../../.env + + # 数据持久化卷(NAS 上是命名卷;如想直挂目录,把左边改为宿主路径) + volumes: + - pear_db:/app/pear.db # SQLite 数据库 + - flask_logs:/app/logs # gunicorn / Flask 日志 + - flask_session:/app/flask_session # Flask-Session 文件会话 + - flask_upload:/app/static/upload # 上传的图片 + + # 资源限制(NAS 推荐;按需调整) + deploy: + resources: + limits: + cpus: '2.0' # 单容器最多用 2 核 + memory: 1024M # 1 GB 上限 + reservations: + cpus: '0.5' + memory: 256M + + # 健康检查:applications/view/health.py 提供 /healthz + # - 200:DB OK + # - 503:DB 不可达(让容器被标 unhealthy) + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:5000/healthz"] + interval: 30s + timeout: 10s + start_period: 40s + retries: 3 + + # 网络模式默认 bridge;如 NAS 上启用了 host 网络可获得更好的局域网性能: + # network_mode: host + networks: + - pear_net + +# ===================================================== +# 命名卷:首次 `docker compose up` 自动创建 +# 查看: docker volume ls | grep pear +# 备份: docker run --rm -v pear-admin-nas_pear_db:/data -v $(pwd)/bak:/backup \ +# alpine cp /data/pear.db /backup/pear-$(date +%F).db +# ===================================================== +volumes: + pear_db: + name: pear-admin-nas_pear_db + flask_logs: + name: pear-admin-nas_logs + flask_session: + name: pear-admin-nas_session + flask_upload: + name: pear-admin-nas_upload + +networks: + pear_net: + driver: bridge \ No newline at end of file diff --git a/deploy/nas/start.sh b/deploy/nas/start.sh new file mode 100644 index 0000000..f276be5 --- /dev/null +++ b/deploy/nas/start.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Pear Admin Flask - NAS 生产启动脚本 +# +# 流程: +# 1. alembic 迁移到最新版本(幂等;migrations 文件夹随镜像打包) +# 2. flask admin init 幂等初始化菜单/权限(已有则跳过) +# 3. exec 切换到 gunicorn,PID 1 由 gunicorn 接管 +# +# 注意:必须 exec,否则容器启动后 PID 1 是 shell 而非 gunicorn, +# docker stop 发出的 SIGTERM 不会被正确转发,graceful shutdown 失效。 + +set -e + +echo "=== [1/3] 等待数据库就绪 ===" +# SQLite 不需要等待;这里预留扩展位(如未来切 MySQL,可在此加 wait-for-it) +if [[ "${SQLALCHEMY_DATABASE_URI:-}" == mysql* ]]; then + echo "检测到 MySQL,等待 10s 让 db 服务启动..." + sleep 10 +fi + +echo "=== [2/3] 初始化数据库(幂等) ===" +# flask 命令依赖 FLASK_APP;镜像里已经设过;这里再 fallback 一次 +export FLASK_APP=${FLASK_APP:-app.py} + +# 数据库迁移:migrate 仅当模型有变才生成新版本;upgrade 永远是幂等的 +flask db upgrade || echo "WARN: flask db upgrade failed (首次启动可能正常)" +flask admin init || true + +echo "=== [3/3] 启动 Gunicorn ===" +# worker 数:NAS 单容器 2-4 足够;线程 4 让 4 类 IO 密集(埋点/统计)能并发 +WORKERS=${GUNICORN_WORKERS:-2} +THREADS=4 +TIMEOUT=60 + +exec gunicorn \ + --bind 0.0.0.0:5000 \ + --workers "$WORKERS" \ + --threads "$THREADS" \ + --timeout "$TIMEOUT" \ + --graceful-timeout 30 \ + --keep-alive 5 \ + --access-logfile - \ + --error-logfile - \ + --log-level info \ + app:app \ No newline at end of file diff --git a/docs/deployment-nas.md b/docs/deployment-nas.md new file mode 100644 index 0000000..9f058f3 --- /dev/null +++ b/docs/deployment-nas.md @@ -0,0 +1,219 @@ +# Pear Admin Flask - 个人 NAS 部署指南 + +适用:内网 / 家庭 / 小团队(≤ 50 并发用户,无 K8s 需求)。 + +--- + +## 1. 前置检查 + +NAS 上需安装: + +| 软件 | 最低版本 | 验证命令 | +|---|---|---| +| Docker Engine | 20.10+ | `docker --version` | +| Docker Compose | v2.x | `docker compose version` | + +> 如果 NAS 是群晖 / 威联通,请在套件中心安装 **Container Manager** 或自行 SSH 安装 docker。 + +--- + +## 2. 准备 .env + +```bash +cd /path/to/pear-admin-flask +cp .env.example .env +# 编辑 .env:SECRET_KEY 必须改成强随机字符串 +python -c "import secrets; print(secrets.token_urlsafe(48))" +# 把输出贴到 .env 的 SECRET_KEY= +``` + +`.env` **绝不能提交到 git**(已在 `.gitignore` 中)。 + +--- + +## 3. 首次构建 + 启动 + +```bash +# 进入项目目录 +cd /volume1/docker/pear-admin-flask # 改成你自己的路径 + +# 构建镜像(首次约 3-5 分钟) +docker compose -f deploy/nas/docker-compose.yaml build + +# 后台启动 +docker compose -f deploy/nas/docker-compose.yaml up -d + +# 查看启动日志(应看到 "Listening at: http://0.0.0.0:5000") +docker compose -f deploy/nas/docker-compose.yaml logs -f +``` + +启动成功后访问:`http://NAS_IP:5000` + +默认账号 `admin` / `123456`(首次登录后**强烈建议改密码**)。 + +--- + +## 4. 健康检查 + +```bash +# 进程是否还活着(DB OK 返回 200;DB 不可达返回 503) +curl -i http://NAS_IP:5000/healthz +# 200 OK: {"status":"ok","db_ok":true,"env":"production","system":"Pear Admin","timestamp":"..."} + +# 容器视角的健康状态(Docker 30s 探一次) +docker inspect --format='{{.State.Health.Status}}' pear-admin-nas +# healthy / unhealthy / starting +``` + +--- + +## 5. 端口与局域网访问 + +`docker-compose.yaml` 中默认 `5000:5000`,局域网内任意设备 `http://NAS_IP:5000` 都能访问。 + +如要改端口(如 NAS 上 5000 被占用): + +```yaml +ports: + - "8080:5000" # 宿主 8080 → 容器 5000 +``` + +手机访问:`http://NAS_IP:8080`(确保 NAS 防火墙放行)。 + +--- + +## 6. 数据持久化 + +| 卷名 | 容器内路径 | 内容 | +|---|---|---| +| `pear-admin-nas_pear_db` | `/app/pear.db` | SQLite 数据库 | +| `pear-admin-nas_logs` | `/app/logs` | gunicorn / Flask 日志 | +| `pear-admin-nas_session` | `/app/flask_session` | 登录会话文件 | +| `pear-admin-nas_upload` | `/app/static/upload` | 上传图片 | + +查看卷: + +```bash +docker volume ls | grep pear +``` + +### 备份 SQLite 数据库 + +```bash +# 备份(推荐每天 1 次,可加入 cron) +docker run --rm \ + -v pear-admin-nas_pear_db:/data \ + -v /volume1/docker/pear-backup:/backup \ + alpine sh -c "cp /data/pear.db /backup/pear-$(date +%F).db && ls -la /backup/" +``` + +恢复: + +```bash +docker compose -f deploy/nas/docker-compose.yaml down +docker run --rm \ + -v pear-admin-nas_pear_db:/data \ + -v /volume1/docker/pear-backup:/backup \ + alpine sh -c "rm -f /data/pear.db && cp /backup/pear-2026-09-06.db /data/pear.db" +docker compose -f deploy/nas/docker-compose.yaml up -d +``` + +--- + +## 7. 升级流程 + +```bash +# 1. 拉取最新代码 +cd /volume1/docker/pear-admin-flask +git pull + +# 2. 重新构建镜像 +docker compose -f deploy/nas/docker-compose.yaml build + +# 3. 重启容器(migrations 自动跑) +docker compose -f deploy/nas/docker-compose.yaml up -d + +# 4. 验证 +curl -i http://NAS_IP:5000/healthz +``` + +数据库迁移由 `start.sh` 中的 `flask db upgrade` 自动执行(幂等)。 + +--- + +## 8. 常见问题 + +### Q1:容器启动后立刻退出 + +```bash +docker compose -f deploy/nas/docker-compose.yaml logs --tail=50 +``` + +最常见原因: +- `SECRET_KEY` 还是占位符 `PLEASE_REPLACE_WITH_RANDOM_STRING` → ProConfig 会拒绝启动 +- 端口被占用 → 修改 `ports` 段 + +### Q2:访问首页 502 / 拒绝连接 + +```bash +# 确认容器在跑 +docker ps | grep pear-admin-nas + +# 看健康状态 +docker inspect --format='{{.State.Health.Status}}' pear-admin-nas +``` + +### Q3:忘记 admin 密码 + +直接重置(删容器、再用空 pear.db 启动,再 `flask admin init` 重新生成默认账号): + +```bash +docker compose -f deploy/nas/docker-compose.yaml down +docker volume rm pear-admin-nas_pear_db +docker compose -f deploy/nas/docker-compose.yaml up -d +``` + +⚠️ **此操作会清空所有数据**,务必先备份。 + +### Q4:日志占满磁盘 + +```bash +# 查看日志大小 +docker system df -v + +# 清理已停止容器的日志 +docker compose -f deploy/nas/docker-compose.yaml down +docker volume rm pear-admin-nas_logs +docker compose -f deploy/nas/docker-compose.yaml up -d +``` + +### Q5:想要外网访问 + +需要做两件事: +1. NAS 上做端口映射(5000 → 公网 IP) + DDNS +2. **必须**在前面套一层 Nginx/Caddy 加 HTTPS(避免密码明文) + +可参考 `deploy/nas/README.md`(如果后续加入 nginx 反代方案)。 + +--- + +## 9. 性能调优 + +NAS 通常内存有限(4-8 GB)。如遇卡顿: + +```yaml +# docker-compose.yaml 中调小资源限制 +deploy: + resources: + limits: + memory: 768M # 从 1024M 降到 768M +``` + +或减少 worker: + +```yaml +environment: + - GUNICORN_WORKERS=1 # 内存紧张时降到 1 +``` + +Gunicorn 默认 `2 workers × 4 threads = 8 个并发槽位**,对 ≤ 50 并发的场景完全够用。 \ No newline at end of file