VPS自托管日历:Radicale配置CalDAV同步
无需Google账号,在VPS上用Radicale搭建CalDAV日历服务器,配置TLS、服务发现、用户隔离与备份,并完成手机和电脑客户端同步。
构建内容
自托管日历是在您控制的 VPS 上运行的一个 CalDAV 服务器。它位于 TLS 后面,并为每个人设置一个登录账号。您手中的手机、桌上的笔记本电脑,以及您伴侣的笔记本电脑,都会显示相同的事件。中间不需要 Google 账号。
这项工作不同于自托管预约页面。预约页面面向陌生人:它发布您的空闲时间,让他人选择其中一个时段。日历服务器面向您自己的设备:它存储事件,并让所有客户端保持一致。很多人会同时运行这两者,此时预约工具会从您将在这里构建的 CalDAV 服务器读取可用时间。
安装过程很小。Radicale 是一个 Python 软件包,配置大约只有十行。决定这套配置能否在第一个月后继续稳定运行的因素,是 TLS、服务发现、按用户划分的集合以及备份。下面将重点介绍这些内容。
什么是 CalDAV,它为什么重要?
CalDAV 是通过 HTTP 进行日历同步的协议。RFC 4791 将其定义为 WebDAV 的扩展。WebDAV(基于 Web 的分布式创作与版本控制)是一组在 RFC 4918 中定义的额外 HTTP 方法。日历是一种集合,其行为类似目录。一个事件就是其中的一个文件,使用 iCalendar 文本格式(RFC 5545)编写,与邮件中的 .ics 附件使用相同的格式。
客户端使用普通 HTTP,并增加了少数方法。PROPFIND 用于查询当前位置的内容及其属性。REPORT 用于请求经过筛选的部分内容,例如指定日期范围内的所有事件。PUT 用于写入一个事件,DELETE 用于删除该事件。每个事件都包含一行 UID,设备通过该标识符确认它们查看的是同一个事件,而不是副本。
可移植性是 CalDAV 的主要收益,也是采用它的根本原因。iOS、macOS、Thunderbird、Evolution,以及通过 DAVx⁵ 接入的 Android 都支持 CalDAV。您的数据不会绑定到当前选择的服务器。将文件迁移到其他 CalDAV 服务器,把客户端指向新的主机名,其他内容无需更改。
CardDAV 也采用相同的方式。它用于联系人,由 RFC 6352 定义,存储的是 vCard 文件,而不是事件。下面的每台服务器都会通过同一账户同时提供这两种协议,因此日历正常工作后,地址簿只需勾选启用即可。
应运行哪种 CalDAV 服务器?
Radicale 是能正常工作的最小方案。它使用 Python,不需要数据库,数据存储在由普通文件组成的目录中。本指南选择它,是因为家庭日历无需更多组件,而且凌晨三点也很少会出问题。
Baikal 提供 Web 管理面板。它基于 PHP 和 sabre/dav 库运行,并将用户和日历存储在 SQLite 或 MySQL 中。您可以在浏览器中添加用户,而不必使用命令行。若账户经常变动,请选择它。
如果日历只是多个功能之一,Nextcloud 更合适。您可以获得日历、联系人、文件和移动应用,但需要 PHP-FPM、数据库和后台任务运行器。如果这对于您的实际需求来说过于复杂,请参阅更轻量的 Nextcloud 替代方案;如果您安装 Nextcloud 的另一项主要需求是文件同步,请参阅自托管文件同步。
DAViCal 是长期使用的 PostgreSQL 方案。只有在您已经运行 PostgreSQL,并希望将日历数据存储在其中时,才值得考虑它。
在 Ubuntu 24.04 上安装 Radicale
截至 2026 年 8 月,Radicale 3.5.10 是当前版本。将其安装到专用虚拟环境中。
sudo apt update
sudo apt install -y python3-venv apache2-utils nginx
sudo useradd --system --user-group --home-dir / --shell /usr/sbin/nologin radicale
sudo install -d -o radicale -g radicale -m 750 /var/lib/radicale/collections
sudo install -d -m 750 -o root -g radicale /etc/radicale
sudo python3 -m venv /opt/radicale/venv
sudo /opt/radicale/venv/bin/pip install --upgrade radicale虚拟环境不是格式选择。sudo pip install radicale 直接安装到系统 Python 会因 error: externally-managed-environment 而失败,因为 Ubuntu 将其 Python 标记为由 apt 管理,防止 pip 覆盖软件包文件。
写入 /etc/radicale/config:
[server]
hosts = 127.0.0.1:5232
[auth]
type = htpasswd
htpasswd_filename = /etc/radicale/users
htpasswd_encryption = autodetect
[storage]
filesystem_folder = /var/lib/radicale/collectionshosts 会有意绑定到 loopback。nginx 负责终止 TLS,并将请求转发到该端口,因此 Radicale 不会直接暴露在互联网中。上游示例中的 0.0.0.0:5232 会发布一个接受密码的未加密服务,这是这里最需要避免的错误。
现在创建账户。-5 选择 SHA-512 crypt,Radicale 可通过 htpasswd_encryption = autodetect 读取它,无需额外模块:
sudo htpasswd -5 -c /etc/radicale/users you
sudo htpasswd -5 /etc/radicale/users partner
sudo chown root:radicale /etc/radicale/users
sudo chmod 640 /etc/radicale/users-c 会创建文件,并截断其中原有的内容。只对第一个用户使用它。数月后再次运行 htpasswd -5 -c 会删除首次创建之后添加的所有账户。其表现为一个用户可以正常同步,而其他用户不断收到密码提示。Bcrypt 同样可用,但需要额外安装 radicale[bcrypt]。
创建 /etc/systemd/system/radicale.service,内容根据 Radicale 文档中的单元文件调整:
[Unit]
Description=CalDAV and CardDAV server
After=network.target
Requires=network.target
[Service]
ExecStart=/opt/radicale/venv/bin/python -m radicale
Restart=on-failure
User=radicale
UMask=0027
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
NoNewPrivileges=true
ReadWritePaths=/var/lib/radicale/
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now radicale
curl -i http://127.0.0.1:5232/正常结果是 401 Unauthorized,并带有 WWW-Authenticate 标头:服务正在监听,且身份验证已启用。Connection refused 表示服务从未启动,journalctl -u radicale -n 50 会指出它拒绝的选项。ProtectSystem=strict 会将文件系统以只读方式挂载给此服务,因此 ReadWritePaths=/var/lib/radicale/ 是服务能够保存事件的必要配置行。删除该行后,读取仍可正常工作,但所有写入都会失败。
TLS 不是可选项,因为客户端会拒绝明文连接
CalDAV 使用 HTTP Basic 进行身份验证,每个请求都会发送经过 user:password Base64 编码的凭据。Base64 是编码方式,不是加密方式。通过普通 HTTP 连接时,手机与服务器之间的每个网络都会收到您的密码;每次同步都是如此。
客户端会强制执行这一要求。Radicale 文档指出,macOS Calendar.app 可能会静默拒绝通过不安全的 HTTP 发送凭据,iOS 的行为也相同。账户看似配置完成,却始终无法同步,而且没有可查看的错误信息。
先为 cal.example.com 设置指向 VPS 的 A 记录,因为证书颁发机构会检查该记录。然后创建 /etc/nginx/sites-available/cal.example.com:
server {
listen 80;
server_name cal.example.com;
location / {
proxy_pass http://localhost:5232/;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass_header Authorization;
}
location = /.well-known/caldav { return 301 https://$host/; }
location = /.well-known/carddav { return 301 https://$host/; }
}这四行代理请求头来自 Radicale 文档。请保持原样。
sudo ln -s /etc/nginx/sites-available/cal.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d cal.example.com
curl -i -u you https://cal.example.com/nginx -t 会输出 syntax is ok 和 test is successful。只有确认输出这些内容后才重新加载配置,因为配置文件损坏时执行 reload 会继续使用旧配置,并将错误隐藏到下一次重启。Certbot 会直接修改站点文件:安装证书,将配置块切换到 443 端口,并添加从 80 端口到 HTTPS 的重定向。最后的 curl 命令会提示输入密码,并应返回 200,这是 Radicale 自带的 Web 界面。出现 502 Bad Gateway 表示 nginx 正在运行,但 Radicale 没有监听 5232 端口。
为什么在手机上添加账户会失败?
原因是服务发现。RFC 6764 说明了客户端如何将主机名转换为日历 URL。客户端会查找 _caldavs._tcp SRV 记录,然后请求 https://cal.example.com/.well-known/caldav,并期望该请求重定向到 DAV 根路径。接着,客户端会请求 current-user-principal,再请求该主体的 calendar-home-set,之后才能看到您的日历。手机只提供一个服务器字段,因此每一步都必须能够自动完成。
curl -sI https://cal.example.com/.well-known/caldav正常情况下,响应应为 HTTP/2 301,并包含 location: https://cal.example.com/ 标头。如果此处返回 404,iOS 就会提示无法验证账户信息,而同一网络中的 Thunderbird 却可以正常工作。原因是 Thunderbird 使用您输入的完整 URL,因此不需要依赖重定向。
重定向目标取决于服务器。Radicale 在站点根路径提供服务时,会重定向到 /。Baikal 附带的示例规则会以 308 状态码重定向到 /dav.php。Nextcloud 会重定向到 /remote.php/dav/。
创建日历,并与伴侣共享一个日历
许多客户端无法创建日历,只能订阅日历。请在浏览器中打开 https://cal.example.com/,以 you 登录,然后在那里创建日历。日历会存储在磁盘上的 /var/lib/radicale/collections/collection-root/you/ 下,文件夹名称使用自动生成的标识符。
Radicale 的默认权限后端是 owner_only:经过身份验证的账户可以在 /USERNAME/ 下读写自己的集合,不能访问其他内容。对于大多数家庭,这是正确的设置。共享日历最简单的方法是创建第三个账户。使用 htpasswd 创建 household,以该账户登录并创建共享日历,然后在每台设备上将其作为第二个 CalDAV 账户添加。所有客户端都支持这种方式,包括 iOS,因为日历位于该账户自己的主目录中。
如果需要更精细的控制,请切换到基于规则的权限。将以下内容添加到 /etc/radicale/config:
[rights]
type = from_file
file = /etc/radicale/rights然后根据 Radicale 文档中的示例配置 /etc/radicale/rights:
[root]
user: .+
collection:
permissions: R
[principal]
user: .+
collection: {user}
permissions: RW
[own-calendars]
user: .+
collection: {user}/[^/]+
permissions: rw
[shared-household]
user: you|partner
collection: you/2f0a9c1e-1f4c-4c2b-9a1b-0d2f7a5c9e11
permissions: rw大写字母和小写字母表示不同的权限。R 和 W 读写不是日历或地址簿的集合,也就是主体文件夹。r 和 w 读写日历本身。请将该标识符替换为上方存储路径中日历实际使用的文件夹名称。
需要明确一个限制:只读取日历主目录集合的客户端不会显示位于其他用户路径下的日历,因为发现过程不会遍历该路径。Thunderbird 和 DAVx⁵ 可以通过完整 URL 添加该日历。iOS 无法这样操作,因此共享账户模式是始终有效的方案。
配置客户端,因为自托管日历往往在这里失败
iPhone 和 iPad。 打开“设置”,然后依次进入“日历”(在较新的 iOS 版本中位于“App”下)、“日历账户”、“添加账户”、“其他”和“添加 CalDAV 账户”。服务器填写 cal.example.com,然后填写用户名和密码。“描述”仅用于设置标签。如果无法保存,请重新打开该账户:高级视图中会显示“使用 SSL”、端口和完整账户 URL;粘贴 URL 可完全跳过发现过程。
Android。 系统没有内置 CalDAV 客户端。请从 F-Droid 或 Google Play 安装 DAVx⁵,使用基 URL https://cal.example.com/ 和用户名添加账户,然后勾选所需的日历。DAVx⁵ 会写入 Android 日历提供程序,因此事件会显示在您现有的任意日历应用中。
Thunderbird。 依次选择“新建日历”、“在网络上”,然后输入用户名和位置 https://cal.example.com/。它会列出发现的内容,并询问要添加哪些日历。
macOS。 打开“系统设置”、“互联网账户”、“添加其他账户”和“CalDAV”,将“账户类型”设置为“手动”,然后填写相同的用户名、密码和服务器地址。
CalDAV 是一种轮询协议。该规范不支持推送,因此您在笔记本电脑上添加的事件会在下一次同步时到达手机,而不是立即同步。请在每个客户端中设置一个您可以接受的同步间隔,并注意,手机上的同步间隔越短,电池消耗越大。
备份仅由文件组成的存储
在 Radicale 中,日历是由 .ics 文件组成的目录,每个事件对应一个文件,每个集合还包含一个小型属性文件。任何能够复制目录的工具都可以完成备份。您可以使用 less 打开备份,确认其中包含真实事件。这是相对于无法直接读取的数据库转储的一项实际优势。
sudo systemctl stop radicale
sudo tar czf /root/radicale-$(date +%F).tar.gz -C /var/lib/radicale collections
sudo systemctl start radicale在归档期间停止服务几秒钟,避免读取文件时客户端正处于写入过程中。完成后将归档复制到服务器之外,因为同一 VPS 上的备份无法应对您正在预防的故障。恢复时反向执行:解压、sudo chown -R radicale:radicale /var/lib/radicale/collections,然后启动服务。每个客户端还会保存日历的本地副本,因此自故障发生后尚未同步的笔记本电脑,也可作为数据的第二份副本。
Baikal 和 Nextcloud 的适用场景
Baikal 0.12.1 于 5 August 2026 发布,需要 PHP 8.2 或更高版本。将其解压到网站根目录之外,并且只对外提供其 html 目录:
sudo apt install -y php-fpm php-sqlite3 php-xml php-mbstring php-curl unzip
cd /tmp
curl -LO https://github.com/sabre-io/Baikal/releases/download/0.12.1/baikal-0.12.1.zip
sudo unzip -q baikal-0.12.1.zip -d /srv
sudo chown -R www-data:www-data /srv/baikal/Specific /srv/baikal/configWeb 服务器只会写入这两个目录,因此不需要让其他目录可写。在 nginx server block 中,Baikal 专用部分如下:
root /srv/baikal/html;
index index.php;
location ~ /(\.ht|Core|Specific|config) { deny all; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location = /.well-known/caldav { return 308 /dav.php; }
location = /.well-known/carddav { return 308 /dav.php; }重新加载 nginx,然后在浏览器中打开站点。设置向导会创建管理员账户和 SQLite 数据库。客户端配置方式与 Radicale 完全相同,服务器地址使用 https://cal.example.com/,因为 well-known 规则会将发现请求转发到 /dav.php。
只有在您还希望通过同一个登录账户使用文件和手机应用时,Nextcloud 才值得增加这些复杂度。它的 DAV 根目录是 /remote.php/dav/,并且使用相同的发现规则。对于上述任一服务,在容器中运行都可以避免在主机上管理 PHP 版本:VPS 上的 Docker Compose介绍 compose 文件及其前置反向代理,2026 年值得自行托管的服务则适合帮助您决定是否要进一步采用这种方案。
故障模式及您将看到的字符串
每次同步都返回 401。 密码文件中的账户可能被第二个 htpasswd -c 删除,也可能是 radicale 用户无法读取该文件。使用 sudo -u radicale cat /etc/radicale/users 检查;如果这里显示 permission denied,原因就确定了。将组设置为 radicale,并将文件权限设置为 640。Radicale 默认还会在每次登录失败后等待一秒,因此使用过期密码的客户端看起来像是响应缓慢,而不是被拒绝。
nginx 对 PROPFIND 返回 405。 该 URL 被作为静态文件提供,因此 WebDAV 方法没有到达 Radicale。直接测试端点:
curl -u you -X PROPFIND -H "Depth: 0" -i https://cal.example.com/you/正常的 DAV 集合会返回 207 Multi-Status。其他任何结果都表示请求在 Web 服务器处停止了。
手机无法验证账户,但浏览器可以正常访问。 通常有两个原因。第一,缺少 well-known 重定向,可使用上面的 curl 命令测试。第二,证书链不完整。浏览器会自动获取缺失的中间证书,因此可能掩盖此问题;iOS 不会这样处理。通过 shell 检查:
openssl s_client -connect cal.example.com:443 -servername cal.example.com </dev/null查找 Verify return code: 0 (ok)。如果失败,说明 nginx 配置指向了 cert.pem,而正确目标应为 fullchain.pem。
导入后出现重复事件。 每个事件都包含一个 UID,客户端将其视为事件标识。使用会重新生成标识的工具两次导入同一个文件,就会产生两个永远不会自动合并的事件。在一台设备上删除多余副本,再让删除操作同步出去。
重启后所有功能都停止。 该服务是手动启动的。sudo systemctl is-enabled radicale 会输出 disabled,而 sudo systemctl enable --now radicale 可永久修复此问题。
FAQ
自托管 CalDAV 服务器确实需要 TLS 吗?
需要。CalDAV 使用 HTTP Basic 进行身份验证,因此密码会在每个请求中以 base64 编码传输,而 base64 很容易还原。客户端也会强制执行这一点:macOS Calendar.app 可能会静默拒绝通过不安全的 HTTP 发送凭据,iOS 的行为也相同。因此,账户看似保存成功,却始终无法同步。sudo certbot --nginx -d cal.example.com 是全部工作。
为什么 Thunderbird 可以添加账户,但手机失败?
Thunderbird 使用您输入的完整 URL。手机只有一个服务器字段,因此会遵循 RFC 6764 发现流程:它请求 https://cal.example.com/.well-known/caldav,并期望服务器重定向到 DAV 根路径。没有此重定向时,手机会收到 404,并提示无法验证账户。在 nginx 中添加 location = /.well-known/caldav { return 301 https://$host/; },然后使用 curl -sI https://cal.example.com/.well-known/caldav 确认返回 301 和 location 响应头。
两个人可以共享一个日历吗?
可以,可靠的方法是使用共享登录。使用 htpasswd 创建第三个账户,将共享日历放在该账户下,然后在每台设备上将其添加为第二个 CalDAV 账户。Radicale 的 rights 文件也可以授予指定用户对另一用户路径下某个集合的读写权限,但只读取自己 calendar home set 的客户端永远不会显示该日历。因此,这种方式更适合 Thunderbird 和 DAVx⁵,而不适合 iOS。
VPS 发生故障时,我的事件会怎样?
Radicale 使用纯文本存储:每个事件对应一个 .ics 文件,存放在 /var/lib/radicale/collections/collection-root/ 下。您可以使用 tar 备份,并使用 less 读取。恢复步骤是解压、执行 chown -R radicale:radicale,然后启动服务。每个已同步的客户端也会保留本地副本,因此故障前已完成同步的笔记本电脑中会保存一份完整的日历副本。
CalDAV 服务器也会同步我的联系人吗?
联系人使用 CardDAV。这是 RFC 6352 定义的同级协议,存储的是 vCard 文件,而不是事件。Radicale、Baikal 和 Nextcloud 都可以使用同一账户和同一主机名提供该服务。在 Android 上,DAVx⁵ 会从一个账户同步日历和联系人。在 iOS 上,您需要使用相同凭据添加一个类型为 CardDAV 的第二个账户。因此,/.well-known/carddav 重定向应与 CalDAV 重定向一起写入 nginx 配置。