feat: ship blog platform admin and deploy stack

This commit is contained in:
2026-03-31 21:48:39 +08:00
parent a9a05aa105
commit 313f174fbc
210 changed files with 25476 additions and 5803 deletions

View File

@@ -0,0 +1,190 @@
# 通知 / digest / systemd 最终上线清单
这份清单按“上线前 -> 首次启动 -> 验证 -> 定时任务”来执行。
## A. 上线前参数确认
### 1. backend / compose
确认 `deploy/docker/config.yaml` 至少已经填写:
- `compose_env.DATABASE_URL`
- `compose_env.REDIS_URL`
- `compose_env.JWT_SECRET`
- `compose_env.APP_BASE_URL`
- `compose_env.PUBLIC_API_BASE_URL`
- `compose_env.ADMIN_API_BASE_URL`
- `compose_env.ADMIN_FRONTEND_BASE_URL`
- `compose_env.TERMI_ADMIN_TRUST_PROXY_AUTH=true`
- `compose_env.TERMI_ADMIN_LOCAL_LOGIN_ENABLED=false`
- `compose_env.TERMI_ADMIN_PROXY_SHARED_SECRET`
### 2. SMTP
订阅 double opt-in 和邮件通知需要:
- `compose_env.SMTP_ENABLE=true`
- `compose_env.SMTP_HOST`
- `compose_env.SMTP_PORT`
- `compose_env.SMTP_SECURE`
- `compose_env.SMTP_USER`
- `compose_env.SMTP_PASSWORD`
- `compose_env.SMTP_HELLO_NAME`
### 3. Caddy / TinyAuth / Pocket ID
确认宿主机 Caddy 已经:
- `blog.init.cool` -> `127.0.0.1:4321`
- `admin.blog.init.cool` -> `127.0.0.1:4322`
- `admin.blog.init.cool/api/*` -> `127.0.0.1:5150`
- `admin.blog.init.cool` 整体挂了 `import tinyauth`
- `/api/*` 转发时附带:
- `X-Termi-Proxy-Secret: {$TERMI_ADMIN_PROXY_SHARED_SECRET}`
可直接参考:
- `deploy/caddy/Caddyfile.tohka.production.example`
## B. 首次启动
推荐命令:
```bash
python deploy/scripts/render_compose_env.py \
--input deploy/docker/config.yaml \
--output deploy/docker/.env
docker compose \
-f deploy/docker/compose.package.yml \
-f deploy/docker/compose.tohka.override.yml \
--env-file deploy/docker/.env up -d
```
然后确认容器状态:
```bash
docker compose \
-f deploy/docker/compose.package.yml \
-f deploy/docker/compose.tohka.override.yml \
--env-file deploy/docker/.env ps
```
应该至少看到:
- `backend`
- `backend-worker`
- `frontend`
- `admin`
## C. 首次验证
### 1. 健康检查
```bash
curl -I http://127.0.0.1:5150/healthz
curl -I http://127.0.0.1:4321/healthz
curl -I http://127.0.0.1:4322/healthz
```
### 2. 后台 SSO
打开:
- `https://admin.blog.init.cool`
确认:
- 可以被 TinyAuth / Pocket ID 正常拦截与登录
- 登录后后台可进入
- 会话信息显示为代理登录(不是本地账号密码)
### 3. 订阅链路
至少做一次:
1. 前台提交邮箱订阅
2. 收到确认邮件
3. 点击确认链接
4. 能打开偏好页
5. 偏好页可暂停 / 恢复 / 退订
### 4. ntfy / webhook / digest
后台里至少验证一次:
- 手动发送测试通知
- 手动发送周报
- 手动发送月报
- 查看 delivery 是否从 `queued` 变成 `sent`
如果 delivery 一直卡在 `queued`
- 优先检查 `backend-worker` 是否在运行
- 检查 `REDIS_URL`
- 检查 SMTP / ntfy / webhook 目标
## D. 安装 systemd timers
```bash
sudo cp deploy/systemd/*.service /etc/systemd/system/
sudo cp deploy/systemd/*.timer /etc/systemd/system/
sudo systemctl daemon-reload
```
启用:
```bash
sudo systemctl enable --now termi-retry-deliveries.timer
sudo systemctl enable --now termi-weekly-digest.timer
sudo systemctl enable --now termi-monthly-digest.timer
sudo systemctl enable --now termi-backup-all.timer
sudo systemctl enable --now termi-backup-prune.timer
sudo systemctl enable --now termi-backup-offsite-sync.timer
```
查看状态:
```bash
systemctl list-timers --all | grep termi
```
## E. 当前默认调度时间
### 通知 / digest
- `termi-retry-deliveries.timer`
- 每 5 分钟执行一次
- `termi-weekly-digest.timer`
- 每周一 09:00
- `termi-monthly-digest.timer`
- 每月 1 日 09:30
### 备份
- `termi-backup-all.timer`
- 每天 03:10
- `termi-backup-prune.timer`
- 每天 04:15
- `termi-backup-offsite-sync.timer`
- 每天 04:40
如果时区不是你们想要的,改 `.timer` 里的 `OnCalendar=` 后重新:
```bash
sudo systemctl daemon-reload
sudo systemctl restart termi-retry-deliveries.timer
sudo systemctl restart termi-weekly-digest.timer
sudo systemctl restart termi-monthly-digest.timer
sudo systemctl restart termi-backup-all.timer
sudo systemctl restart termi-backup-prune.timer
sudo systemctl restart termi-backup-offsite-sync.timer
```
## F. 上线后一周内建议额外确认
- [ ] 有真实订阅确认邮件成功送达
- [ ] 有真实 ntfy / webhook 通知成功送达
- [ ] 至少有一条 digest 成功发出
- [ ] retry timer 能把失败投递重新入队
- [ ] 备份目录持续产生新文件
- [ ] prune 正常清理旧备份
- [ ] offsite sync 确实有异地副本
- [ ] 至少做过一次恢复演练

27
deploy/systemd/README.md Normal file
View File

@@ -0,0 +1,27 @@
# systemd timer 模板
这些模板默认假设:
- 仓库部署路径:`/opt/termi-astro`
- 使用 `docker compose -f deploy/docker/compose.package.yml --env-file deploy/docker/.env`
- backend / backend-worker 容器已长期运行
启用方式示例:
```bash
sudo cp deploy/systemd/*.service /etc/systemd/system/
sudo cp deploy/systemd/*.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now termi-backup-all.timer
sudo systemctl enable --now termi-backup-prune.timer
sudo systemctl enable --now termi-backup-offsite-sync.timer
sudo systemctl enable --now termi-retry-deliveries.timer
sudo systemctl enable --now termi-weekly-digest.timer
sudo systemctl enable --now termi-monthly-digest.timer
```
如果你们不使用 systemd也可以直接参考同目录命令改成 cron。
最终上线前建议再对照:
- `deploy/systemd/GO_LIVE_CHECKLIST.md`

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Termi backup all data
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/opt/termi-astro
ExecStart=/usr/bin/env bash ./deploy/scripts/backup/backup-all.sh

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Run Termi full backup nightly
[Timer]
OnCalendar=*-*-* 03:10:00
Persistent=true
Unit=termi-backup-all.service
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Termi sync backups offsite
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/opt/termi-astro
Environment=OFFSITE_TARGET=/mnt/offsite/termi-astro-backups
ExecStart=/usr/bin/env bash ./deploy/scripts/backup/sync-backups-offsite.sh

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Sync Termi backups offsite every morning
[Timer]
OnCalendar=*-*-* 04:40:00
Persistent=true
Unit=termi-backup-offsite-sync.service
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,8 @@
[Unit]
Description=Termi prune local backups
After=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/opt/termi-astro
ExecStart=/usr/bin/env bash ./deploy/scripts/backup/prune-backups.sh

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Prune old Termi backups daily
[Timer]
OnCalendar=*-*-* 04:15:00
Persistent=true
Unit=termi-backup-prune.service
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Termi monthly digest dispatcher
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/opt/termi-astro
ExecStart=/usr/bin/docker compose -f deploy/docker/compose.package.yml --env-file deploy/docker/.env exec -T backend termi_api-cli -e production task send_monthly_digest

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Run monthly digest on day 1
[Timer]
OnCalendar=*-*-01 09:30:00
Persistent=true
Unit=termi-monthly-digest.service
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Termi retry queued notification deliveries
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/opt/termi-astro
ExecStart=/usr/bin/docker compose -f deploy/docker/compose.package.yml --env-file deploy/docker/.env exec -T backend termi_api-cli -e production task retry_deliveries limit:200

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Retry notification deliveries every 5 minutes
[Timer]
OnCalendar=*:0/5
Persistent=true
Unit=termi-retry-deliveries.service
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Termi weekly digest dispatcher
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/opt/termi-astro
ExecStart=/usr/bin/docker compose -f deploy/docker/compose.package.yml --env-file deploy/docker/.env exec -T backend termi_api-cli -e production task send_weekly_digest

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Run weekly digest every Monday morning
[Timer]
OnCalendar=Mon *-*-* 09:00:00
Persistent=true
Unit=termi-weekly-digest.service
[Install]
WantedBy=timers.target