feat(deploy): add Ubuntu 26.04 production package
This commit is contained in:
+11
-3
@@ -1,9 +1,17 @@
|
|||||||
.git
|
.git
|
||||||
.github
|
.github
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
node_modules
|
||||||
**/node_modules
|
**/node_modules
|
||||||
|
dist
|
||||||
**/dist
|
**/dist
|
||||||
coverage
|
coverage
|
||||||
.env
|
**/coverage
|
||||||
*.log
|
*.log
|
||||||
docs
|
apps/miniprogram/.miniprogram
|
||||||
*.png
|
apps/miniprogram/miniprogram_npm
|
||||||
|
mysql-data
|
||||||
|
minio-data
|
||||||
|
redis-data
|
||||||
|
docs/ui_reference.png
|
||||||
|
|||||||
@@ -35,3 +35,9 @@ pnpm build
|
|||||||
集成测试只允许连接数据库名为 `lehuo_test` 的 MySQL。可先运行 `docker compose --profile test up -d mysql-test`,再设置 `TEST_DATABASE_URL` 后执行测试。
|
集成测试只允许连接数据库名为 `lehuo_test` 的 MySQL。可先运行 `docker compose --profile test up -d mysql-test`,再设置 `TEST_DATABASE_URL` 后执行测试。
|
||||||
|
|
||||||
项目约束、当前阶段和决策分别见 `docs/01_PROJECT_CONSTITUTION.md`、`docs/10_TASK_STATE.md`、`docs/11_DECISIONS.md`。
|
项目约束、当前阶段和决策分别见 `docs/01_PROJECT_CONSTITUTION.md`、`docs/10_TASK_STATE.md`、`docs/11_DECISIONS.md`。
|
||||||
|
|
||||||
|
## Ubuntu 26.04 云服务器部署
|
||||||
|
|
||||||
|
Ubuntu 26.04 LTS 单机部署包位于 `infra/ubuntu/`,提供 Docker 官方源安装、生产环境密钥生成、双域名 TLS、私有 MySQL/Redis/MinIO、API/管理后台镜像、migration、备份恢复、冒烟和回滚脚本。完整步骤见 `infra/ubuntu/README.md`。
|
||||||
|
|
||||||
|
该部署包不替代 staging、UAT、微信合法域名/真实 AppID、隐私法务、告警接收与异地备份验收;外部项未完成前不得宣称已生产上线。
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
FROM node:24.13.0-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
RUN corepack enable
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
|
||||||
|
COPY apps/admin-web/package.json apps/admin-web/package.json
|
||||||
|
COPY packages/contracts/package.json packages/contracts/package.json
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
COPY apps/admin-web apps/admin-web
|
||||||
|
COPY packages/contracts packages/contracts
|
||||||
|
ARG VITE_API_BASE_URL
|
||||||
|
RUN test -n "$VITE_API_BASE_URL"
|
||||||
|
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
|
||||||
|
RUN pnpm --filter @lehuo/admin-web build
|
||||||
|
|
||||||
|
FROM nginx:1.28.0-alpine AS runtime
|
||||||
|
COPY --from=build /app/apps/admin-web/dist /usr/share/nginx/html
|
||||||
|
COPY infra/ubuntu/nginx/default.conf.template /etc/nginx/templates/default.conf.template
|
||||||
|
RUN rm -f /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80 443
|
||||||
+2
-1
@@ -12,12 +12,13 @@ RUN DATABASE_URL=mysql://build:build@127.0.0.1:3306/build pnpm --filter @lehuo/a
|
|||||||
FROM node:24.13.0-alpine AS runtime
|
FROM node:24.13.0-alpine AS runtime
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN corepack enable
|
|
||||||
COPY --from=build /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./
|
COPY --from=build /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./
|
||||||
COPY --from=build /app/node_modules ./node_modules
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
COPY --from=build /app/apps/api/package.json ./apps/api/package.json
|
COPY --from=build /app/apps/api/package.json ./apps/api/package.json
|
||||||
COPY --from=build /app/apps/api/node_modules ./apps/api/node_modules
|
COPY --from=build /app/apps/api/node_modules ./apps/api/node_modules
|
||||||
COPY --from=build /app/apps/api/dist ./apps/api/dist
|
COPY --from=build /app/apps/api/dist ./apps/api/dist
|
||||||
|
COPY --from=build /app/apps/api/prisma ./apps/api/prisma
|
||||||
|
COPY --from=build /app/apps/api/prisma.config.ts ./apps/api/prisma.config.ts
|
||||||
COPY --from=build /app/packages/contracts/package.json ./packages/contracts/package.json
|
COPY --from=build /app/packages/contracts/package.json ./packages/contracts/package.json
|
||||||
COPY --from=build /app/packages/contracts/dist ./packages/contracts/dist
|
COPY --from=build /app/packages/contracts/dist ./packages/contracts/dist
|
||||||
USER node
|
USER node
|
||||||
|
|||||||
@@ -6,6 +6,20 @@
|
|||||||
|
|
||||||
2026-08-15
|
2026-08-15
|
||||||
|
|
||||||
|
任务:生成 Ubuntu 26.04 LTS 云服务器部署版本。
|
||||||
|
|
||||||
|
完成:新增独立 `infra/ubuntu` 生产部署包,采用 Nginx/管理后台、API、MySQL 8.4、Redis 7.4、私有 MinIO 的单机 Docker Compose 基线,仅发布 80/443。提供 Ubuntu 26.04 Docker 官方仓库安装、生产环境文件生成与 600 权限保护、TLS/域名/磁盘/Compose 预检、不可变镜像构建、数据服务初始化、migration、首次管理员初始化、HTTPS/readiness 冒烟、应用镜像回滚、MySQL 校验和备份、受控恢复和 systemd 每日备份定时器。API 生产镜像包含 Prisma migration 与管理员初始化产物且运行时不依赖 Corepack 下载;管理后台使用独立生产镜像和 Nginx HTTPS 网关。同步补充根目录、国内部署、发布清单与生产就绪文档,新增 D-024。
|
||||||
|
|
||||||
|
范围:未改变业务架构、认证、数据库 schema、API 契约、权限、状态机或 V1.0 产品范围;没有把单机基线扩展为 Kubernetes、微服务或多租户。部署包是可复现交付,不代表真实生产发布条件已经满足。
|
||||||
|
|
||||||
|
验证:全仓格式检查、Lint、类型检查、28 个单元测试、38 个真实 MySQL 集成测试和生产构建全部通过;测试库 12 个 migration 无待执行项,小程序 28 个页面完整性与运行时依赖门禁通过。Ubuntu Compose 配置、全部 Bash 脚本语法、22 个控制器权限扫描和 npm 官方 High 级依赖审计通过;MySQL、Redis、MinIO、MinIO Client 的固定镜像标签均可拉取;API 与管理后台生产镜像构建通过,最终镜像内 Prisma 7.9.1 可直接执行,管理端静态资源完整且双域名 HTTPS Nginx 配置通过真实 `nginx -t`。
|
||||||
|
|
||||||
|
遗留:部署前仍需用户提供两个已备案合法 HTTPS 域名及 TLS 证书、微信 AppID/AppSecret 与合法域名配置,并在真实服务器确认防火墙、安全组、DNS、告警接收方和异地加密备份目标;随后执行 staging、恢复演练、UAT 和生产发布审批。当前未使用示例域名或测试密钥冒充生产配置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
2026-08-15
|
||||||
|
|
||||||
任务:发布前安全与环境配置加固并重新同步仓库。
|
任务:发布前安全与环境配置加固并重新同步仓库。
|
||||||
|
|
||||||
完成:在既有未提交安全改动基础上完成复核和修正。审计增加原始会话 token 防御性剥离;后台写请求使用精确 Origin 或无 Origin 时的完整 Referer origin 校验,不允许前缀或通配来源;管理 Cookie 仅在 Secure 模式附加 Partitioned,避免本地 HTTP 丢失登录态;私有上传签名由 600 秒缩短到 120 秒;台账导出审计补充关键词条件;数据库启动只记录已完成 migration 数量与最新名称,不把运行时日志冒充待部署校验。小程序开发版保留 localhost 回环调试,体验版/正式版拒绝回环、`0.0.0.0` 和非 HTTPS API,防止不安全发布配置。
|
完成:在既有未提交安全改动基础上完成复核和修正。审计增加原始会话 token 防御性剥离;后台写请求使用精确 Origin 或无 Origin 时的完整 Referer origin 校验,不允许前缀或通配来源;管理 Cookie 仅在 Secure 模式附加 Partitioned,避免本地 HTTP 丢失登录态;私有上传签名由 600 秒缩短到 120 秒;台账导出审计补充关键词条件;数据库启动只记录已完成 migration 数量与最新名称,不把运行时日志冒充待部署校验。小程序开发版保留 localhost 回环调试,体验版/正式版拒绝回环、`0.0.0.0` 和非 HTTPS API,防止不安全发布配置。
|
||||||
|
|||||||
@@ -285,6 +285,20 @@ Excel 在管理端浏览器本地读取和生成,API 只接收固定结构、
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## D-024 Ubuntu 26.04 初期生产采用可迁移单机 Compose 基线
|
||||||
|
|
||||||
|
状态:ACCEPTED
|
||||||
|
|
||||||
|
决定:为用户已准备的 Ubuntu 26.04 LTS 云服务器提供独立 Linux 部署包。初期拓扑在单机 Docker Compose 内运行 Nginx/管理后台、模块化单体 API、MySQL 8.4、Redis 7.4 和私有 MinIO,仅向宿主机发布 80/443;管理后台/API 与签名文件服务使用两个不同的合法 HTTPS 域名。数据、对象和缓存使用独立持久卷,生产环境文件以 600 权限保存在 `/etc/lehuo`,数据库每日备份并生成 SHA-256。
|
||||||
|
|
||||||
|
API 与管理后台镜像使用批准的 Git commit 作为不可变标签并可本机构建;固定执行预检、构建、迁移前备份、migration、部署、HTTPS/readiness 冒烟和观察。应用回滚只允许切换到本机存在的上一不可变镜像,不自动回滚数据库。自建 MinIO 继续通过既有 S3 Adapter 接入,未来可替换为腾讯 COS 或其他 S3 兼容私有存储,不改变业务服务。
|
||||||
|
|
||||||
|
该基线是可部署交付,不代表生产发布条件已满足。真实域名/备案/TLS、微信配置、staging、UAT、告警接收、异地加密备份、恢复演练和发布审批仍按 D-016 阻断;单机部署不扩展为 Kubernetes、微服务或多租户架构。
|
||||||
|
|
||||||
|
原因:在不改变批准技术栈和 V1 边界的前提下,为现有 Ubuntu 云服务器提供可复现、可备份、可回滚的最低复杂度部署路径,并保持未来迁移到托管 MySQL/Redis/COS 的能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 待确认建议
|
## 待确认建议
|
||||||
|
|
||||||
Codex 若有会改变已批准架构的建议,只能写在这里,不得直接实施:
|
Codex 若有会改变已批准架构的建议,只能写在这里,不得直接实施:
|
||||||
|
|||||||
@@ -153,3 +153,15 @@ CI 自动化测试。
|
|||||||
- 审核材料
|
- 审核材料
|
||||||
|
|
||||||
微信平台规则可能变化,因此上线前必须重新查官方文档,不得只依赖本文件。
|
微信平台规则可能变化,因此上线前必须重新查官方文档,不得只依赖本文件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Ubuntu 26.04 单机部署基线
|
||||||
|
|
||||||
|
- Ubuntu 26.04 LTS 使用 Docker 官方 apt 仓库安装 Docker Engine 与 Compose plugin,不使用生产环境不推荐的 convenience script。
|
||||||
|
- 初期单机拓扑采用 Docker Compose:Nginx/管理后台、API、MySQL 8.4、Redis 7.4 和私有 MinIO;仅发布 80/443,其他服务不映射宿主机端口。
|
||||||
|
- 管理后台/API 与对象存储使用两个不同的合法 HTTPS 域名;对象存储保持私有 Bucket、版本控制、精确管理端 CORS 和 120 秒签名 URL。
|
||||||
|
- API 和管理后台镜像以批准的 Git commit 作为不可变标签,本机构建不使用 `latest`;发布顺序仍为预检、构建、备份、migration、部署、冒烟和观察。
|
||||||
|
- 环境文件固定保存到 `/etc/lehuo/lehuo.env` 且权限为 600;TLS、备份和发布状态分别保存在 `/opt/lehuo/shared`、`/opt/lehuo/backups` 和 `/opt/lehuo/releases`。
|
||||||
|
- 每日备份由 systemd timer 执行并生成 SHA-256;本机 30 天保留不替代异地加密备份和季度独立库恢复演练。
|
||||||
|
- 完整资产与执行命令见 `infra/ubuntu/README.md`。该基线只提供可部署版本,不代表真实 staging、UAT、微信联调或 production approval 已完成。
|
||||||
|
|||||||
@@ -50,8 +50,13 @@
|
|||||||
- [x] health
|
- [x] health
|
||||||
- [x] monitor
|
- [x] monitor
|
||||||
- [x] alert
|
- [x] alert
|
||||||
|
- [ ] Ubuntu 26.04 host preflight pass
|
||||||
|
- [ ] only 80/443 exposed by host/cloud firewall
|
||||||
|
- [ ] TLS certificate covers admin/API and file domains
|
||||||
- [ ] backup
|
- [ ] backup
|
||||||
- [x] restore procedure
|
- [x] restore procedure
|
||||||
|
- [ ] offsite encrypted backup configured
|
||||||
|
- [ ] systemd backup timer verified
|
||||||
- [ ] rollback image available
|
- [ ] rollback image available
|
||||||
|
|
||||||
## UAT
|
## UAT
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ Phase 0–7 的可自动化开发与本地生产工程验证已完成。当前
|
|||||||
|
|
||||||
当前证据:统计/导出、health/metrics、权限扫描、官方依赖审计、密钥扫描、CodeQL 工作流、备份/恢复脚本、Nginx、immutable image Compose、预检/冒烟和告警规则已完成;本地恢复演练源库与恢复库均为 42 张表。真实 staging、UAT、告警接收和生产发布尚未执行,因此本退出条件仍未满足。
|
当前证据:统计/导出、health/metrics、权限扫描、官方依赖审计、密钥扫描、CodeQL 工作流、备份/恢复脚本、Nginx、immutable image Compose、预检/冒烟和告警规则已完成;本地恢复演练源库与恢复库均为 42 张表。真实 staging、UAT、告警接收和生产发布尚未执行,因此本退出条件仍未满足。
|
||||||
|
|
||||||
|
2026-08-15 补充 Ubuntu 26.04 单机部署包:增加 Docker 官方源安装、生产 Compose、管理后台/API 镜像、双域名 TLS、私有 MySQL/Redis/MinIO、Linux 预检、migration、备份恢复、初始管理员、systemd 定时备份、HTTPS 冒烟与应用回滚。该交付仅关闭“Ubuntu 可执行部署资产”差距,不改变真实域名、微信、staging、UAT、告警接收、异地备份和发布审批阻断项。
|
||||||
|
|
||||||
## 3. 需要人工或外部环境配合
|
## 3. 需要人工或外部环境配合
|
||||||
|
|
||||||
以下内容不能用测试值替代正式验收:
|
以下内容不能用测试值替代正式验收:
|
||||||
|
|||||||
@@ -18,3 +18,7 @@
|
|||||||
6. 异常时将 `IMAGE_TAG` 恢复到上一批准版本重新部署。若 migration 不向后兼容,停止发布并按已验证的数据库恢复步骤执行。
|
6. 异常时将 `IMAGE_TAG` 恢复到上一批准版本重新部署。若 migration 不向后兼容,停止发布并按已验证的数据库恢复步骤执行。
|
||||||
|
|
||||||
备份默认每日一次,保留 30 天;必须每季度在独立数据库做恢复演练并记录表数量、关键行数和耗时。脚本会校验 SHA-256,恢复目标数据库名必须显式提供。
|
备份默认每日一次,保留 30 天;必须每季度在独立数据库做恢复演练并记录表数量、关键行数和耗时。脚本会校验 SHA-256,恢复目标数据库名必须显式提供。
|
||||||
|
|
||||||
|
## Ubuntu 26.04
|
||||||
|
|
||||||
|
原有 `.ps1` 工具保留用于 Windows 运维环境。Ubuntu 26.04 云服务器应使用 `infra/ubuntu/` 中的 Compose、Bash 脚本与 systemd timer;完整步骤见 `infra/ubuntu/README.md`。两套工具遵循相同的不可变标签、migration、备份、冒烟、观察和应用回滚顺序。
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# Ubuntu 26.04 LTS 单机部署包
|
||||||
|
|
||||||
|
本目录用于把乐活公益平台部署到一台已安装 Ubuntu 26.04 LTS 的云服务器。Docker 官方已将 Ubuntu 26.04(Resolute)列为 Docker Engine 支持系统:<https://docs.docker.com/engine/install/ubuntu/>。
|
||||||
|
|
||||||
|
该部署包适合初期单机 staging/production:
|
||||||
|
|
||||||
|
- 管理后台与 API:`https://PUBLIC_HOST`
|
||||||
|
- 私有对象存储签名访问:`https://FILES_HOST`
|
||||||
|
- 仅向宿主机发布 `80/443`;MySQL、Redis、MinIO 与 API 不发布主机端口
|
||||||
|
- MySQL 8.4、Redis 7.4、MinIO、NestJS API、Vue 管理后台/Nginx 均使用 Docker Compose
|
||||||
|
- 数据、对象和 Redis 使用独立持久卷
|
||||||
|
- 自动 migration、上线前备份、HTTPS 冒烟、不可变 Git 提交标签和应用回滚
|
||||||
|
- 每日 MySQL 备份、SHA-256 校验、30 天本机保留;异地加密同步仍须由运维配置
|
||||||
|
|
||||||
|
这不是“已完成生产上线”的证明。真实域名/备案、TLS、微信配置、隐私材料、staging、UAT、告警接收人和异地备份仍是发布阻断项。
|
||||||
|
|
||||||
|
## 1. 云服务器准备
|
||||||
|
|
||||||
|
建议最低资源:4 vCPU、8 GiB 内存、100 GiB SSD;测试环境可降低,但生产容量必须以压测结果为准。云安全组仅开放:
|
||||||
|
|
||||||
|
- SSH 管理端口(优先限制管理 IP,端口以服务器实际配置为准)
|
||||||
|
- TCP 80(TLS 跳转/证书验证)
|
||||||
|
- TCP 443(管理后台、API、小程序请求与签名文件上传)
|
||||||
|
|
||||||
|
不要开放 3000、3306、6379、9000 或 9001。Docker 发布端口可能绕过 UFW 规则,因此本 Compose 只发布 80/443;仍需用云安全组做外层限制。
|
||||||
|
|
||||||
|
为以下两个不同域名配置 A/AAAA 记录到服务器,并准备覆盖二者的证书(双域名 SAN 或通配证书):
|
||||||
|
|
||||||
|
- `admin.example.com`:管理后台和 API
|
||||||
|
- `files.example.com`:私有对象存储签名上传/下载
|
||||||
|
|
||||||
|
## 2. 放置代码与安装 Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo install -d -m 0755 /opt/lehuo
|
||||||
|
sudo git clone --branch main https://gitee.com/climber-ge/wechatapp.git /opt/lehuo/app
|
||||||
|
cd /opt/lehuo/app
|
||||||
|
sudo bash ./infra/ubuntu/scripts/install-docker.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
安装脚本严格检查 Ubuntu 26.04,使用 Docker 官方 apt 仓库,并且不会自动卸载已有容器运行时,也不会自动把普通用户加入等同 root 权限的 `docker` 用户组。
|
||||||
|
|
||||||
|
## 3. 生成生产环境文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/lehuo/app
|
||||||
|
sudo bash ./infra/ubuntu/scripts/generate-env.sh \
|
||||||
|
--public-host admin.example.com \
|
||||||
|
--files-host files.example.com
|
||||||
|
sudo install -m 0600 /path/to/fullchain.pem /opt/lehuo/shared/tls/fullchain.pem
|
||||||
|
sudo install -m 0600 /path/to/privkey.pem /opt/lehuo/shared/tls/privkey.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
生成结果位于 `/etc/lehuo/lehuo.env`,权限为 `600`,包含随机数据库密码、Redis 密码、对象存储密钥、会话 pepper、审计 HMAC key 和 32 字节 Base64 字段加密密钥。不要提交、复制到聊天、工单或普通日志。
|
||||||
|
|
||||||
|
首次部署时微信参数可以保持空值且 `NOTIFICATION_WORKER_ENABLED=false`。取得有效 AppID/AppSecret 和全部模板后再填入真实值并重新部署,禁止使用测试值冒充成功。
|
||||||
|
|
||||||
|
如果不用自带 MinIO 而使用腾讯 COS 或其他 S3 兼容私有对象存储,应单独评审并修改 Compose 服务,同时保持 Adapter、私有 Bucket、短期签名 URL、最小权限密钥与 CORS 边界。
|
||||||
|
|
||||||
|
## 4. 预检与首次部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/lehuo/app
|
||||||
|
sudo bash ./infra/ubuntu/scripts/preflight.sh /etc/lehuo/lehuo.env
|
||||||
|
sudo bash ./infra/ubuntu/scripts/deploy.sh /etc/lehuo/lehuo.env
|
||||||
|
sudo bash ./infra/ubuntu/scripts/bootstrap-admin.sh /etc/lehuo/lehuo.env
|
||||||
|
```
|
||||||
|
|
||||||
|
`deploy.sh` 固定执行:预检 → 以 Git 提交号构建不可变镜像 → 启动数据服务 → 初始化私有 Bucket/CORS/版本控制 →(已有版本时)迁移前备份 → Prisma migration → 部署 API/Nginx → HTTPS/ready/对象存储冒烟。
|
||||||
|
|
||||||
|
初始管理员脚本从终端隐藏读取密码,不写入环境文件或 shell history;数据库已有后台凭据时服务端会拒绝重复初始化。
|
||||||
|
|
||||||
|
## 5. 小程序生产地址
|
||||||
|
|
||||||
|
在提审的小程序代码中把 `apps/miniprogram/miniprogram/app.ts` 的 `apiBaseUrl` 改为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://admin.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
并在微信公众平台配置 request 合法域名 `https://admin.example.com`,uploadFile/downloadFile 合法域名 `https://files.example.com`。体验版/正式版会拒绝 localhost、`0.0.0.0` 和非 HTTPS 地址。
|
||||||
|
|
||||||
|
## 6. 备份与恢复演练
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash ./infra/ubuntu/scripts/install-backup-timer.sh
|
||||||
|
sudo systemctl start lehuo-backup.service
|
||||||
|
sudo journalctl -u lehuo-backup.service --since today
|
||||||
|
|
||||||
|
# 恢复到隔离数据库做演练,不覆盖生产库
|
||||||
|
sudo bash ./infra/ubuntu/scripts/restore.sh \
|
||||||
|
/opt/lehuo/backups/lehuo-YYYYMMDDTHHMMSSZ.sql.gz \
|
||||||
|
lehuo_restore_test --confirm lehuo_restore_test
|
||||||
|
```
|
||||||
|
|
||||||
|
备份文件与 `.sha256` 必须同步到异地加密存储。每季度至少在隔离数据库完成一次恢复演练,核对表数量、关键事实行数、耗时并留存记录。
|
||||||
|
|
||||||
|
## 7. 升级与回滚
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/lehuo/app
|
||||||
|
sudo git fetch --all --prune
|
||||||
|
sudo git switch main
|
||||||
|
sudo git pull --ff-only
|
||||||
|
|
||||||
|
# 把 IMAGE_TAG 更新为本次批准的完整/短 Git commit;禁止 latest
|
||||||
|
sudo editor /etc/lehuo/lehuo.env
|
||||||
|
sudo bash ./infra/ubuntu/scripts/deploy.sh /etc/lehuo/lehuo.env
|
||||||
|
```
|
||||||
|
|
||||||
|
应用异常且 migration 向后兼容时:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash ./infra/ubuntu/scripts/rollback.sh /etc/lehuo/lehuo.env
|
||||||
|
```
|
||||||
|
|
||||||
|
回滚脚本只切换本机仍存在的上一版 API/管理后台镜像,不回滚数据库。若 migration 不向后兼容,必须停止发布并按已审批的数据库恢复方案处理,不能盲目执行应用回滚。
|
||||||
|
|
||||||
|
## 8. 日常检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/lehuo/app
|
||||||
|
sudo docker compose --project-directory infra/ubuntu \
|
||||||
|
--env-file /etc/lehuo/lehuo.env -f infra/ubuntu/compose.yaml ps
|
||||||
|
curl -fsS https://admin.example.com/health/live
|
||||||
|
curl -fsS https://admin.example.com/health/ready
|
||||||
|
sudo docker system df
|
||||||
|
sudo systemctl status lehuo-backup.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
日志中禁止输出 token、完整手机号、证件、详细地址、儿童信息或服务描述。容器日志轮转、CPU/内存/磁盘/API 5xx/P95、数据库连接、通知死信和备份缺失告警需接入实际有人值守的监控渠道。
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
name: lehuo-production
|
||||||
|
|
||||||
|
x-api-environment: &api-environment
|
||||||
|
APP_ENV: production
|
||||||
|
API_PORT: '3000'
|
||||||
|
DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
|
||||||
|
REDIS_URL: ${REDIS_URL:?REDIS_URL is required}
|
||||||
|
SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required}
|
||||||
|
SESSION_TOKEN_PEPPER: ${SESSION_TOKEN_PEPPER:?SESSION_TOKEN_PEPPER is required}
|
||||||
|
AUDIT_IP_HASH_KEY: ${AUDIT_IP_HASH_KEY:?AUDIT_IP_HASH_KEY is required}
|
||||||
|
FIELD_ENCRYPTION_KEY: ${FIELD_ENCRYPTION_KEY:?FIELD_ENCRYPTION_KEY is required}
|
||||||
|
ADMIN_WEB_ORIGIN: ${ADMIN_WEB_ORIGIN:?ADMIN_WEB_ORIGIN is required}
|
||||||
|
ADMIN_COOKIE_SECURE: 'true'
|
||||||
|
AUTH_SESSION_TTL_SECONDS: ${AUTH_SESSION_TTL_SECONDS:-28800}
|
||||||
|
WECHAT_SESSION_TTL_SECONDS: ${WECHAT_SESSION_TTL_SECONDS:-7200}
|
||||||
|
WECHAT_APP_ID: ${WECHAT_APP_ID:-}
|
||||||
|
WECHAT_APP_SECRET: ${WECHAT_APP_SECRET:-}
|
||||||
|
WECHAT_TEMPLATE_APPLICATION_SUBMITTED: ${WECHAT_TEMPLATE_APPLICATION_SUBMITTED:-}
|
||||||
|
WECHAT_TEMPLATE_APPLICATION_ACCEPTED: ${WECHAT_TEMPLATE_APPLICATION_ACCEPTED:-}
|
||||||
|
WECHAT_TEMPLATE_SERVICE_ARRANGED: ${WECHAT_TEMPLATE_SERVICE_ARRANGED:-}
|
||||||
|
WECHAT_TEMPLATE_SERVICE_COMPLETED: ${WECHAT_TEMPLATE_SERVICE_COMPLETED:-}
|
||||||
|
WECHAT_TEMPLATE_FOLLOW_UP_PENDING: ${WECHAT_TEMPLATE_FOLLOW_UP_PENDING:-}
|
||||||
|
WECHAT_TEMPLATE_ACTIVITY_REGISTRATION_RESULT: ${WECHAT_TEMPLATE_ACTIVITY_REGISTRATION_RESULT:-}
|
||||||
|
NOTIFICATION_WORKER_ENABLED: ${NOTIFICATION_WORKER_ENABLED:-false}
|
||||||
|
OBJECT_STORAGE_ENDPOINT: ${OBJECT_STORAGE_ENDPOINT:?OBJECT_STORAGE_ENDPOINT is required}
|
||||||
|
OBJECT_STORAGE_REGION: ${OBJECT_STORAGE_REGION:-auto}
|
||||||
|
OBJECT_STORAGE_BUCKET: ${OBJECT_STORAGE_BUCKET:?OBJECT_STORAGE_BUCKET is required}
|
||||||
|
OBJECT_STORAGE_ACCESS_KEY: ${OBJECT_STORAGE_ACCESS_KEY:?OBJECT_STORAGE_ACCESS_KEY is required}
|
||||||
|
OBJECT_STORAGE_SECRET_KEY: ${OBJECT_STORAGE_SECRET_KEY:?OBJECT_STORAGE_SECRET_KEY is required}
|
||||||
|
OBJECT_STORAGE_FORCE_PATH_STYLE: 'true'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.4.8
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --character-set-server=utf8mb4
|
||||||
|
- --collation-server=utf8mb4_0900_ai_ci
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE:?MYSQL_DATABASE is required}
|
||||||
|
MYSQL_USER: ${MYSQL_USER:?MYSQL_USER is required}
|
||||||
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD is required}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD is required}
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- mysql-data:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
- MYSQL_PWD=$$MYSQL_ROOT_PASSWORD mysqladmin ping -h 127.0.0.1 -u root --silent
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 40s
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7.4-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- redis-server
|
||||||
|
- --appendonly
|
||||||
|
- 'yes'
|
||||||
|
- --requirepass
|
||||||
|
- ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
healthcheck:
|
||||||
|
test: [CMD-SHELL, 'redis-cli -a "$$REDIS_PASSWORD" ping | grep -q PONG']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 8
|
||||||
|
environment:
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
|
||||||
|
restart: unless-stopped
|
||||||
|
command: server /data
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}
|
||||||
|
MINIO_BROWSER: 'off'
|
||||||
|
MINIO_SERVER_URL: https://${FILES_HOST:?FILES_HOST is required}
|
||||||
|
volumes:
|
||||||
|
- minio-data:/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
healthcheck:
|
||||||
|
test: [CMD, curl, -f, http://localhost:9000/minio/health/live]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 8
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
minio-init:
|
||||||
|
image: minio/mc:RELEASE.2025-08-13T08-35-41Z
|
||||||
|
restart: 'no'
|
||||||
|
entrypoint: [/bin/sh, /bootstrap/init-minio.sh]
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}
|
||||||
|
OBJECT_STORAGE_BUCKET: ${OBJECT_STORAGE_BUCKET:?OBJECT_STORAGE_BUCKET is required}
|
||||||
|
ADMIN_WEB_ORIGIN: ${ADMIN_WEB_ORIGIN:?ADMIN_WEB_ORIGIN is required}
|
||||||
|
volumes:
|
||||||
|
- ./minio/init-minio.sh:/bootstrap/init-minio.sh:ro
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
api:
|
||||||
|
image: lehuo-api:${IMAGE_TAG:?IMAGE_TAG is required}
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: apps/api/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
environment: *api-environment
|
||||||
|
extra_hosts:
|
||||||
|
- ${FILES_HOST:?FILES_HOST is required}:host-gateway
|
||||||
|
expose:
|
||||||
|
- '3000'
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
minio-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- node
|
||||||
|
- -e
|
||||||
|
- fetch('http://127.0.0.1:3000/health/live').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 8
|
||||||
|
start_period: 30s
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:size=64m,noexec,nosuid
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
image: lehuo-api:${IMAGE_TAG:?IMAGE_TAG is required}
|
||||||
|
profiles: [tools]
|
||||||
|
command:
|
||||||
|
- ./apps/api/node_modules/.bin/prisma
|
||||||
|
- migrate
|
||||||
|
- deploy
|
||||||
|
- --config
|
||||||
|
- apps/api/prisma.config.ts
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:size=64m,noexec,nosuid
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
web:
|
||||||
|
image: lehuo-admin:${IMAGE_TAG:?IMAGE_TAG is required}
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: apps/admin-web/Dockerfile
|
||||||
|
args:
|
||||||
|
VITE_API_BASE_URL: https://${PUBLIC_HOST:?PUBLIC_HOST is required}
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PUBLIC_HOST: ${PUBLIC_HOST:?PUBLIC_HOST is required}
|
||||||
|
FILES_HOST: ${FILES_HOST:?FILES_HOST is required}
|
||||||
|
NGINX_ENVSUBST_FILTER: ^(PUBLIC_HOST|FILES_HOST)$$
|
||||||
|
ports:
|
||||||
|
- '80:80'
|
||||||
|
- '443:443'
|
||||||
|
volumes:
|
||||||
|
- ${TLS_CERT_PATH:?TLS_CERT_PATH is required}:/etc/nginx/tls/fullchain.pem:ro
|
||||||
|
- ${TLS_KEY_PATH:?TLS_KEY_PATH is required}:/etc/nginx/tls/privkey.pem:ro
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
- edge
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /var/cache/nginx:size=32m,noexec,nosuid
|
||||||
|
- /var/run:size=4m,noexec,nosuid
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
edge:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql-data:
|
||||||
|
minio-data:
|
||||||
|
redis-data:
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 复制到服务器 /etc/lehuo/lehuo.env,并设置 chmod 600。
|
||||||
|
# 推荐使用 scripts/generate-env.sh 自动生成随机密钥,不要直接沿用本文件占位值。
|
||||||
|
IMAGE_TAG=replace-with-git-commit
|
||||||
|
PUBLIC_HOST=admin.example.com
|
||||||
|
FILES_HOST=files.example.com
|
||||||
|
TLS_CERT_PATH=/opt/lehuo/shared/tls/fullchain.pem
|
||||||
|
TLS_KEY_PATH=/opt/lehuo/shared/tls/privkey.pem
|
||||||
|
BACKUP_DIR=/opt/lehuo/backups
|
||||||
|
|
||||||
|
MYSQL_DATABASE=lehuo
|
||||||
|
MYSQL_USER=lehuo
|
||||||
|
MYSQL_PASSWORD=replace-with-random-url-safe-password
|
||||||
|
MYSQL_ROOT_PASSWORD=replace-with-different-random-password
|
||||||
|
DATABASE_URL=mysql://lehuo:replace-with-random-url-safe-password@mysql:3306/lehuo
|
||||||
|
|
||||||
|
REDIS_PASSWORD=replace-with-random-password
|
||||||
|
REDIS_URL=redis://:replace-with-random-password@redis:6379
|
||||||
|
|
||||||
|
MINIO_ROOT_USER=lehuo_storage
|
||||||
|
MINIO_ROOT_PASSWORD=replace-with-random-password
|
||||||
|
OBJECT_STORAGE_ENDPOINT=https://files.example.com
|
||||||
|
OBJECT_STORAGE_REGION=auto
|
||||||
|
OBJECT_STORAGE_BUCKET=lehuo-private
|
||||||
|
OBJECT_STORAGE_ACCESS_KEY=lehuo_storage
|
||||||
|
OBJECT_STORAGE_SECRET_KEY=replace-with-same-minio-password
|
||||||
|
|
||||||
|
SESSION_SECRET=replace-with-random-value
|
||||||
|
SESSION_TOKEN_PEPPER=replace-with-random-value
|
||||||
|
AUDIT_IP_HASH_KEY=replace-with-random-value
|
||||||
|
FIELD_ENCRYPTION_KEY=replace-with-base64-encoded-32-byte-key
|
||||||
|
ADMIN_WEB_ORIGIN=https://admin.example.com
|
||||||
|
AUTH_SESSION_TTL_SECONDS=28800
|
||||||
|
WECHAT_SESSION_TTL_SECONDS=7200
|
||||||
|
|
||||||
|
# 未取得真实微信参数前保持空值,并保持 worker 关闭。
|
||||||
|
WECHAT_APP_ID=
|
||||||
|
WECHAT_APP_SECRET=
|
||||||
|
WECHAT_TEMPLATE_APPLICATION_SUBMITTED=
|
||||||
|
WECHAT_TEMPLATE_APPLICATION_ACCEPTED=
|
||||||
|
WECHAT_TEMPLATE_SERVICE_ARRANGED=
|
||||||
|
WECHAT_TEMPLATE_SERVICE_COMPLETED=
|
||||||
|
WECHAT_TEMPLATE_FOLLOW_UP_PENDING=
|
||||||
|
WECHAT_TEMPLATE_ACTIVITY_REGISTRATION_RESULT=
|
||||||
|
NOTIFICATION_WORKER_ENABLED=false
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
: "${MINIO_ROOT_USER:?MINIO_ROOT_USER is required}"
|
||||||
|
: "${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}"
|
||||||
|
: "${OBJECT_STORAGE_BUCKET:?OBJECT_STORAGE_BUCKET is required}"
|
||||||
|
: "${ADMIN_WEB_ORIGIN:?ADMIN_WEB_ORIGIN is required}"
|
||||||
|
|
||||||
|
mc alias set lehuo http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" --api S3v4 >/dev/null
|
||||||
|
mc mb --ignore-existing "lehuo/$OBJECT_STORAGE_BUCKET" >/dev/null
|
||||||
|
mc anonymous set none "lehuo/$OBJECT_STORAGE_BUCKET" >/dev/null
|
||||||
|
mc version enable "lehuo/$OBJECT_STORAGE_BUCKET" >/dev/null
|
||||||
|
|
||||||
|
cat > /tmp/cors.xml <<EOF
|
||||||
|
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||||
|
<CORSRule>
|
||||||
|
<AllowedOrigin>${ADMIN_WEB_ORIGIN}</AllowedOrigin>
|
||||||
|
<AllowedMethod>GET</AllowedMethod>
|
||||||
|
<AllowedMethod>PUT</AllowedMethod>
|
||||||
|
<AllowedMethod>HEAD</AllowedMethod>
|
||||||
|
<AllowedHeader>*</AllowedHeader>
|
||||||
|
<ExposeHeader>ETag</ExposeHeader>
|
||||||
|
<MaxAgeSeconds>3600</MaxAgeSeconds>
|
||||||
|
</CORSRule>
|
||||||
|
</CORSConfiguration>
|
||||||
|
EOF
|
||||||
|
mc cors set "lehuo/$OBJECT_STORAGE_BUCKET" /tmp/cors.xml >/dev/null
|
||||||
|
rm -f /tmp/cors.xml
|
||||||
|
|
||||||
|
echo "MinIO private bucket initialized."
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
limit_req_zone $binary_remote_addr zone=api_per_ip:10m rate=10r/s;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ${PUBLIC_HOST} ${FILES_HOST};
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name ${PUBLIC_HOST};
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/tls/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/tls/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_session_cache shared:TLS:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
||||||
|
add_header X-Frame-Options DENY always;
|
||||||
|
|
||||||
|
client_max_body_size 12m;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
limit_req zone=api_per_ip burst=30 nodelay;
|
||||||
|
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 https;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_pass http://api:3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /health/ {
|
||||||
|
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 https;
|
||||||
|
proxy_pass http://api:3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name ${FILES_HOST};
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/tls/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/tls/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_session_cache shared:TLS:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header Referrer-Policy no-referrer always;
|
||||||
|
|
||||||
|
client_max_body_size 12m;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
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 https;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_pass http://minio:9000;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||||
|
|
||||||
|
env_file="${1:-/etc/lehuo/lehuo.env}"
|
||||||
|
require_command docker
|
||||||
|
require_command gzip
|
||||||
|
require_command sha256sum
|
||||||
|
require_env_file "$env_file"
|
||||||
|
|
||||||
|
backup_dir="$(env_value "$env_file" BACKUP_DIR)"
|
||||||
|
[[ "$backup_dir" == /opt/lehuo/backups || "$backup_dir" == /opt/lehuo/backups/* ]] || {
|
||||||
|
echo 'BACKUP_DIR 必须位于 /opt/lehuo/backups。' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
install -d -m 0700 "$backup_dir"
|
||||||
|
|
||||||
|
database="$(env_value "$env_file" MYSQL_DATABASE)"
|
||||||
|
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
target="$backup_dir/lehuo-${timestamp}.sql.gz"
|
||||||
|
temporary="${target}.tmp"
|
||||||
|
trap 'rm -f -- "$temporary"' EXIT
|
||||||
|
|
||||||
|
compose "$env_file" exec -T mysql sh -c \
|
||||||
|
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysqldump --single-transaction --routines --triggers --events --set-gtid-purged=OFF -u root "$MYSQL_DATABASE"' \
|
||||||
|
| gzip -9 > "$temporary"
|
||||||
|
|
||||||
|
gzip -t "$temporary"
|
||||||
|
mv -- "$temporary" "$target"
|
||||||
|
chmod 600 "$target"
|
||||||
|
sha256sum "$target" > "${target}.sha256"
|
||||||
|
chmod 600 "${target}.sha256"
|
||||||
|
find "$backup_dir" -maxdepth 1 -type f \( -name 'lehuo-*.sql.gz' -o -name 'lehuo-*.sql.gz.sha256' \) -mtime +30 -delete
|
||||||
|
|
||||||
|
printf '数据库备份完成:%s(数据库:%s)\n' "$target" "$database"
|
||||||
|
echo '请将备份及 SHA-256 文件同步到异地加密存储。'
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||||
|
|
||||||
|
env_file="${1:-/etc/lehuo/lehuo.env}"
|
||||||
|
require_env_file "$env_file"
|
||||||
|
|
||||||
|
read -r -p '初始管理员用户名:' username
|
||||||
|
read -r -p '初始管理员显示名:' display_name
|
||||||
|
read -r -s -p '初始管理员密码:' password
|
||||||
|
echo
|
||||||
|
[[ -n "$username" && -n "$display_name" && ${#password} -ge 12 ]] || {
|
||||||
|
echo '用户名/显示名不能为空,密码至少 12 位。' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
compose "$env_file" run --rm \
|
||||||
|
-e "BOOTSTRAP_ADMIN_USERNAME=${username}" \
|
||||||
|
-e "BOOTSTRAP_ADMIN_DISPLAY_NAME=${display_name}" \
|
||||||
|
-e "BOOTSTRAP_ADMIN_PASSWORD=${password}" \
|
||||||
|
api node apps/api/dist/scripts/bootstrap-admin.js
|
||||||
|
|
||||||
|
unset password
|
||||||
|
echo '初始管理员创建完成;命令会拒绝重复初始化。'
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||||
|
|
||||||
|
env_file="${1:-/etc/lehuo/lehuo.env}"
|
||||||
|
require_env_file "$env_file"
|
||||||
|
bash "$(dirname -- "${BASH_SOURCE[0]}")/preflight.sh" "$env_file"
|
||||||
|
|
||||||
|
image_tag="$(env_value "$env_file" IMAGE_TAG)"
|
||||||
|
release_dir='/opt/lehuo/releases'
|
||||||
|
install -d -m 0700 "$release_dir"
|
||||||
|
|
||||||
|
current_tag=''
|
||||||
|
if [[ -f "$release_dir/current" ]]; then
|
||||||
|
current_tag="$(<"$release_dir/current")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "构建不可变镜像:${image_tag}"
|
||||||
|
compose "$env_file" build --pull api web
|
||||||
|
|
||||||
|
echo '启动数据服务并初始化私有对象存储。'
|
||||||
|
compose "$env_file" up -d mysql redis minio minio-init
|
||||||
|
|
||||||
|
if [[ -n "$current_tag" ]]; then
|
||||||
|
echo '迁移前创建生产数据库备份。'
|
||||||
|
bash "$(dirname -- "${BASH_SOURCE[0]}")/backup.sh" "$env_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo '执行数据库 migration。'
|
||||||
|
compose "$env_file" --profile tools run --rm migrate
|
||||||
|
|
||||||
|
echo '部署 API 与管理后台网关。'
|
||||||
|
compose "$env_file" up -d --remove-orphans api web
|
||||||
|
|
||||||
|
bash "$(dirname -- "${BASH_SOURCE[0]}")/smoke.sh" "$env_file"
|
||||||
|
|
||||||
|
if [[ -n "$current_tag" && "$current_tag" != "$image_tag" ]]; then
|
||||||
|
printf '%s\n' "$current_tag" > "$release_dir/previous"
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$image_tag" > "$release_dir/current"
|
||||||
|
chmod 600 "$release_dir/current"
|
||||||
|
[[ ! -f "$release_dir/previous" ]] || chmod 600 "$release_dir/previous"
|
||||||
|
|
||||||
|
printf '部署完成,当前镜像标签:%s\n' "$image_tag"
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo '用法:generate-env.sh --public-host admin.example.com --files-host files.example.com [--output /etc/lehuo/lehuo.env]'
|
||||||
|
}
|
||||||
|
|
||||||
|
public_host=''
|
||||||
|
files_host=''
|
||||||
|
output='/etc/lehuo/lehuo.env'
|
||||||
|
while (($#)); do
|
||||||
|
case "$1" in
|
||||||
|
--public-host) public_host="${2:-}"; shift 2 ;;
|
||||||
|
--files-host) files_host="${2:-}"; shift 2 ;;
|
||||||
|
--output) output="${2:-}"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) usage >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ "$public_host" =~ ^[A-Za-z0-9.-]+$ ]] || { echo 'PUBLIC_HOST 格式无效。' >&2; exit 1; }
|
||||||
|
[[ "$files_host" =~ ^[A-Za-z0-9.-]+$ ]] || { echo 'FILES_HOST 格式无效。' >&2; exit 1; }
|
||||||
|
[[ "$public_host" != "$files_host" ]] || { echo '管理后台/API 域名与文件域名必须不同。' >&2; exit 1; }
|
||||||
|
command -v openssl >/dev/null 2>&1 || { echo '缺少 openssl。' >&2; exit 1; }
|
||||||
|
command -v git >/dev/null 2>&1 || { echo '缺少 git。' >&2; exit 1; }
|
||||||
|
|
||||||
|
if [[ -e "$output" ]]; then
|
||||||
|
printf '拒绝覆盖已有环境文件:%s\n' "$output" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -m 0700 "$(dirname -- "$output")"
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
mysql_password="$(openssl rand -hex 24)"
|
||||||
|
mysql_root_password="$(openssl rand -hex 32)"
|
||||||
|
redis_password="$(openssl rand -hex 32)"
|
||||||
|
minio_password="$(openssl rand -hex 32)"
|
||||||
|
session_secret="$(openssl rand -hex 32)"
|
||||||
|
session_pepper="$(openssl rand -hex 32)"
|
||||||
|
audit_key="$(openssl rand -hex 32)"
|
||||||
|
field_key="$(openssl rand -base64 32 | tr -d '\n')"
|
||||||
|
image_tag="$(git -C "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../.." && pwd)" rev-parse --short=12 HEAD)"
|
||||||
|
|
||||||
|
cat > "$output" <<EOF
|
||||||
|
IMAGE_TAG=${image_tag}
|
||||||
|
PUBLIC_HOST=${public_host}
|
||||||
|
FILES_HOST=${files_host}
|
||||||
|
TLS_CERT_PATH=/opt/lehuo/shared/tls/fullchain.pem
|
||||||
|
TLS_KEY_PATH=/opt/lehuo/shared/tls/privkey.pem
|
||||||
|
BACKUP_DIR=/opt/lehuo/backups
|
||||||
|
MYSQL_DATABASE=lehuo
|
||||||
|
MYSQL_USER=lehuo
|
||||||
|
MYSQL_PASSWORD=${mysql_password}
|
||||||
|
MYSQL_ROOT_PASSWORD=${mysql_root_password}
|
||||||
|
DATABASE_URL=mysql://lehuo:${mysql_password}@mysql:3306/lehuo
|
||||||
|
REDIS_PASSWORD=${redis_password}
|
||||||
|
REDIS_URL=redis://:${redis_password}@redis:6379
|
||||||
|
MINIO_ROOT_USER=lehuo_storage
|
||||||
|
MINIO_ROOT_PASSWORD=${minio_password}
|
||||||
|
OBJECT_STORAGE_ENDPOINT=https://${files_host}
|
||||||
|
OBJECT_STORAGE_REGION=auto
|
||||||
|
OBJECT_STORAGE_BUCKET=lehuo-private
|
||||||
|
OBJECT_STORAGE_ACCESS_KEY=lehuo_storage
|
||||||
|
OBJECT_STORAGE_SECRET_KEY=${minio_password}
|
||||||
|
SESSION_SECRET=${session_secret}
|
||||||
|
SESSION_TOKEN_PEPPER=${session_pepper}
|
||||||
|
AUDIT_IP_HASH_KEY=${audit_key}
|
||||||
|
FIELD_ENCRYPTION_KEY=${field_key}
|
||||||
|
ADMIN_WEB_ORIGIN=https://${public_host}
|
||||||
|
AUTH_SESSION_TTL_SECONDS=28800
|
||||||
|
WECHAT_SESSION_TTL_SECONDS=7200
|
||||||
|
WECHAT_APP_ID=
|
||||||
|
WECHAT_APP_SECRET=
|
||||||
|
WECHAT_TEMPLATE_APPLICATION_SUBMITTED=
|
||||||
|
WECHAT_TEMPLATE_APPLICATION_ACCEPTED=
|
||||||
|
WECHAT_TEMPLATE_SERVICE_ARRANGED=
|
||||||
|
WECHAT_TEMPLATE_SERVICE_COMPLETED=
|
||||||
|
WECHAT_TEMPLATE_FOLLOW_UP_PENDING=
|
||||||
|
WECHAT_TEMPLATE_ACTIVITY_REGISTRATION_RESULT=
|
||||||
|
NOTIFICATION_WORKER_ENABLED=false
|
||||||
|
EOF
|
||||||
|
chmod 600 "$output"
|
||||||
|
|
||||||
|
install -d -m 0700 /opt/lehuo/shared/tls /opt/lehuo/backups /opt/lehuo/releases
|
||||||
|
printf '环境文件已生成:%s\n' "$output"
|
||||||
|
echo '下一步:放置覆盖两个域名的 TLS 证书,然后运行 preflight.sh。'
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
if [[ "${EUID}" -ne 0 ]]; then
|
||||||
|
echo '请使用 sudo 运行此脚本。' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ubuntu_dir="$(cd -- "$script_dir/.." && pwd)"
|
||||||
|
repo_root="$(cd -- "$ubuntu_dir/../.." && pwd)"
|
||||||
|
[[ "$repo_root" == '/opt/lehuo/app' ]] || {
|
||||||
|
echo '安装定时器前请把仓库部署到 /opt/lehuo/app。' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ -f /etc/lehuo/lehuo.env ]] || { echo '缺少 /etc/lehuo/lehuo.env。' >&2; exit 1; }
|
||||||
|
|
||||||
|
install -m 0644 "$ubuntu_dir/systemd/lehuo-backup.service" /etc/systemd/system/lehuo-backup.service
|
||||||
|
install -m 0644 "$ubuntu_dir/systemd/lehuo-backup.timer" /etc/systemd/system/lehuo-backup.timer
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now lehuo-backup.timer
|
||||||
|
systemctl list-timers lehuo-backup.timer --no-pager
|
||||||
|
|
||||||
|
echo '每日备份定时器已安装。首次正式发布前仍须手工执行一次备份和恢复演练。'
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
if [[ "${EUID}" -ne 0 ]]; then
|
||||||
|
echo '请使用 sudo 运行此脚本。' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -r /etc/os-release ]]; then
|
||||||
|
echo '无法识别操作系统。' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source /etc/os-release
|
||||||
|
if [[ "${ID:-}" != 'ubuntu' || "${VERSION_ID:-}" != '26.04' ]]; then
|
||||||
|
printf '本部署包仅验证 Ubuntu 26.04;当前为 %s %s。\n' "${ID:-unknown}" "${VERSION_ID:-unknown}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker version >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||||
|
echo 'Docker Engine 与 Compose plugin 已安装。'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
conflicts=()
|
||||||
|
for package in docker.io docker-compose docker-compose-v2 podman-docker containerd runc; do
|
||||||
|
if dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q 'install ok installed'; then
|
||||||
|
conflicts+=("$package")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if ((${#conflicts[@]})); then
|
||||||
|
printf '检测到可能冲突的软件包:%s\n' "${conflicts[*]}" >&2
|
||||||
|
echo '请先人工确认并按 Docker 官方文档移除冲突包,再重新运行;脚本不会自动卸载现有容器运行时。' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y ca-certificates curl
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
|
||||||
|
architecture="$(dpkg --print-architecture)"
|
||||||
|
codename="${UBUNTU_CODENAME:-${VERSION_CODENAME}}"
|
||||||
|
cat > /etc/apt/sources.list.d/docker.sources <<EOF
|
||||||
|
Types: deb
|
||||||
|
URIs: https://download.docker.com/linux/ubuntu
|
||||||
|
Suites: ${codename}
|
||||||
|
Components: stable
|
||||||
|
Architectures: ${architecture}
|
||||||
|
Signed-By: /etc/apt/keyrings/docker.asc
|
||||||
|
EOF
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
systemctl enable --now docker
|
||||||
|
docker version
|
||||||
|
docker compose version
|
||||||
|
|
||||||
|
echo 'Docker 安装完成。Docker 组等同 root 权限,本脚本不会自动把普通用户加入 docker 组。'
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ubuntu_dir() {
|
||||||
|
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd
|
||||||
|
}
|
||||||
|
repo_root() {
|
||||||
|
cd -- "$(ubuntu_dir)/../.." && pwd
|
||||||
|
}
|
||||||
|
|
||||||
|
compose_file() {
|
||||||
|
printf '%s/compose.yaml\n' "$(ubuntu_dir)"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
|
printf '缺少命令:%s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env_file() {
|
||||||
|
local file="$1"
|
||||||
|
[[ -f "$file" ]] || {
|
||||||
|
printf '环境文件不存在:%s\n' "$file" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
local mode
|
||||||
|
mode="$(stat -c '%a' "$file")"
|
||||||
|
if [[ "$mode" != '600' && "$mode" != '400' ]]; then
|
||||||
|
printf '环境文件权限必须是 600 或 400,当前为 %s:%s\n' "$mode" "$file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
env_value() {
|
||||||
|
local file="$1"
|
||||||
|
local key="$2"
|
||||||
|
sed -n "s/^${key}=//p" "$file" | tail -n 1
|
||||||
|
}
|
||||||
|
|
||||||
|
compose() {
|
||||||
|
local env_file="$1"
|
||||||
|
shift
|
||||||
|
docker compose --project-directory "$(ubuntu_dir)" --env-file "$env_file" -f "$(compose_file)" "$@"
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||||
|
|
||||||
|
env_file="${1:-/etc/lehuo/lehuo.env}"
|
||||||
|
require_command docker
|
||||||
|
require_command curl
|
||||||
|
require_command openssl
|
||||||
|
require_env_file "$env_file"
|
||||||
|
|
||||||
|
docker version >/dev/null
|
||||||
|
docker compose version >/dev/null
|
||||||
|
|
||||||
|
required_keys=(IMAGE_TAG PUBLIC_HOST FILES_HOST TLS_CERT_PATH TLS_KEY_PATH BACKUP_DIR DATABASE_URL REDIS_URL FIELD_ENCRYPTION_KEY ADMIN_WEB_ORIGIN)
|
||||||
|
for key in "${required_keys[@]}"; do
|
||||||
|
value="$(env_value "$env_file" "$key")"
|
||||||
|
[[ -n "$value" && "$value" != replace-* ]] || {
|
||||||
|
printf '环境变量缺失或仍为占位值:%s\n' "$key" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
image_tag="$(env_value "$env_file" IMAGE_TAG)"
|
||||||
|
[[ "$image_tag" != 'latest' ]] || { echo '禁止使用 latest 镜像标签。' >&2; exit 1; }
|
||||||
|
|
||||||
|
public_host="$(env_value "$env_file" PUBLIC_HOST)"
|
||||||
|
files_host="$(env_value "$env_file" FILES_HOST)"
|
||||||
|
[[ "$public_host" != "$files_host" ]] || { echo 'PUBLIC_HOST 与 FILES_HOST 必须不同。' >&2; exit 1; }
|
||||||
|
|
||||||
|
cert_path="$(env_value "$env_file" TLS_CERT_PATH)"
|
||||||
|
key_path="$(env_value "$env_file" TLS_KEY_PATH)"
|
||||||
|
[[ -r "$cert_path" && -r "$key_path" ]] || { echo 'TLS 证书或私钥不存在/不可读。' >&2; exit 1; }
|
||||||
|
openssl x509 -in "$cert_path" -noout -checkend 604800 >/dev/null || {
|
||||||
|
echo 'TLS 证书将在 7 天内过期或无效。' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
openssl x509 -in "$cert_path" -noout -checkhost "$public_host" >/dev/null || {
|
||||||
|
printf 'TLS 证书不包含管理端域名:%s\n' "$public_host" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
openssl x509 -in "$cert_path" -noout -checkhost "$files_host" >/dev/null || {
|
||||||
|
printf 'TLS 证书不包含文件域名:%s\n' "$files_host" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
openssl pkey -in "$key_path" -noout -check >/dev/null
|
||||||
|
|
||||||
|
compose "$env_file" config --quiet
|
||||||
|
|
||||||
|
available_kb="$(df -Pk "$(repo_root)" | awk 'NR==2 {print $4}')"
|
||||||
|
((available_kb >= 10 * 1024 * 1024)) || { echo '可用磁盘不足 10 GiB。' >&2; exit 1; }
|
||||||
|
|
||||||
|
echo 'Ubuntu 生产部署预检通过。'
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo '用法:restore.sh <backup.sql.gz> <target_database> --confirm <target_database> [env-file]'
|
||||||
|
}
|
||||||
|
|
||||||
|
backup="${1:-}"
|
||||||
|
target_database="${2:-}"
|
||||||
|
confirm_flag="${3:-}"
|
||||||
|
confirm_database="${4:-}"
|
||||||
|
env_file="${5:-/etc/lehuo/lehuo.env}"
|
||||||
|
[[ -n "$backup" && -n "$target_database" && "$confirm_flag" == '--confirm' && "$confirm_database" == "$target_database" ]] || {
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$target_database" =~ ^[A-Za-z0-9_]+$ ]] || { echo '目标数据库名无效。' >&2; exit 1; }
|
||||||
|
|
||||||
|
require_command docker
|
||||||
|
require_command gzip
|
||||||
|
require_command sha256sum
|
||||||
|
require_command realpath
|
||||||
|
require_env_file "$env_file"
|
||||||
|
|
||||||
|
backup_dir="$(realpath -m "$(env_value "$env_file" BACKUP_DIR)")"
|
||||||
|
backup_path="$(realpath -e "$backup")"
|
||||||
|
[[ "$backup_path" == "$backup_dir"/* ]] || { echo '备份文件必须位于 BACKUP_DIR。' >&2; exit 1; }
|
||||||
|
[[ -f "${backup_path}.sha256" ]] || { echo '缺少 SHA-256 校验文件。' >&2; exit 1; }
|
||||||
|
(cd -- "$backup_dir" && sha256sum -c "$(basename -- "${backup_path}.sha256")")
|
||||||
|
gzip -t "$backup_path"
|
||||||
|
|
||||||
|
production_database="$(env_value "$env_file" MYSQL_DATABASE)"
|
||||||
|
if [[ "$target_database" == "$production_database" && "${ALLOW_PRODUCTION_RESTORE:-}" != 'YES' ]]; then
|
||||||
|
echo '拒绝覆盖生产数据库。若已完成停机、审批和二次备份,请显式设置 ALLOW_PRODUCTION_RESTORE=YES。' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
compose "$env_file" exec -T -e RESTORE_DATABASE="$target_database" mysql sh -c \
|
||||||
|
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -u root -e "CREATE DATABASE IF NOT EXISTS \`$RESTORE_DATABASE\` CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci"'
|
||||||
|
gzip -dc "$backup_path" | compose "$env_file" exec -T -e RESTORE_DATABASE="$target_database" mysql sh -c \
|
||||||
|
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -u root "$RESTORE_DATABASE"'
|
||||||
|
|
||||||
|
printf '恢复完成:%s → %s\n' "$backup_path" "$target_database"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||||
|
|
||||||
|
env_file="${1:-/etc/lehuo/lehuo.env}"
|
||||||
|
require_env_file "$env_file"
|
||||||
|
release_dir='/opt/lehuo/releases'
|
||||||
|
[[ -f "$release_dir/previous" ]] || { echo '没有可回滚的上一镜像标签。' >&2; exit 1; }
|
||||||
|
|
||||||
|
previous_tag="$(<"$release_dir/previous")"
|
||||||
|
current_tag="$(<"$release_dir/current")"
|
||||||
|
[[ -n "$previous_tag" && "$previous_tag" != 'latest' ]] || { echo '上一镜像标签无效。' >&2; exit 1; }
|
||||||
|
|
||||||
|
if ! docker image inspect "lehuo-api:${previous_tag}" >/dev/null 2>&1 || ! docker image inspect "lehuo-admin:${previous_tag}" >/dev/null 2>&1; then
|
||||||
|
echo '上一版本镜像在本机不存在,停止回滚。' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo '注意:此操作只回滚应用镜像,不回滚数据库。migration 必须保持向后兼容。'
|
||||||
|
IMAGE_TAG="$previous_tag" compose "$env_file" up -d --no-build api web
|
||||||
|
bash "$(dirname -- "${BASH_SOURCE[0]}")/smoke.sh" "$env_file"
|
||||||
|
printf '%s\n' "$previous_tag" > "$release_dir/current"
|
||||||
|
printf '%s\n' "$current_tag" > "$release_dir/previous"
|
||||||
|
chmod 600 "$release_dir/current" "$release_dir/previous"
|
||||||
|
printf '应用已回滚到:%s\n' "$previous_tag"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||||
|
|
||||||
|
env_file="${1:-/etc/lehuo/lehuo.env}"
|
||||||
|
require_command curl
|
||||||
|
require_env_file "$env_file"
|
||||||
|
|
||||||
|
public_host="$(env_value "$env_file" PUBLIC_HOST)"
|
||||||
|
files_host="$(env_value "$env_file" FILES_HOST)"
|
||||||
|
|
||||||
|
curl --fail --silent --show-error --max-time 15 "https://${public_host}/health/live" >/dev/null
|
||||||
|
curl --fail --silent --show-error --max-time 15 "https://${public_host}/health/ready" >/dev/null
|
||||||
|
curl --fail --silent --show-error --max-time 15 "https://${public_host}/" >/dev/null
|
||||||
|
curl --fail --silent --show-error --max-time 15 "https://${files_host}/minio/health/live" >/dev/null
|
||||||
|
|
||||||
|
echo 'HTTPS、API readiness、管理后台与对象存储冒烟检查通过。'
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Lehuo公益平台 MySQL backup
|
||||||
|
Requires=docker.service
|
||||||
|
After=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/lehuo/app
|
||||||
|
ExecStart=/usr/bin/bash /opt/lehuo/app/infra/ubuntu/scripts/backup.sh /etc/lehuo/lehuo.env
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=best-effort
|
||||||
|
IOSchedulingPriority=7
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Run Lehuo公益平台 MySQL backup daily
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 03:30:00 Asia/Shanghai
|
||||||
|
Persistent=true
|
||||||
|
RandomizedDelaySec=15m
|
||||||
|
Unit=lehuo-backup.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Reference in New Issue
Block a user