# Astro 博客部署与运维文档

> 部署时间：2026-05-15  
> 博客框架：Astro v5 + AstroPaper 主题 + MDX  
> 反向代理：Caddy v2.11.3 (Docker)  
> 文件同步：WebDAV (wsgidav, Docker)  
> 自动构建：inotifywait + pnpm (Docker, blog-watcher)

---

## 目录

1. [架构概览](#1-架构概览)
2. [访问入口](#2-访问入口)
3. [文件结构](#3-文件结构)
4. [配置详情](#4-配置详情)
   - 4.1 Docker Compose 编排
   - 4.2 Caddy 配置
   - 4.3 Astro 配置
5. [WebDAV 同步](#5-webdav-同步)
6. [自动构建机制](#6-自动构建机制)
7. [日常运维](#7-日常运维)
   - 7.1 管理命令
   - 7.2 发布文章流程
   - 7.3 检查状态
8. [故障排查](#8-故障排查)
9. [关键决策记录](#9-关键决策记录)

---

## 1. 架构概览

```
┌─ Obsidian ──────────────────┐
│  Remotely Save 插件          │
│  (WebDAV 同步)               │
└──────────┬───────────────────┘
           │ HTTPS
           ▼
┌──────────────────────────────────────────────────┐
│              Cloudflare (Proxy 🟠)                │
│  blog.sercypress.cn → SSL 终止 + CDN 缓存          │
│  回源端口：443（SSL Mode: Full）                    │
└──────────┬───────────────────────────────────────┘
           │ HTTPS
           ▼
┌──────────┴──────────────────────────────────────┐
│              Caddy (Docker, host network)         │
│  ├── blog.sercypress.cn      → file_server       │
│  │     root: /data/blog/dist                     │
│  ├── webdav.sercypress.cn    → reverse_proxy     │
│  │     127.0.0.1:8080                            │
│  └── dg4-lobe/panel/...  → 其他站点反代           │
└──────────┬──────────────────────────────────────┘
           │
      ┌────┴──────────────────────────────────┐
      │           Docker 容器栈               │
      │                                       │
      │  blog-webdav (wsgidav)                │
      │  端口: 127.0.0.1:8080                  │
      │  挂载: /data → /data                  │
      │  功能: Obsidian 文件同步入口            │
      │                                       │
      │  blog-watcher (inotify + pnpm)        │
      │  挂载: /data/blog → /blog             │
      │  功能: 监听文章变更 → 自动构建          │
      └───────────────────────────────────────┘
```

### 请求流程（blog）

```
用户浏览器 → Cloudflare (HTTPS) → Caddy (443) → /data/blog/dist/ (静态文件)
                                        ↑
                                  Let's Encrypt 证书
                                  (HTTP-01 自动签发)
```

### 请求流程（webdav）

```
Obsidian → Cloudflare (DNS-only ⚪, HTTPS) → Caddy (443) → blog-webdav:8080 → /data/
```

---

## 2. 访问入口

| 域名 | 类型 | 用途 | 状态 |
|------|------|------|------|
| https://blog.sercypress.cn | 🟠 CF Proxy | 博客站点 | ✅ |
| https://webdav.sercypress.cn | ⚪ DNS-only | WebDAV 同步入口 | ✅ |

---

## 3. 文件结构

```
/data/
├── blog/                          # Astro 博客根目录
│   ├── docker-compose.yml         # 统一编排（webdav + watcher）
│   ├── watcher/
│   │   ├── Dockerfile             # blog-watcher 镜像定义
│   │   └── entrypoint.sh          # inotify 监听循环脚本
│   ├── webdav/
│   │   ├── Dockerfile             # blog-webdav 镜像定义
│   │   └── htpasswd               # WebDAV 密码文件
│   ├── src/data/blog/             # 文章目录（WebDAV 同步目标）
│   ├── dist/                      # 构建产物（Caddy 服务目录）
│   ├── astro.config.ts            # Astro 配置文件
│   ├── package.json
│   └── pnpm-lock.yaml
│
├── caddy/
│   ├── Caddyfile                  # Caddy 主配置（含 blog/webdav）
│   └── docker-compose.yml         # Caddy 编排
│
└── webdav/                        # 旧目录，已迁移至 /data/blog/webdav/
```

---

## 4. 配置详情

### 4.1 Docker Compose 编排

文件位置：`/data/blog/docker-compose.yml`

```yaml
services:
  webdav:
    build: ./webdav
    container_name: blog-webdav
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - /data:/data

  watcher:
    build: ./watcher
    container_name: blog-watcher
    restart: unless-stopped
    volumes:
      - /data/blog:/blog
```

#### webdav 镜像（/data/blog/webdav/Dockerfile）

```dockerfile
FROM python:3.12-alpine
RUN pip install --no-cache-dir wsgidav cheroot
EXPOSE 80
CMD ["wsgidav", "--host=0.0.0.0", "--port=80", "--root=/data", "--auth=anonymous"]
```

> **注意**：当前使用匿名认证。由于 WebDAV 容器绑定 `127.0.0.1:8080`（仅本机可访问），且 Caddy 对外服务 HTTPS，安全性由网络层保障。如需密码认证，可参考 `wsgidav` 的配置文件方式添加。

#### watcher 镜像（/data/blog/watcher/Dockerfile）

```dockerfile
FROM node:22-alpine
RUN apk add --no-cache inotify-tools && \
    npm install -g pnpm
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
WORKDIR /blog
CMD ["/entrypoint.sh"]
```

> **环境变量**：需设置 `CI=true`（见 docker-compose.yml），否则 pnpm 在无 TTY 环境下会拒绝操作 `node_modules`。

#### watcher entrypoint（/data/blog/watcher/entrypoint.sh）

```bash
#!/bin/sh
set -e
echo "[watcher] Starting inotify watch on /blog/src/data/blog/..."
while true; do
  inotifywait -r -e modify,create,delete,move /blog/src/data/blog/
  echo "[watcher] Content changed, rebuilding..."
  cd /blog && pnpm run build
  echo "[watcher] Build complete, watching for changes..."
done
```

---

### 4.2 Caddy 配置

文件位置：`/data/caddy/Caddyfile`

```caddyfile
# Caddy 配置
# 所有站点 DNS-only，Caddy 自动 acme SSL
{
}

dg4-lobe.sercypress.cn {
    reverse_proxy 127.0.0.1:3210
}

dg4-lobe-fs.sercypress.cn {
    reverse_proxy 127.0.0.1:9000
}

openlist.sercypress.cn {
    reverse_proxy 127.0.0.1:15244
}

panel.sercypress.cn {
    reverse_proxy https://127.0.0.1:58900 {
        transport http {
            tls_insecure_skip_verify
        }
        header_up Host {http.request.host}
    }
}

vaultwarden.sercypress.cn {
    reverse_proxy 127.0.0.1:48080
}

blog.sercypress.cn {
    root * /data/blog/dist
    file_server
    encode gzip
    header {
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}

webdav.sercypress.cn {
    reverse_proxy 127.0.0.1:8080
}
```

> **注意**：Caddy 容器通过 volume 挂载 `/data/blog/dist:/data/blog`，因此配置中 `root` 指向容器内路径 `/data/blog`。

Caddy 编排文件：`/data/caddy/docker-compose.yml`

```yaml
services:
  caddy:
    image: caddy:2
    container_name: caddy
    network_mode: host
    restart: unless-stopped
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
      - /data/blog/dist:/data/blog

volumes:
  caddy_data:
  caddy_config:
```

---

### 4.3 Astro 配置

文件位置：`/data/blog/astro.config.ts`

关键配置点：

| 项目 | 值 |
|------|-----|
| 框架 | Astro v5 (^5.16.6) |
| 主题 | AstroPaper |
| 包管理 | pnpm v11 |
| 代码高亮 | Shiki (min-light / night-owl) |
| 搜索 | Pagefind 全文搜索 |
| RSS | @astrojs/rss ✅ |
| Sitemap | @astrojs/sitemap ✅ |
| MDX | @astrojs/mdx ✅ |
| 输出模式 | static（纯静态站点） |

---

## 5. WebDAV 同步

### 同步方式

- **客户端**：Obsidian + Remotely Save 插件
- **协议**：WebDAV
- **服务器**：https://webdav.sercypress.cn
- **根路径**：`/blog/src/data/blog/`

### 同步流程

```
Obsidian 编辑 .md 文件
    ↓ 点击同步按钮
Remotely Save 通过 WebDAV PUT 请求上传
    ↓
webdav.sercypress.cn → Caddy → blog-webdav:8080
    ↓
文件写入 /data/blog/src/data/blog/
    ↓
inotifywait 检测到变更 → 触发 pnpm build
```

### WebDAV 路径映射

| WebDAV URL | 宿主机路径 |
|------------|-----------|
| `/blog/src/data/blog/` | `/data/blog/src/data/blog/` |
| `/blog/dist/` | `/data/blog/dist/` |
| `/backups/` | `/data/backups/` |
| `/caddy/` | `/data/caddy/` |

> 根路径指向整个 `/data` 目录，因此可以通过不同子路径访问服务器上所有数据。

---

## 6. 自动构建机制

### 触发条件

`inotifywait` 监听 `/data/blog/src/data/blog/` 目录，以下事件触发构建：

| 事件 | 说明 |
|------|------|
| `modify` | 文件内容修改 |
| `create` | 新文件创建 |
| `delete` | 文件删除 |
| `move` | 文件重命名/移动 |

### 构建流程

```
文件变更 → inotifywait 事件
    ↓
cd /blog && pnpm run build
    ↓
1. Astro 编译 (43 pages, ~37s)
2. Pagefind 索引构建 (~0.6s)
3. 输出到 dist/
    ↓
Caddy 重新读取 dist/（文件系统级，无需重载）
    ↓
用户刷新浏览器即可看到更新
```

### 构建产物统计（首次构建）

- **页面数**：43 pages
- **构建时间**：~37s
- **搜索索引**：1 种语言（en），16 页索引，2218 词
- **RSS** ✓ | **Sitemap** ✓ | **搜索** ✓

---

## 7. 日常运维

### 7.1 管理命令

```bash
# 启动所有服务
docker compose -f /data/blog/docker-compose.yml up -d

# 停止所有服务
docker compose -f /data/blog/docker-compose.yml down

# 查看日志（持续跟踪）
docker compose -f /data/blog/docker-compose.yml logs -f

# 仅查看构建日志
docker compose -f /data/blog/docker-compose.yml logs -f watcher

# 查看 WebDAV 日志
docker compose -f /data/blog/docker-compose.yml logs -f webdav

# 手动触发一次构建
cd /data/blog && /usr/lib/node_modules/corepack/shims/pnpm run build

# 重启 Caddy（配置变更后）
sudo docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile

# 重启 Caddy 容器（volume 变更后）
cd /data/caddy && docker compose restart
```

### 7.2 发布文章流程

```
1. Obsidian 中编写 .md 文件
   - 文件放到 Obsidian 同步文件夹
   - 文件名格式：文章标题.md 或 文章标题.mdx
   - 需包含 AstroPaper 要求的 frontmatter

2. 在 Obsidian 中点击 Remotely Save 插件同步按钮
   - 文件通过 WebDAV 上传至服务器

3. 等待构建完成（约 40s）
   - 可执行 docker compose logs -f watcher 查看进度

4. 刷新 https://blog.sercypress.cn 确认新文章上线
```

**文章 frontmatter 示例：**

```yaml
---
title: "文章标题"
published: 2026-05-15
description: "简短描述"
tags: ["标签1", "标签2"]
category: 分类名
draft: false
---
```

### 7.3 检查状态

```bash
# 所有容器状态
docker ps --filter "name=blog-"
docker ps --filter "name=caddy"

# WebDAV 连通性
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/

# 博客可达性
curl -s -o /dev/null -w "%{http_code}" --resolve blog.sercypress.cn:443:127.0.0.1 https://blog.sercypress.cn/

# watcher 是否在监听
docker logs blog-watcher --tail 3

# 当前文章数量
ls /data/blog/src/data/blog/*.md 2>/dev/null | wc -l

# 磁盘使用
df -h /data
```

---

## 8. 故障排查

### 8.1 博客 404 / 内容未更新

```
1. 检查构建是否成功
   docker compose -f /data/blog/docker-compose.yml logs watcher

2. 检查 dist 目录是否有新内容
   ls -lt /data/blog/dist/index.html

3. 检查 Caddy 是否读到最新文件
   docker exec caddy ls -la /data/blog/

4. 如果缺少 dist，执行手动构建
   cd /data/blog && pnpm run build
```

### 8.2 WebDAV 连接失败

```
1. 检查 WebDAV 容器是否运行
   docker ps --filter "name=blog-webdav"

2. 测试本地连通性
   curl -v http://127.0.0.1:8080/

3. 测试通过 Caddy 的连通性
   curl -v --resolve webdav.sercypress.cn:443:127.0.0.1 https://webdav.sercypress.cn/

4. 检查日志
   docker compose -f /data/blog/docker-compose.yml logs webdav
```

### 8.3 Caddy 证书问题

```
# 查看证书状态
docker logs caddy --tail 50 | grep -i "cert\|acme\|tls\|error"

# 证书存储位置（named volume）
# Caddy 自动通过 Let's Encrypt HTTP-01 挑战续期
# blog.sercypress.cn 的挑战可通过 Cloudflare Proxy（SSL Mode: Full）正常完成

# 如果证书反复失败，检查 DNS 记录
# blog.sercypress.cn 必须为 Proxied 🟠
# webdav.sercypress.cn 必须为 DNS-only ⚪
```

### 8.4 watcher 不触发构建

```
1. 检查 watcher 是否运行
   docker ps --filter "name=blog-watcher"

2. 检查监听目录是否正确
   docker exec blog-watcher ls -la /blog/src/data/blog/

3. 查看 watcher 日志
   docker logs blog-watcher

4. 重启 watcher
   docker compose -f /data/blog/docker-compose.yml restart watcher

5. 手动测试触发
   touch /data/blog/src/data/blog/test-trigger.md
   # 然后查看日志是否触发构建
```

---

## 9. 关键决策记录

### 架构决策

| 决策 | 选项 | 选择 | 理由 |
|------|------|------|------|
| 框架 | Hexo / Astro / Hugo | **Astro** | 现代化，MDX 支持，性能好 |
| 主题 | 自建 / AstroPaper / Fuwari | **AstroPaper** | 技术博客风格，开箱即用 |
| 包管理器 | npm / pnpm / yarn | **pnpm** | Astro 生态首选，更快 |
| 输出模式 | static / server | **static** | 纯博客无需 SSR |
| 同步方式 | GitHub / WebDAV / rsync | **WebDAV** | 用户偏好，对接 Obsidian Remotely Save |
| 构建触发 | systemd inotify / 手动 / cron | **Docker inotify** | 编排统一，不依赖 systemd |
| SSL 策略 | Caddy auto / CF origin cert | **Caddy auto (HTTP-01)** | 零配置，Let's Encrypt 自动续期 |

### Cloudflare SSL 配置

- **模式**：Full（非 Flexible）
- **效果**：CF 回源时走 HTTPS 443，Caddy 正常服务 HTTPS
- **证书**：Caddy 通过 HTTP-01 challenge 获取 Let's Encrypt 证书
- **注意**：HTTP-01 挑战请求经过 CF Proxy 到达 Caddy，Caddy 自动处理 `/.well-known/acme-challenge/` 路径

### 容器网络

- **Caddy**：`network_mode: host`（因后端服务均绑定 127.0.0.1）
- **blog-webdav**：端口映射 `127.0.0.1:8080:80`
- **blog-watcher**：无端口暴露，仅 volume 挂载

### WebDAV 认证

- 当前状态：**匿名访问**（容器仅绑定 127.0.0.1，安全边界由网络层保障）
- 如需添加认证：可使用 wsgidav 的 `--auth=basic` + `--password-file` 参数，配合 proxy 添加 Caddy-level 认证

---

## 附录

### A. 相关凭证

| 项目 | 值 | 存放位置 |
|------|-----|---------|
| WebDAV 密码 | （见容器配置文件） | `/data/blog/webdav/htpasswd` |
| CF API Token | cfat_PYgy... | acme.sh 配置中 |
| CF Zone ID | 5c12d24acb58197cdddb3e0915bd3aff | hk.sercypress.cn 证书配置 |

### B. 依赖版本

| 组件 | 版本 |
|------|------|
| Node.js | v22.22.0 |
| pnpm | v11.1.2 |
| Astro | ^5.16.6 |
| @astrojs/mdx | 5.0.4 |
| Caddy | v2.11.3 |
| wsgidav (Python) | latest (pip) |
| inotify-tools | 3.22.6.0 |
| 操作系统 | Debian 12 (bookworm) |

### C. 相关文件

| 文件 | 路径 |
|------|------|
| Docker Compose | `/data/blog/docker-compose.yml` |
| Caddy 配置 | `/data/caddy/Caddyfile` |
| Caddy 编排 | `/data/caddy/docker-compose.yml` |
| Astro 配置 | `/data/blog/astro.config.ts` |
| Watcher Dockerfile | `/data/blog/watcher/Dockerfile` |
| Watcher entrypoint | `/data/blog/watcher/entrypoint.sh` |
| WebDAV Dockerfile | `/data/blog/webdav/Dockerfile` |
| WebDAV 密码文件 | `/data/blog/webdav/htpasswd` |
