diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0da954e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +.git +.gitignore +.dockerignore + +.venv/ +venv/ +env/ +ENV/ +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ + +apps/frontend/node_modules/ +apps/frontend/dist/ +apps/frontend/.vite/ + +data/ +sessions/ +logs/ +*.log +*.pid +*.lock +!uv.lock + +.env +.env.* +!.env.example +!deploy/compose.env.example + +debug_page_source.html +debug_screenshot.png +chrome-linux64/ +chrome-win64/ +chromedriver +chromedriver.exe + +.playwright-cli/ +output/ +.DS_Store +Thumbs.db + +.claude/ +.codex/ +openspec/ diff --git a/.gitignore b/.gitignore index 6a76f8c..5319fbf 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,8 @@ debug_page_source.html debug_screenshot.png # 运行时文件 -sessions/ +sessions/* +!sessions/.gitkeep *.lock !/uv.lock *.log @@ -29,14 +30,16 @@ backend.pid frontend.pid # 数据库 -data/ +data/* +!data/.gitkeep # 配置文件 .env config.ini # 日志 -logs/ +logs/* +!logs/.gitkeep # IDE .vscode/ diff --git a/README.md b/README.md index 33c9ff4..04b112d 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,18 @@ pnpm dev uv run python apps/backend/scripts/create_admin.py ``` +### Docker Compose 部署 + +生产环境推荐使用 Docker Compose。主机只需要 Docker/Compose,不需要单独安装 Python、Node.js、pnpm 或 Chromium。 + +```bash +cp deploy/compose.env.example .env +# 编辑 .env,至少修改 SECRET_KEY、CORS_ORIGINS、FRONTEND_URL +docker compose up -d --build +``` + +默认访问地址: + ### 访问地址 - 前端: @@ -68,7 +80,7 @@ python main.py frontend-build 复制 `.env.example` 到 `.env` -nginx 与 systemd 的配置文件参考已给出,见 `.example` +Docker Compose 环境变量参考 `deploy/compose.env.example`。nginx 与 systemd 的传统部署配置文件参考已给出,见 `.example` ## 文档 diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..efe067c --- /dev/null +++ b/compose.yaml @@ -0,0 +1,49 @@ +services: + backend: + build: + context: . + dockerfile: deploy/docker/backend/Dockerfile + image: checkin-app-backend:local + container_name: checkin-backend + env_file: + - path: .env + required: false + environment: + DATABASE_URL: ${DATABASE_URL:-sqlite:////app/data/checkin.db} + LOG_FILE: ${LOG_FILE:-/app/logs/backend.log} + SESSION_DIR: ${SESSION_DIR:-/app/sessions} + BROWSER_EXECUTABLE_PATH: ${BROWSER_EXECUTABLE_PATH:-} + volumes: + - ./data:/app/data + - ./sessions:/app/sessions + - ./logs:/app/logs + restart: unless-stopped + healthcheck: + test: + [ + "CMD-SHELL", + "python -c \"import json, urllib.request; r=urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5); raise SystemExit(0 if json.load(r).get('status') == 'healthy' else 1)\"", + ] + interval: 30s + timeout: 10s + retries: 5 + start_period: 40s + + web: + build: + context: . + dockerfile: deploy/docker/web/Dockerfile + image: checkin-app-web:local + container_name: checkin-web + depends_on: + backend: + condition: service_healthy + ports: + - "${CHECKIN_WEB_PORT:-8080}:80" + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/health >/dev/null"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 10s diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/.gitkeep @@ -0,0 +1 @@ + diff --git a/deploy/compose.env.example b/deploy/compose.env.example new file mode 100644 index 0000000..eea40b5 --- /dev/null +++ b/deploy/compose.env.example @@ -0,0 +1,38 @@ +# Copy this file to .env before running Docker Compose: +# cp deploy/compose.env.example .env + +# Public web port exposed by the web service. +CHECKIN_WEB_PORT=8080 + +# ==================== Backend runtime paths ==================== +# Container-local defaults for the Compose deployment. +DATABASE_URL=sqlite:////app/data/checkin.db +LOG_FILE=/app/logs/backend.log +SESSION_DIR=/app/sessions + +# ==================== Security and public URLs ==================== +# Change this before production use. +SECRET_KEY=change-this-to-a-long-random-secret + +# Use the browser origins that will open the frontend. +# Public domain + optional intranet origins. +CORS_ORIGINS=https://checkin.example.com,http://192.168.1.10:8080,http://checkin.lan:8080 +FRONTEND_URL=https://checkin.example.com + +# ==================== Logging ==================== +LOG_LEVEL=INFO + +# ==================== Mail settings ==================== +SMTP_SERVER=smtp.example.com +SMTP_PORT=465 +SMTP_SENDER_EMAIL=your-email@example.com +SMTP_SENDER_PASSWORD=your-auth-code-here +SMTP_USE_SSL=True + +# ==================== Playwright browser settings ==================== +# Leave empty to use the Chromium installed in the backend image. +BROWSER_EXECUTABLE_PATH= + +# ==================== Scheduler settings ==================== +TOKEN_CHECK_INTERVAL_MINUTES=30 +SESSION_CLEANUP_INTERVAL_HOURS=24 diff --git a/deploy/docker/backend/Dockerfile b/deploy/docker/backend/Dockerfile new file mode 100644 index 0000000..7501734 --- /dev/null +++ b/deploy/docker/backend/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.14-slim AS runtime + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app/apps \ + PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \ + PATH="/app/.venv/bin:${PATH}" + +WORKDIR /app + +RUN python -m pip install --no-cache-dir --upgrade pip uv + +COPY pyproject.toml uv.lock README.md ./ +COPY main.py ./main.py +COPY apps ./apps + +RUN uv sync --frozen --no-dev --extra production \ + && playwright install --with-deps chromium \ + && mkdir -p /app/data /app/sessions /app/logs + +EXPOSE 8000 + +CMD ["python", "main.py", "backend", "--host", "0.0.0.0", "--port", "8000", "--no-reload", "--log-level", "info"] diff --git a/deploy/docker/web/Dockerfile b/deploy/docker/web/Dockerfile new file mode 100644 index 0000000..9d176ad --- /dev/null +++ b/deploy/docker/web/Dockerfile @@ -0,0 +1,18 @@ +FROM node:24-trixie-slim AS frontend-build + +WORKDIR /app/apps/frontend + +RUN corepack enable + +COPY apps/frontend/package.json apps/frontend/pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY apps/frontend/ ./ +RUN pnpm build + +FROM nginx:1.27-alpine AS runtime + +COPY deploy/docker/web/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=frontend-build /app/apps/frontend/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/deploy/docker/web/nginx.conf b/deploy/docker/web/nginx.conf new file mode 100644 index 0000000..6c20bd9 --- /dev/null +++ b/deploy/docker/web/nginx.conf @@ -0,0 +1,47 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + client_max_body_size 10M; + + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml; + + location /api/ { + proxy_pass http://backend:8000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_buffering off; + proxy_request_buffering off; + } + + location ~ ^/(docs|redoc|openapi.json|health)$ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } +} diff --git a/docs/deployment.md b/docs/deployment.md index 817bae9..81ba5b4 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,6 +1,161 @@ # 部署指南 -## 环境准备 +## 推荐方式:Docker Compose + +Docker Compose 是当前推荐的生产部署方式。主机只需要 Docker Engine 和 Docker Compose,Python、Node.js、pnpm、Playwright Chromium 都由镜像构建和容器运行时负责。 + +### 系统要求 + +- Linux 服务器,建议 Ubuntu 20.04+ / Debian 11+ / CentOS 7+ +- Docker Engine 24+ +- Docker Compose v2 +- 2GB+ RAM +- 可访问外网以构建镜像和下载依赖 + +### 首次部署 + +```bash +git clone +cd CheckInApp + +cp deploy/compose.env.example .env +``` + +编辑 `.env`,至少修改: + +- `SECRET_KEY`: 改成足够长的随机字符串。 +- `CORS_ORIGINS`: 改成浏览器实际访问的站点,例如 `https://checkin.example.com`。 +- `FRONTEND_URL`: 改成同一个公开站点,用于邮件里的链接。 +- `CHECKIN_WEB_PORT`: 默认 `8080`,按服务器端口规划调整。 +- SMTP 配置:需要邮件通知时填写真实 SMTP 参数。 + +启动: + +```bash +docker compose up -d --build +``` + +默认访问: + +- Web UI: `http://localhost:8080` +- API 健康检查: `http://localhost:8080/health` +- API 文档: `http://localhost:8080/docs` + +### 运行结构 + +Compose 启动两个服务: + +- `backend`: FastAPI、APScheduler、SQLAlchemy、Playwright Chromium。 +- `web`: nginx,负责前端静态资源、SPA fallback,以及代理 `/api`、`/docs`、`/redoc`、`/openapi.json`、`/health` 到 backend。 + +持久化目录: + +- `./data:/app/data`: SQLite 数据库,默认 `data/checkin.db`。 +- `./sessions:/app/sessions`: Playwright 登录会话状态。 +- `./logs:/app/logs`: 后端日志文件。 + +容器重建或镜像更新不会删除这些目录。 + +### 创建管理员 + +先让目标用户完成一次 QQ 扫码注册,然后在容器内运行管理员脚本: + +```bash +docker compose exec backend uv run python apps/backend/scripts/create_admin.py +``` + +脚本会提示输入要升级的用户别名。 + +### 常用运维命令 + +```bash +# 查看服务状态 +docker compose ps + +# 查看日志 +docker compose logs -f backend +docker compose logs -f web + +# 重启 +docker compose restart + +# 停止 +docker compose down +``` + +### 更新 + +```bash +git pull +docker compose up -d --build +``` + +不要使用 `docker compose down -v`,否则会删除 Compose 管理的卷。当前默认使用项目目录 bind mount,仍应避免误删 `data/`、`sessions/`、`logs/`。 + +### 备份与恢复 + +备份 SQLite 和运行时状态: + +```bash +mkdir -p backups +tar -czf backups/checkin-$(date +%Y%m%d-%H%M%S).tar.gz data sessions logs .env +``` + +恢复: + +```bash +docker compose down +tar -xzf backups/.tar.gz +docker compose up -d +``` + +### 回滚 + +```bash +git checkout +docker compose up -d --build +``` + +回滚时复用同一份 `.env`、`data/`、`sessions/`、`logs/`。如果未来版本引入数据库迁移,按对应版本说明先确认迁移是否可逆。 + +### Compose 故障排查 + +端口占用: + +```bash +sudo lsof -i :8080 +``` + +修改 `.env` 中的 `CHECKIN_WEB_PORT` 后重新启动: + +```bash +docker compose up -d +``` + +后端健康检查失败: + +```bash +docker compose logs backend +docker compose exec backend uv run python main.py backend --check +``` + +Playwright 问题: + +- Compose 部署不需要在主机安装 Chrome/Chromium。 +- 如果 QQ 登录或 payload 捕获失败,先查看 `docker compose logs backend` 和 `logs/backend.log`。 +- 重新构建镜像可刷新 Playwright 浏览器依赖:`docker compose build --no-cache backend`。 + +权限问题: + +```bash +mkdir -p data sessions logs +chmod -R u+rwX data sessions logs +docker compose restart backend +``` + +## 备选方式:传统部署 + +传统部署适合已经有主机级 Python、Node.js、pnpm、Chromium、nginx 和 systemd 管理经验的环境。 ### 系统要求 @@ -27,137 +182,127 @@ npm install -g pnpm curl -LsSf https://astral.sh/uv/install.sh | sh ``` -## 生产部署 - -### 方式一:传统部署 - -#### 1. 后端部署 +### 后端部署 ```bash -# 克隆项目 git clone cd CheckInApp -# 安装依赖 uv sync - -# 生产环境额外依赖 uv sync --extra production -# 配置环境变量 cp .env.example .env -vim .env # 修改环境变量 +vim .env + +uv run playwright install chromium +uv run python main.py backend --no-reload ``` -#### 2. 前端部署 +### 前端部署 ```bash cd apps/frontend - -# 安装依赖 pnpm install --frozen-lockfile - -# 构建生产版本 pnpm build - -# 输出在 dist/ 目录 ``` -**使用 Nginx 托管**: +生产静态资源输出在 `apps/frontend/dist/`。 -[示例文件](../deploy/nginx/checkin-app.conf.example) +nginx 示例文件:[deploy/nginx/checkin-app.conf.example](../deploy/nginx/checkin-app.conf.example) -#### 3. 使用 Systemd 管理 - -[示例文件](../deploy/systemd/checkin-app.service.example) - -### 方式二:Docker 部署(推荐) - -TODO(Maybe never) +systemd 示例文件:[deploy/systemd/checkin-app.service.example](../deploy/systemd/checkin-app.service.example) ## 配置优化 ### 生产环境变量 -[示例文件](../.env.example) +Compose 部署参考:[deploy/compose.env.example](../deploy/compose.env.example) + +传统部署参考:[.env.example](../.env.example) ### 数据库迁移到 PostgreSQL +当前 Compose baseline 使用 SQLite 单后端部署。需要 PostgreSQL 时,先按传统方式准备数据库并修改 `DATABASE_URL`: + ```bash -# 安装 PostgreSQL sudo apt install postgresql postgresql-contrib -# 创建数据库 sudo -u postgres createdb checkin sudo -u postgres createuser checkin_user sudo -u postgres psql -c "ALTER USER checkin_user WITH PASSWORD 'password';" -# 修改 .env DATABASE_URL=postgresql://checkin_user:password@localhost/checkin ``` ## 安全加固 -### 1. 防火墙配置 +### 防火墙配置 ```bash -sudo ufw allow 22/tcp # SSH -sudo ufw allow 80/tcp # HTTP -sudo ufw allow 443/tcp # HTTPS +sudo ufw allow 22/tcp +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp sudo ufw enable ``` -### 2. SSL 证书(Let's Encrypt) +如果直接暴露 Compose web 端口,也需要允许 `CHECKIN_WEB_PORT` 对应端口。 + +### HTTPS + +推荐在 Compose web 服务前放置外部反向代理或云厂商负载均衡来终止 TLS。传统 nginx 部署可以使用 Let's Encrypt: ```bash sudo apt install certbot python3-certbot-nginx - sudo certbot --nginx -d your-domain.com - -# 自动续期 sudo systemctl enable certbot.timer ``` -### 3. 限制访问 +### 访问限制 -- 修改 `.env` 中的 `CORS_ORIGINS` 为实际域名 -- 在 Nginx 中配置 rate limiting -- 使用 fail2ban 防止暴力破解 +- 生产环境必须修改默认 `SECRET_KEY`。 +- 将 `CORS_ORIGINS` 和 `FRONTEND_URL` 设置为实际公开域名。 +- 在外部反向代理中配置 rate limiting。 +- 使用 fail2ban 防止暴力破解。 ## 监控维护 -### 日志管理 +### 日志 + +Compose: + +```bash +docker compose logs -f backend +tail -f logs/backend.log +``` + +传统部署: ```bash -# 查看后端日志 tail -f logs/backend.log ``` ### 数据库备份 +SQLite: + ```bash -# SQLite 备份 cp data/checkin.db data/checkin.db.backup - -# PostgreSQL 备份 -pg_dump checkin > backup.sql - -# 定时备份(crontab) -0 2 * * * /path/to/backup.sh ``` -### 性能监控 +PostgreSQL: -使用工具: - -- Prometheus + Grafana -- New Relic -- Sentry(错误追踪) +```bash +pg_dump checkin > backup.sql +``` ## 扩展部署 ### 负载均衡 +当前应用包含内置调度器,默认 Compose baseline 只运行一个 backend 服务。多 backend 实例需要先设计调度器互斥或外部调度机制。 + +传统 nginx upstream 示例: + ```nginx upstream backend { server 127.0.0.1:8000; @@ -174,50 +319,10 @@ server { ### Redis 缓存 -```python -# 安装 redis +```bash uv sync --extra redis +``` -# 配置会话存储 +```env REDIS_URL=redis://localhost:6379/0 ``` - -## 故障排查 - -### 端口占用 - -```bash -sudo lsof -i :8000 -sudo kill -9 -``` - -### Playwright 问题 - -```bash -# 安装浏览器 -uv run playwright install chromium - -# 检查系统浏览器(可选) -chromium --version -google-chrome --version -``` - -### 权限问题 - -```bash -# 确保目录权限正确 -sudo chown -R www-data:www-data /path/to/CheckInApp -sudo chmod -R 755 /path/to/CheckInApp -``` - -## 回滚策略 - -```bash -# 保存当前版本 -git tag -a v2.0.0 -m "Production release" - -# 回滚到上一版本 -git checkout v1.9.0 -docker-compose down -docker-compose up -d --build -``` diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/logs/.gitkeep @@ -0,0 +1 @@ + diff --git a/sessions/.gitkeep b/sessions/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/sessions/.gitkeep @@ -0,0 +1 @@ +