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:
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user