feat(deploy): 个人 NAS 单机 Docker 部署方案

适用:内网 / 家庭 / 小团队(≤50 并发),无 K8s 需求。
特性:单容器 + SQLite + 端口暴露,最小维护成本。

新增:
- deploy/nas/Dockerfile: python:3.11-slim 基础镜像 + 非 root + HEALTHCHECK
- deploy/nas/start.sh: flask db upgrade 幂等迁移 + gunicorn (2 workers × 4 threads)
- deploy/nas/docker-compose.yaml: 单服务 + 4 个命名卷 (db/logs/session/upload) + 资源限制
- docs/deployment-nas.md: 完整部署/备份/升级/排障指南

完善:
- .env.example: 改为生产环境导向 (PEAR_ENV=pro,SECRET_KEY 必填)
- .dockerignore: 排除 deploy/dev (开发用容器配置)、tests/截图、一次性脚本
- applications/view/public/__init__.py: 注释说明 /healthz 已在 applications/view/health.py 注册

复用现有:
- /healthz 接口 (applications/view/health.py) 含 DB 探活,DB 不可达返回 503
  → 容器 HEALTHCHECK 直接用 /healthz 即可

⚠️ 本机无 docker,未执行 docker build 验证;已静态校验 YAML/bash 语法、
   文件路径、.env 字段消费、ProConfig 严格性、/healthz 端点可用性。
   首次 NAS 上部署时跑一遍文档步骤即可。
This commit is contained in:
bwstudio
2026-09-06 18:35:31 +08:00
parent 0ffc103e46
commit 98edfdaf2d
7 changed files with 498 additions and 41 deletions
+60 -7
View File
@@ -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
+37 -33
View File
@@ -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 / propro 会强制校验 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
+2 -1
View File
@@ -18,4 +18,5 @@ bp = nav_bp
def register_public_bp(app):
"""注册 public 子蓝图"""
app.register_blueprint(about_bp)
app.register_blueprint(friend_bp)
app.register_blueprint(friend_bp)
# /healthz 已在 applications/view/health.py 注册(位于 applications/view/__init__.py),不在这里重复
+54
View File
@@ -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"]
+81
View File
@@ -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_dbSQLite 数据库文件
# - flask_logs:容器内 /app/logsgunicorn + Flask 日志)
# - flask_sessionFlask-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
# - 200DB OK
# - 503DB 不可达(让容器被标 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
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
# Pear Admin Flask - NAS 生产启动脚本
#
# 流程:
# 1. alembic 迁移到最新版本(幂等;migrations 文件夹随镜像打包)
# 2. flask admin init 幂等初始化菜单/权限(已有则跳过)
# 3. exec 切换到 gunicornPID 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
+219
View File
@@ -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
# 编辑 .envSECRET_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 并发的场景完全够用。