跳到主要内容

Radicale 部署:把日历和通讯录留在自己的 VPS

固定版本部署私人 CalDAV 和 CardDAV 服务,核对账号隔离、手机客户端与存储条件,完整保存配置、密码哈希和集合并从副本恢复。

先分清同步与备份

手机上新增日程,电脑却看不到;换一部手机,又要重新导入联系人。如果你需要的是几台设备之间的日历与通讯录同步,Radicale 可以提供 CalDAV、CardDAV 服务。浏览器里的管理页面用于建立集合,日常查看和编辑仍交给兼容的日历、联系人客户端。

先决定哪些设备和账号参与同步。删除一条已同步日程,其他设备之后也可能收到删除结果;服务器同步目录不能兼任唯一备份。本文使用固定的 Radicale 3.9.0、独立账号和 owner_only 权限,不配置公开分享、群组共享、外部存储或第三方插件。

为小数据选机器,先看访问与维护成本

个人日程和普通联系人主要占用少量结构化文件,但联系人照片、历史数量、客户端同步频率仍会改变负载。单人起步可把 1 vCPU、1 GiB 内存作为选型预算,再结合系统和其他进程的实际余量调整;这是编辑建议,不是本应用最低配置或性能实测。已有 Linux VPS 有余量时直接复用,不必另外购买。

地区选手机和电脑都能稳定连接的位置。一次打开网页很快,不代表手机在移动网络下持续同步正常;按实际 Wi-Fi、移动网络分别验证,先用月付核对,再考虑长账期。预算还要包括域名、续费、异机备份及维护时间。普通公网网络即可,不需要住宅 IP。

需要新实例时,可比较普通云服务器的持久盘、系统支持与续费条件。雨云 RCS 可作为候选,是否合适取决于你实际访问的地区和订单条件;购买前核对账期、磁盘、流量和备份范围,不把厂商快照当成本文的数据副本。

查看雨云普通云服务器 ↗

注册推荐码:KuZhuJi

新目录、固定镜像与两个持久位置

以下命令面向 Ubuntu 24.04、已安装 Docker Engine 和 Compose 插件的机器,在 root shell 中执行。`/srv/radicale` 必须是新路径;已有安装不要重跑初始化,也不要把现有目录改归另一套服务。官方镜像内的运行用户 UID/GID 为 1000,示例只赋权给新建的专用目录,不使用其他镜像的 PUID、PGID 参数。

sh;使用前请核对本文前提。
set -eu
base=/srv/radicale
test ! -e "$base"
test ! -L "$base"
install -d -m 0700 "$base"
install -d -m 0700 -o 1000 -g 1000 "$base/config" "$base/data"
cd "$base"

将下列内容保存为 `compose.yaml`。镜像固定到 3.9.0 的多架构摘要,主机入口只绑定回环地址;容器内仍监听所有地址,供 Docker 端口映射连接。

yaml;使用前请核对本文前提。
services:
  radicale:
    image: ghcr.io/kozea/radicale:3.9.0@sha256:f68a99907f44b09dc12d81c3a1cdf221e3c55f08ceccfa17dc012991be9fb397
    command: ["-C", "/etc/radicale/config", "--server-hosts", "0.0.0.0:5232"]
    ports:
      - "127.0.0.1:5232:5232"
    volumes:
      - ./config:/etc/radicale:ro
      - ./data:/var/lib/radicale
    restart: unless-stopped

保存 `config/config`:

ini;使用前请核对本文前提。
[auth]
type = htpasswd
htpasswd_filename = /etc/radicale/users
htpasswd_encryption = autodetect

[rights]
type = owner_only

[storage]
type = multifilesystem
filesystem_folder = /var/lib/radicale/collections

[sharing]
type = none

`owner_only` 允许已认证用户读写自己 `/用户名/` 下的集合。不要为解决“看不到别人日历”直接改成 `authenticated`,后者允许已认证账号读写全部集合;需要共享时,应另行设计授权并用两个账号验证。

先验证密码与账号隔离,再开放 HTTPS

在主机安装 `apache2-utils`,交互创建 bcrypt 密码文件。首次建文件才使用 `-c`;以后加账号或改密码去掉 `-c`,否则可能覆盖原来的账号。用户名用简单小写字母,例如 alice、bob,不与另一人共用账号。

sh;使用前请核对本文前提。
apt-get update
apt-get install -y apache2-utils
cd /srv/radicale
test ! -e config/users
test ! -L config/users
htpasswd -B -c config/users alice
htpasswd -B config/users bob
chown 1000:1000 config/config config/users
chmod 0600 config/config config/users compose.yaml
docker compose config --quiet
docker compose up -d
docker compose logs --tail=80 radicale

在自己的电脑建立隧道,保持 SSH 窗口开启,然后访问 `http://127.0.0.1:15232/.web/`。这里的 HTTP 流量经过 SSH 隧道,不能把它换成公网裸 HTTP。

sh;使用前请核对本文前提。
ssh -N -L 15232:127.0.0.1:5232 user@your-server

用 alice 建立一个测试日历和一个地址簿。可以从管理页面复制集合地址,但日常操作用客户端;确认客户端保存的是自己的账号目录,不是另一人的集合。用 bob 登录,应看不到 alice 的私人集合。再退出账号,用匿名窗口访问 alice 的集合地址,确认没有日程或联系人泄露。

正式接入域名时,使用独立子域名,把下文的 `dav.your-domain.tld` 替换成你实际控制的域名。若 Caddy 直接运行在同一宿主机,可用下列站点配置;若 Caddy 在另一容器中,`127.0.0.1` 指向它自己,需另行设计容器网络,不能照抄地址。

caddyfile;使用前请核对本文前提。
dav.your-domain.tld {
    reverse_proxy 127.0.0.1:5232
}

先核对 DNS、80/443 和证书签发条件,在现有 Caddy 配置中加入这个独立站点并验证后重载。不要覆盖其他站点,也不要开放 5232 到公网。完整代理 DAV 请求方法及 Authorization,不用普通静态网站的 GET/POST 白名单截断 PROPFIND、REPORT、PUT、DELETE 等请求。反向代理不代替 Radicale 的账号认证。

在两台设备核对事件、联系人和删除

向兼容 CalDAV/CardDAV 的客户端填写 `https://dav.your-domain.tld/`、账号和密码。若自动发现失败,使用管理页提供的准确集合 URL,不靠随意添加路径解决。Android 可选 DAVx⁵ 等兼容客户端,Thunderbird 等桌面客户端也有对应支持;客户端的版本、费用和系统权限要自行核对,服务器能启动不代表每款客户端都已兼容。

先用测试集合建一条有明确时区的日程、一条重复日程和一位虚构联系人。从设备 A 新建、设备 B 读取,再从 B 修改、A 读取。核对时区、重复规则、姓名、电话号码和联系人照片;提醒通知通常由客户端和系统权限决定,Radicale 不承诺像完整协作套件那样发邮件提醒。

最后删除一条测试日程,观察另一端同步后的结果。这能帮助你理解误删会沿同步传播。正式导入前先导出原客户端的日历和联系人;若已有重复项,先在小集合中处理,不让多台设备同时对整本地址簿批量改写。

排障时先看客户端保存的 URL 与服务器日志。401 先核账号密码,403 核账号对应的集合与 rights,502 核代理能否连接正确端口;时间显示异常再核客户端时区与原始事件。不要为了排障关闭认证或把整个存储目录改为所有人可读写。

把账号、配置和集合一起冷备

数据在 `data`,账号密码哈希、认证和权限规则在 `config`。仅从界面导出一个 ICS 或 VCF 不包含整套服务。默认文件存储有锁,不应在正在写入时随手复制部分文件就称一致备份;这里采用暂停本服务、确认正常退出后打包的方案,期间客户端暂不能同步。

以下脚本只适用于本目录的单个、原本正在运行的服务。它拒绝 OOM、强杀或未知异常退出,归档失败也会尝试启动原服务;恢复服务启动仍需看日志和实际登录。备份文件含私人资料与密码哈希,生成后转移至受保护的异机存储,异机副本另做加密和保留期限管理。

sh;使用前请核对本文前提。
cd /srv/radicale
bash <<'SH'
set -eu
umask 077
cid=$(docker compose ps -q radicale)
test -n "$cid"
test "$(docker inspect -f '{{.State.Running}}' "$cid")" = true
restart=0
finish() {
  rc=$?
  trap - EXIT
  if [ "$restart" = 1 ]; then
    docker compose start radicale || rc=1
  fi
  exit "$rc"
}
trap finish EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
restart=1
docker compose stop -t 45 radicale
test "$(docker inspect -f '{{.State.Running}}' "$cid")" = false
test "$(docker inspect -f '{{.State.OOMKilled}}' "$cid")" = false
code=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
case "$code" in 0|143) ;; *) exit 1 ;; esac
mkdir -p backups
dest="backups/$(date -u +%Y%m%dT%H%M%SZ)-$$"
mkdir -m 0700 "$dest"
tar -czf "$dest/radicale.tar.gz" config data compose.yaml
tar -tzf "$dest/radicale.tar.gz" >/dev/null
(cd "$dest" && sha256sum radicale.tar.gz > SHA256SUMS)
printf '%s\n' "$dest"
SH
docker compose logs --tail=50 radicale

归档命令没有证明客户端状态正确。备份后再用一个测试账号读取日历,并定期从副本真正恢复;把备份放在原 VPS 的同一块盘上,只能应对一部分误操作。

从新目录恢复,升级时保留成套旧版本

选一份可信的自有归档,先在其目录执行 `sha256sum -c SHA256SUMS`。以下示例把 `BACKUP` 改成该归档的绝对路径,在 root shell 操作。目标必须是全新目录;不要覆盖正在同步的原实例。

sh;使用前请核对本文前提。
set -eu
BACKUP=/srv/radicale/backups/REPLACE_WITH_BACKUP/radicale.tar.gz
target=/srv/radicale-restore
test -f "$BACKUP"
test ! -e "$target"
test ! -L "$target"
install -d -m 0700 "$target"
tar --numeric-owner -xzf "$BACKUP" -C "$target"
cd "$target"
python3 - <<'PY'
from pathlib import Path
p = Path('compose.yaml')
s = p.read_text()
old = '127.0.0.1:5232:5232'
assert s.count(old) == 1
p.write_text(s.replace(old, '127.0.0.1:5233:5232'))
PY
docker compose -p radicale-restore config --quiet
docker compose -p radicale-restore up -d

从电脑另建隧道 `ssh -N -L 15233:127.0.0.1:5233 user@your-server`。用新的测试客户端配置访问恢复副本,核对原账号、事件、联系人以及匿名和跨账号拒绝;不要直接让所有正式客户端连接两套实例。确认后停止副本 `docker compose -p radicale-restore stop`,保留归档与结果。

升级前阅读目标固定版本说明,完成一次上述备份,再更换镜像版本和摘要。若新版已经改写数据,直接换回旧镜像不能保证兼容;恢复应同时匹配旧镜像、配置与备份数据,在独立目录验证后再安排切换。日历的可用性最终取决于客户端往返、权限和恢复结果,不只是容器显示 Running。

本文关联资源