# Docker 部署（Package 镜像）

补充架构说明见：

- `deploy/docker/ARCHITECTURE.md`
- `deploy/caddy/Caddyfile.tohka.example`
- `deploy/caddy/Caddyfile.tohka.production.example`
- `deploy/docker/TOHKA_POCKET_ID.md`
- `deploy/docker/TOHKA_DEPLOY_RUNBOOK.md`
- `deploy/docker/config.yaml.example`
- `deploy/docker/BACKUP_AND_RECOVERY.md`

## 1) 准备主配置文件（config.yaml）

现在推荐把：

- `deploy/docker/config.yaml`

作为部署主配置源；`deploy/docker/.env` 改为脚本生成产物。

先复制模板：

```bash
cp deploy/docker/config.yaml.example deploy/docker/config.yaml
```

然后填写至少这些核心项：

- `compose_env.DATABASE_URL`
- `compose_env.REDIS_URL`
- `compose_env.JWT_SECRET`
- 邮件确认 / 邮件通知上线前，请同时补齐 SMTP 配置

填完后执行：

```bash
python deploy/scripts/render_compose_env.py \
  --input deploy/docker/config.yaml \
  --output deploy/docker/.env
```

如果你们内部喜欢先审 YAML，再部署，这就是现在的推荐流程。

建议在 `config.yaml -> compose_env` 下同时检查这些运行时变量：

- `BACKEND_MEMORY_LIMIT / BACKEND_MEMORY_SWAP_LIMIT`：backend 容器内存 / swap 上限；对小内存主机建议显式设置
- `BACKEND_WORKER_MEMORY_LIMIT / BACKEND_WORKER_MEMORY_SWAP_LIMIT`：worker 容器内存 / swap 上限
- `FRONTEND_MEMORY_LIMIT / FRONTEND_MEMORY_SWAP_LIMIT`：frontend 容器内存 / swap 上限
- `ADMIN_MEMORY_LIMIT / ADMIN_MEMORY_SWAP_LIMIT`：admin 容器内存 / swap 上限
- `INTERNAL_API_BASE_URL`：frontend SSR 容器访问 backend 用，compose 默认推荐 `http://backend:5150/api`
- `PUBLIC_API_BASE_URL`：浏览器访问 backend API 用；留空时前台会回退到“当前主机 + `:5150/api`”
- `PUBLIC_COMMENT_TURNSTILE_SITE_KEY`：前台评论 / 订阅表单使用的 Cloudflare Turnstile site key
- `PUBLIC_WEB_PUSH_VAPID_PUBLIC_KEY`：前台浏览器推送订阅使用的 VAPID public key
- `PUBLIC_IMAGE_ALLOWED_HOSTS`：前台 `/_img` 图片优化端点允许的额外图片 host（逗号分隔）
- `ADMIN_API_BASE_URL`：admin 浏览器访问 backend API 用；留空时后台会回退到“当前主机 + `:5150`”
- `ADMIN_FRONTEND_BASE_URL`：admin 里“打开前台 / 问答页 / 文章页预览”跳转用
- `TERMI_ADMIN_TRUST_PROXY_AUTH`：是否信任前置代理（如 Caddy + TinyAuth）注入的后台认证头
- `TERMI_ADMIN_LOCAL_LOGIN_ENABLED`：是否保留本地账号密码登录兜底
- `TERMI_ADMIN_PROXY_SHARED_SECRET`：代理 SSO 共享密钥；建议和 Caddy 的 `X-Termi-Proxy-Secret` 配套使用
- `TERMI_TURNSTILE_SECRET_KEY`：backend 评论 / 订阅接口使用的 Cloudflare Turnstile secret key（兼容旧的 `TERMI_COMMENT_TURNSTILE_SECRET_KEY`）
- `TERMI_WEB_PUSH_VAPID_PRIVATE_KEY`：backend / worker 发送浏览器推送时使用的 VAPID private key
- `TERMI_WEB_PUSH_VAPID_SUBJECT`：浏览器推送 VAPID subject，推荐 `mailto:xxx@example.com`
- `SMTP_ENABLE / SMTP_HOST / SMTP_PORT / SMTP_SECURE / SMTP_USER / SMTP_PASSWORD / SMTP_HELLO_NAME`：订阅确认和邮件通知需要

例如：

```yaml
compose_env:
  BACKEND_MEMORY_LIMIT: 768m
  BACKEND_MEMORY_SWAP_LIMIT: 768m
  BACKEND_WORKER_MEMORY_LIMIT: 1g
  BACKEND_WORKER_MEMORY_SWAP_LIMIT: 1g
  FRONTEND_MEMORY_LIMIT: 256m
  FRONTEND_MEMORY_SWAP_LIMIT: 256m
  ADMIN_MEMORY_LIMIT: 128m
  ADMIN_MEMORY_SWAP_LIMIT: 128m
  PUBLIC_API_BASE_URL: https://api.blog.init.cool
  PUBLIC_COMMENT_TURNSTILE_SITE_KEY: 1x00000000000000000000AA
  PUBLIC_WEB_PUSH_VAPID_PUBLIC_KEY: replace-with-web-push-vapid-public-key
  ADMIN_API_BASE_URL: https://admin.blog.init.cool
  ADMIN_FRONTEND_BASE_URL: https://blog.init.cool
  TERMI_ADMIN_TRUST_PROXY_AUTH: true
  TERMI_ADMIN_LOCAL_LOGIN_ENABLED: false
  TERMI_ADMIN_PROXY_SHARED_SECRET: replace-with-a-long-random-secret
  TERMI_TURNSTILE_SECRET_KEY: replace-with-turnstile-secret-key
  TERMI_WEB_PUSH_VAPID_PRIVATE_KEY: replace-with-web-push-vapid-private-key
  TERMI_WEB_PUSH_VAPID_SUBJECT: mailto:noreply@blog.init.cool
```

> 这些值最终会被渲染成 `deploy/docker/.env`，再由 `compose.package.yml` 读取。  
> 如果镜像构建期也注入了 `FRONTEND_PUBLIC_API_BASE_URL` / `ADMIN_VITE_API_BASE` / `ADMIN_VITE_FRONTEND_BASE_URL`，则运行时变量优先级更高。

## 2) 启动

在仓库根目录执行：

```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 --env-file deploy/docker/.env up -d
```

如果是在 `tohka` 上并且前面接宿主机 Caddy，推荐改用：

```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
```

当前 compose 推荐一起启动 4 个容器：

- `backend`：API / migration / `/healthz`
- `backend-worker`：Redis 队列消费者（通知异步投递、失败重试）
- `frontend`：Astro SSR(Node)
- `admin`：静态 SPA + Nginx

`compose.tohka.override.yml` 额外会把三个对外端口绑到 `127.0.0.1`，避免把 backend / admin 直接暴露到公网。

## 3) 更新镜像后重拉

```bash
docker compose -f deploy/docker/compose.package.yml --env-file deploy/docker/.env pull
docker compose -f deploy/docker/compose.package.yml --env-file deploy/docker/.env up -d
```

## 4) 问答记录（2026-03-31）

### Q1: 为什么前台容器是 `4321` 端口？
A: 前台是 **Astro SSR(Node)**（`output: 'server'`），容器内运行 Node 服务（`dist/server/entry.mjs`），所以需要监听内部端口 `4321`。

### Q2: 两个前端都能做成纯静态 + 反代吗？
A: 
- `admin` 可以，当前就是静态资源由 Nginx 提供。
- `frontend` 当前不行（未做纯静态改造）：项目里使用了 `prerender = false`、`Astro.request`、`Astro.cookies` 等请求期逻辑。

### Q3: SSR 和 CSR 怎么选？
A: 当前站点对外内容页优先 SEO 与首屏可见性，保留 SSR 更稳；后台管理台继续 CSR/静态站即可。

### Q4: 生产推荐端口设计是什么？
A: 推荐前置 Caddy/Nginx 统一暴露 `80/443`，`frontend:4321` / `backend:5150` / `admin:80` 仅走内网。
当前 `compose.package.yml` 属于直连端口版，便于快速部署与联调。  
另外因为通知已经走异步队列，生产务必同时启动 `backend-worker`。  
AI 索引重建当前直接在 `backend` 进程本地启动，不依赖 `backend-worker` 消费 Redis 队列。

### Q5: 为什么 compose 里没看到 `ADMIN_VITE_FRONTEND_BASE_URL`？
A:
- 现在 compose 运行时可以直接设置 `ADMIN_FRONTEND_BASE_URL`，用于覆盖 admin 里所有前台跳转链接。
- `ADMIN_VITE_FRONTEND_BASE_URL` 仍然可以作为 **镜像构建期默认值** 保留在 CI / workflow 里。
- 如果两者都存在，**运行时 `ADMIN_FRONTEND_BASE_URL` 优先**。

### Q6: 前台 / 后台为什么拆成 public/internal 两种 API 地址？
A:
- `frontend` 是 Astro SSR，服务端渲染请求 backend 时更适合走内网地址（如 `http://backend:5150/api`）。
- 但浏览器里的评论、问答、搜索请求必须走用户可访问的公开地址。
- `admin` 是纯静态 SPA，也只能使用浏览器可访问的公开 API 地址。
- 所以现在区分为：
  - `INTERNAL_API_BASE_URL`：frontend SSR 内部访问
  - `PUBLIC_API_BASE_URL`：前台浏览器访问
  - `ADMIN_API_BASE_URL`：admin 浏览器访问

### Q7: 现在后台 OIDC / Pocket ID 怎么接？
A:
- 推荐直接复用 tohka 上现成的 **TinyAuth + Pocket ID**。
- Caddy 在 `admin` 域名入口加 `import tinyauth`，并把 `/api/*` 同域转发到 backend。
- backend 开启：
  - `TERMI_ADMIN_TRUST_PROXY_AUTH=true`
  - `TERMI_ADMIN_LOCAL_LOGIN_ENABLED=false`
  - `TERMI_ADMIN_PROXY_SHARED_SECRET=<随机长字符串>`
- Caddy 在 `/api/*` -> backend 时补：
  - `X-Termi-Proxy-Secret: {$TERMI_ADMIN_PROXY_SHARED_SECRET}`
- backend 会信任 TinyAuth 转发的：
  - `Remote-User`
  - `Remote-Email`
  - `Remote-Groups`
- 本地开发仍可保留内置账号密码登录。

### Q8: 备份现在放在哪看？
A:
- 参考脚本：`deploy/scripts/backup/`
- 恢复文档：`deploy/docker/BACKUP_AND_RECOVERY.md`

### Q9: 现在健康检查和 migration 怎么处理？
A:
- `backend` 镜像启动时会先执行 `db migrate`
- `backend` 提供 `/healthz`
- `backend-worker` 不提供 HTTP `/healthz`；compose 会覆盖镜像默认 healthcheck，改为检查主进程是否仍以 `--worker` 模式运行
- `frontend` 提供 `/healthz`
- `admin` 继续由 Nginx 提供 `/healthz`
- compose 现在使用 `depends_on.condition: service_healthy`
