Homelab 备份体系实战:Kopia + CronJob + GitOps 实现零数据丢失
目录
背景#
我的 Homelab 由两个 K3s 集群组成:
- homelab (Proxmox VM):运行 Vault、ZITADEL SSO、Calibre-Web、Grafana 等核心服务
- oracle-k3s (Oracle Cloud):运行 Miniflux RSS、KaraKeep 书签、Uptime Kuma 监控等轻量服务
整套基础设施通过 Terraform、Ansible、ArgoCD 实现了完整的 IaC + GitOps 管理。理论上,任何一个集群崩溃,我都可以通过 Git 仓库里的代码重建一切。
但有一个致命的盲区:数据。
IaC 可以重建基础设施,GitOps 可以重建应用配置,但 Vault 的密钥、ZITADEL 的用户数据、Miniflux 的 RSS 阅读历史——这些有状态数据一旦丢失,就真的没了。
本文记录我如何在这套双集群架构上构建自动化的 Kopia 备份体系,顺便分享修复一个 Traefik 路由 bug 的过程。
问题发现:backup.meirong.dev 无法访问#
在着手做备份自动化之前,我发现 Kopia 的 Web UI (backup.meirong.dev) 打不开了。
排查过程#
先检查 Pod 状态——Kopia 正常运行:
$ kubectl get pods -n kopia
NAME READY STATUS RESTARTS AGE
kopia-56cf7cd87f-mp9xn 1/1 Running 0 161m
查看日志,关键信息:
SERVER ADDRESS: http://[::]:51515
注意这里是 HTTP,因为 Kopia 启动参数是 --insecure(HTTP 模式)。
但 Traefik 的路由配置是这样的:
# TraefikService 配置了 scheme: https
apiVersion: traefik.io/v1alpha1
kind: TraefikService
metadata:
name: kopia-ts
namespace: kopia
spec:
weighted:
services:
- name: kopia
port: 51515
scheme: https # ← 问题在这里!
serversTransport: kopia-transport
Traefik 认为后端是 HTTPS,但实际是 HTTP。TLS 握手自然失败:
TLS connect error: error:0A00010B:SSL routines::wrong version number
修复#
最简单的方案:去掉 TraefikService 中间层,让 HTTPRoute 直连 Kopia 的 Service:
# 修复前:通过 TraefikService 代理 (scheme: https)
backendRefs:
- group: traefik.io
kind: TraefikService
name: kopia-ts
port: 51515
# 修复后:直接路由到 Service (HTTP)
backendRefs:
- group: ""
kind: Service
name: kopia
port: 51515
weight: 1
同时删除不再需要的 ServersTransport 和 TraefikService 资源。
ArgoCD 自愈的坑#
改完 manifest 后执行 kubectl apply — 表面上成功,1 秒后 ArgoCD 就把修改撤回了。
原因:ArgoCD 配置了 selfHeal: true,它从 GitHub 远端同步,而我的改动还没 push。活状态和 Git 不一致时,ArgoCD 会立刻撤销掉任何手动修改。
解决方法很简单:先 git push,再让 ArgoCD 同步。这也正是 GitOps 的核心哲学——Git 是唯一的真相来源。
数据分类#
在做备份策略之前,先对所有有状态数据做分类。我用了一个简单的优先级模型:
| 优先级 | 服务 | 集群 | 数据特征 |
|---|---|---|---|
| 🔴 P0 | Vault | homelab | 所有服务的 secrets 源头,丢失 = 全部服务失联 |
| 🔴 P0 | ZITADEL PostgreSQL | homelab | SSO 身份数据,丢失 = 所有用户无法登录 |
| 🟡 P1 | Calibre-Web | homelab | 电子书库配置 |
| 🟡 P1 | Miniflux PostgreSQL | oracle-k3s | RSS 订阅与阅读历史 |
| 🟡 P1 | KaraKeep | oracle-k3s | 书签与全文快照 (SQLite) |
| 🟡 P1 | Gotify | homelab | 通知历史 |
| 🟢 P2 | Uptime Kuma | oracle-k3s | 监控历史 (SQLite) |
| 🟢 P2 | Timeslot | oracle-k3s | 日历数据 (SQLite) |
| ⚪ 无需 | IT-Tools / PDF / Squoosh | oracle-k3s | 无状态,随时可重建 |
关键洞察:P0 数据决定了整个系统能否正常运行, P1 数据丢失只是不方便但不致命。
Kopia 备份架构#
整体设计#
homelab NFS volumes ──→ Kopia CronJob (本地) ──→ Kopia repository (NFS 1Ti)
↑
oracle-k3s volumes ──→ Kopia CronJob ──(Tailscale)──→ Kopia server (NodePort 31515)
为什么选 Kopia#
在 K8s 生态里备份方案不少(Velero、Longhorn 快照、Restic 等),选 Kopia 的原因:
- 增量去重:支持内容寻址存储,增量快照效率高
- 服务端/客户端模式:oracle-k3s 可以通过网络直连 homelab 的 Kopia server
- 标签系统:可以给快照打标签 (
cluster:homelab,priority:P0),方便筛选 - Web UI:
backup.meirong.dev提供可视化管理
homelab 集群备份 CronJob#
homelab 的备份 CronJob 设计为一个多阶段 Pod:
initContainer[0]: vault-backup # cp Vault 数据目录
initContainer[1]: zitadel-pg-dump # pg_dump ZITADEL PostgreSQL
initContainer[2]: calibre-config # cp Calibre-Web 配置
initContainer[3]: gotify-backup # cp Gotify 数据
container: kopia-snapshot # 统一创建 Kopia 快照
关键设计决策:
1. 使用 initContainer 链式执行
每个 initContainer 负责一个服务的数据提取,写入共享的 emptyDir staging 目录。最后一个 main container 统一做 Kopia 快照。这样任一步骤失败都有清晰的错误边界。
2. 跨命名空间访问 PVC
Vault 数据在 vault namespace,ZITADEL PG 在 zitadel namespace,但 CronJob 在 kopia namespace。
对于 NFS PVC(ReadWriteMany),K8s 允许跨 namespace 挂载同一个底层 NFS volume。对于 PostgreSQL,使用 service DNS (zitadel-db-postgresql.zitadel.svc.cluster.local) 做 pg_dump。
3. 标签驱动的快照管理
每个快照都带有 cluster、service、priority 标签:
kopia snapshot create /staging/vault \
--tags="cluster:homelab" \
--tags="service:vault" \
--tags="priority:P0"
恢复时可以按标签精确筛选。
oracle-k3s 跨集群备份#
oracle-k3s 的 CronJob 更有趣——需要通过 Tailscale 网络连接到 homelab 的 Kopia server。
两个 CronJob 错开时间避免冲突:
rss-system: 03:00 UTC — Miniflux pg_dump + KaraKeep SQLitepersonal-services: 03:30 UTC — Uptime Kuma SQLite + Timeslot SQLite
连接方式:
kopia repository connect server \
--url="$KOPIA_SERVER_URL" \ # https://100.96.84.32:31515
--server-cert-fingerprint="$KOPIA_SERVER_FINGERPRINT" \
--override-username=backup \
--override-hostname=oracle-k3s
credentials 通过 ESO (External Secrets Operator) 从 Vault 同步,不落 Git。
SQLite 备份的注意事项#
SQLite 数据库不能直接对运行中的 .db 文件做快照,WAL 文件 (.db-wal) 可能包含未写入的数据。我的做法是把 .db + .db-wal + .db-shm 三个文件一起复制:
cp /uptime-kuma/kuma.db /staging/uptime-kuma/kuma.db
cp /uptime-kuma/kuma.db-wal /staging/uptime-kuma/kuma.db-wal 2>/dev/null || true
cp /uptime-kuma/kuma.db-shm /staging/uptime-kuma/kuma.db-shm 2>/dev/null || true
PVC 以 readOnly: true 挂载,确保备份过程不会影响正在运行的服务。
GitOps 管理备份#
所有备份配置都以 YAML 清单的形式存在 Git 仓库中:
k8s/helm/manifests/
├── kopia.yaml # Kopia server Deployment + Service
└── kopia-backup.yaml # Backup CronJob + ExternalSecret
cloud/oracle/manifests/
├── rss-system/
│ └── backup-cronjob.yaml # Miniflux + KaraKeep 备份
└── personal-services/
└── backup-cronjob.yaml # Uptime Kuma + Timeslot 备份
ArgoCD Application 配置:
# argocd/applications/kopia.yaml
source:
path: k8s/helm/manifests
directory:
include: "{kopia.yaml,kopia-backup.yaml}"
syncPolicy:
automated:
prune: true
selfHeal: true
修改备份策略只需要编辑 YAML → git push → ArgoCD 自动同步。
灾难恢复 SOP#
有了备份,还需要可执行的恢复流程。完整的灾难恢复顺序:
1. Proxmox VM 重建 → terraform apply (proxmox/)
2. K3s 安装 → just setup-k8s (k8s/ansible/)
3. NFS Provisioner 安装 → just setup-nfs-provisioner
4. Vault 数据恢复 + unseal → kopia restore + just vault-unseal
5. ESO 安装 → Vault secrets 同步到 K8s
6. ArgoCD 安装 → just deploy-argocd
7. ArgoCD 同步所有 App → just argocd-sync
8. ZITADEL PG 恢复 → pg_restore
9. 验证 SSO 链路 → curl auth.meirong.dev
10. Cloudflare DNS → just apply (cloudflare/terraform/)
关键点:步骤 1-3 和 5-7 完全由 IaC/GitOps 自动完成。唯一需要手动干预的是步骤 4(Vault 恢复和 unseal)和步骤 8(ZITADEL 数据库恢复)。
经验总结#
1. GitOps 先行原则#
这次调试 backup.meirong.dev 遇到的 ArgoCD 自愈问题,再次印证了一个原则:所有生产变更都必须先经过 Git。
直接 kubectl apply 在 ArgoCD selfHeal 模式下是无效的。正确的工作流是:
编辑 YAML → git commit → git push → ArgoCD 自动同步
2. 测试环境模拟生产路径#
调试 Traefik 路由时,我用了一个简单的 curl Pod 来模拟 Cloudflare Tunnel 到 Traefik 的请求路径:
kubectl run test --image=curlimages/curl --rm -it --restart=Never -- \
sh -c "curl -sSv -H 'Host: backup.meirong.dev' http://traefik.kube-system.svc:80/"
这比从外网测试更快、更容易定位问题在哪一层。
3. 数据分类决定投资重点#
不是所有数据都值得同等对待。P0 数据(Vault、ZITADEL)配每日备份,P2 数据(监控历史)丢了也无所谓。分类驱动策略,避免过度工程。
4. 备份尚存的不足#
- 无离站副本:所有备份在同一个 NFS 后端(Proxmox 主机)。NFS 故障 = 备份也丢了
- 未做恢复演练:SOP 写了,但没有实际执行过完整的恢复流程
- ZITADEL pg_dump 密码管理:需要在 Vault 中配置
secret/homelab/zitadel→db-password
下一步计划:添加 Backblaze B2 作为离站备份目标,并做一次完整的恢复演练。
完整变更清单#
本次实施的所有变更:
| 变更 | 文件 | 说明 |
|---|---|---|
| 修复 backup.meirong.dev | k8s/helm/manifests/gateway.yaml |
TraefikService HTTPS → 直连 HTTP Service |
| 清理冗余资源 | k8s/helm/manifests/kopia.yaml |
删除 ServersTransport |
| homelab 自动备份 | k8s/helm/manifests/kopia-backup.yaml |
CronJob: Vault + ZITADEL PG + Calibre + Gotify |
| ArgoCD 更新 | argocd/applications/kopia.yaml |
include 新增 kopia-backup.yaml |
| 运维手册更新 | docs/runbooks/backup-recovery.md |
自动备份调度文档 |
| 架构文档更新 | docs/architecture/TODO.md |
标记已完成项 |
所有变更通过一次 Git push 即可生效——ArgoCD 在 3 分钟内自动同步到集群。