# 生产部署说明 目标访问地址: ```text https://主站域名 # CADDY_DOMAIN,哈夫币系统 https://选号网域名 # CADDY_SHOW_DOMAIN(可选),hfb_show 静态选号站 https://monitor.主站域名 # Beszel 监控 ``` 生产部署使用 Caddy 作为公网入口,只暴露宿主机 `80`、`443` 端口。Caddy 会自动申请和续签 HTTPS 证书,并直接托管前端静态资源(主站 `dist` 与可选的选号网 `show-dist` 在构建 Caddy 镜像时打入);后端、MySQL、Redis 均通过 Docker 内网通信,图片对象存储使用阿里云 OSS。选号网与主站**共用同一 backend**,浏览器访问选号网域名下的 `/api/*` 由 Caddy 反代到本机 backend,**不会直连数据库**。 部署前请确认: - 主站域名 A 记录已解析到服务器公网 IP;启用选号网时,选号网域名也要解析到同一 IP。 - 服务器安全组或防火墙已放行 `80/tcp` 和 `443/tcp`。 - 服务器上没有其他服务占用 `80`、`443`。 - 国内服务器如需对外访问,请先确认域名备案和云厂商限制。 ## 1. 准备环境变量 ```bash cp backend/.env.prod.example backend/.env ``` 编辑 `backend/.env`,至少替换以下占位值: ```text CADDY_DOMAIN CADDY_EMAIL MYSQL_ROOT_PASSWORD MYSQL_PASSWORD MYSQL_DSN JWT_SECRET STORAGE_ACCESS_KEY_ID STORAGE_SECRET_ACCESS_KEY BACKEND_UID BACKEND_GID BACKUP_DIR BACKUP_RESTORE_DIR BACKUP_OSS_URI BACKUP_PASSPHRASE BACKUP_KMS_KEY_ID BACKUP_MYSQL_PASSWORD ``` `BACKEND_UID`、`BACKEND_GID` 填写服务器业务用户的数值 ID,例如: ```bash id -u yml id -g yml ``` 后端容器会使用该 UID/GID 运行。部署脚本由 root 执行时会自动把已有日志修正为该属主;非 root 执行且发现旧的 root 日志时,会给出需要执行的 `chown` 命令后停止。 ### 对象存储:阿里云 OSS 图片等业务文件存储在私有 OSS Bucket,`backend/.env` 的 `STORAGE_*` 直接指向 OSS: ```text STORAGE_ENDPOINT=https://oss-cn-hangzhou-internal.aliyuncs.com STORAGE_BUCKET=hfb-sys-assets STORAGE_ACCESS_KEY_ID=change-oss-access-key-id STORAGE_SECRET_ACCESS_KEY=change-oss-access-key-secret STORAGE_REGION=cn-hangzhou STORAGE_BUCKET_LOOKUP=dns ``` RAM 凭证只授予该 Bucket 的列举、读取、写入对象权限,不要使用阿里云主账号 AccessKey。杭州 ECS 必须使用内网 Endpoint,避免跨公网访问。 历史 MinIO 数据已通过 `scripts/migrate-minio-to-oss.sh` 迁移完成,生产编排不再包含 MinIO;旧 MinIO volume 观察期结束后可删除。 ### 数据库在线备份与时间点恢复 生产 MySQL 开启了 `ROW` 格式 binlog 和崩溃安全刷盘。备份期间业务持续读写:每日用 XtraBackup 做物理全量备份,每分钟轮转并归档已关闭的 binlog。备份对象先做客户端加密,再强制以 OSS KMS 加密上传;远端仅在 payload、checksum 和 manifest 都上传成功后写入 `complete.env`。 部署脚本会创建或更新 `BACKUP_MYSQL_USER`,该用户只有 XtraBackup 所需的全局备份权限和业务库只读权限,不开放远程 root。 备份 Bucket 必须独立于业务 Bucket,备份 RAM 身份仅授予该 Bucket 的列举、读写对象及指定 KMS Key 的加解密权限。推荐 Bucket 开启版本控制和不可变保留策略。 部署机需安装 `ossutil`、`openssl` 和 `flock`,并使用与 cron 相同的业务用户完成 `ossutil` 凭证配置;部署脚本会在启动服务前检查这些依赖。 ```bash # 每日全量热备,本地留存并上传 OSS 备份桶 ./scripts/backup-online.sh full # binlog 分钟级归档,配合全量实现最近保留窗口内的时间点恢复 ./scripts/archive-binlog.sh archive # 校验本地状态对应的远端完整对象 / 清理过期本地备份 ./scripts/archive-binlog.sh verify ./scripts/backup-online.sh status ./scripts/backup-online.sh prune ``` 推荐 cron(以部署用户运行,并把日志写到受限目录): ```cron * * * * * cd /srv/hfb_sys && ./scripts/archive-binlog.sh archive >> /data/backups/archive-binlog.log 2>&1 30 4 * * * cd /srv/hfb_sys && ./scripts/backup-online.sh full >> /data/backups/full-backup.log 2>&1 15 5 * * * cd /srv/hfb_sys && ./scripts/backup-online.sh prune >> /data/backups/prune.log 2>&1 10 5 * * 0 cd /srv/hfb_sys && ./scripts/archive-binlog.sh verify >> /data/backups/archive-verify.log 2>&1 ``` OSS 生命周期建议:`mysql/full/daily/` 30 天、`weekly/` 60 天、`monthly/` 365 天、`mysql/binlog/` 至少 45 天。时间点恢复窗口受最早全量备份和 binlog 保留期共同限制。 每周必须在隔离目录做一次恢复演练,绝不向生产容器回放: ```bash # 从 status 输出选取一个带 complete.env 的全量备份 URI。 ./scripts/restore-online.sh prepare oss://hfb-backup/mysql/full/daily///complete.env ./scripts/restore-online.sh fetch-all /data/restore// ./scripts/restore-online.sh start /data/restore// ./scripts/restore-online.sh replay /data/restore// hfb-restore- '2026-08-16 20:00:00' ``` 恢复容器使用 `--network none`,只可通过 `docker exec` 检查。演练结束后手动删除名为 `hfb-restore-*` 的恢复容器和 `/data/restore` 对应目录;生产容器不在恢复工具的可选目标中。 ### 同机部署选号网(hfb_show,可选) 在 `backend/.env` 增加(域名不要带 `https://`): ```text CADDY_SHOW_DOMAIN=show.example.com HFB_SHOW_DIR=../hfb_show ``` 要求: 1. 服务器上 `hfb_show` 与 `hfb_sys` 同级目录(或 `HFB_SHOW_DIR` 指向真实路径)。 2. 部署机有 Docker 即可;**不必安装 Node/npm**(无 npm 时脚本用 `node:24-alpine` 容器构建选号网)。 3. 选号网域名 DNS 已指向本机。 一键脚本会自动: - `npm ci && npm run build` 选号网; - 将 `dist` 同步到 `deploy/caddy/show-dist`; - 生成 Caddy 站点配置 `deploy/caddy/conf.d/show.caddy`; - 构建 Caddy 镜像时打入主站 + 选号网静态资源; - 为选号网域名申请 HTTPS,并把 `/api/*` 反代到 `backend:8080`。 不配置 `CADDY_SHOW_DOMAIN` 则行为与原来一致,仅部署主站。 如果正式对外运营,还需要配置真实短信和实名服务,避免继续使用测试配置。 ## 2. 一键部署 ```bash ./scripts/deploy-prod.sh ``` 脚本会自动完成: - 检查 `backend/.env` 是否仍指向 `127.0.0.1`、`localhost`、占位值或已下线的 MinIO。 - 构建并启动生产容器。 - 使用配置的非 root UID/GID 运行后端,并检查日志目录属主。 - 通过 Caddy 自动申请或续签 HTTPS 证书。 - 等待 MySQL、Redis 就绪。 - 按顺序执行尚未应用的数据库迁移。 - 重启后端并检查 `https://你的域名/api/health`。 如需部署完成后直接跟随查看后端日志: ```bash ./scripts/deploy-prod.sh --logs ``` 后端运行日志会写入宿主机: ```text backend/logs/app-YYYY-MM-DD.log ``` 测试阶段需要清空并重建数据库时: ```bash ./scripts/deploy-prod.sh --reset-db ``` ## 3. 手动启动服务 ```bash set -a source backend/.env set +a docker compose -f deploy/docker-compose.prod.yml up -d --build ``` 手动执行 `docker compose` 时需要先导出 `CADDY_DOMAIN` 和 `CADDY_EMAIL`,否则 Caddy 无法读取证书域名配置。优先推荐使用一键部署脚本,它会自动从 `backend/.env` 导出这两个变量。 ## 4. 手动执行数据库迁移 通常直接使用一键部署脚本即可。确实需要手动迁移时,使用后端镜像内置的 goose: ```bash MYSQL_DSN="$(awk -F= '$1=="MYSQL_DSN"{sub(/^[^=]*=/,""); print; exit}' backend/.env)" case "$MYSQL_DSN" in *multiStatements=*) GOOSE_DSN="$MYSQL_DSN" ;; *\?*) GOOSE_DSN="${MYSQL_DSN}&multiStatements=true" ;; *) GOOSE_DSN="${MYSQL_DSN}?multiStatements=true" ;; esac docker compose -f deploy/docker-compose.prod.yml run --rm --no-deps backend \ /app/goose -dir /app/migrations mysql "$GOOSE_DSN" up ``` ## 5. 验证 ```bash curl https://主站域名/health curl https://主站域名/api/health # 若启用了选号网: curl -I https://选号网域名/ curl https://选号网域名/api/health curl "https://选号网域名/api/listings?page=1&page_size=1" ``` 浏览器访问: ```text https://主站域名 https://主站域名/你配置的后台入口/login https://选号网域名 # 选号网首页 ``` Caddy 证书和 ACME 账号数据保存在 Docker 卷 `caddy_data`、`caddy_config` 中。不要随意删除这两个卷,否则 Caddy 会重新申请证书,频繁重建可能触发证书签发频率限制。