SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-16

VPS 自架 CalDAV 行事曆:Radicale 設定教學

不用 Google 帳戶,使用 Radicale 在 VPS 建立 CalDAV 伺服器,涵蓋 TLS、服務探索、帳號、備份,以及手機與筆電用戶端設定。

建置內容

自架行事曆是在你管理的 VPS 上執行一個 CalDAV 伺服器,透過 TLS 保護,並為每位使用者建立一組登入資料。你口袋中的手機、桌上的筆記型電腦,以及伴侶的筆記型電腦,都會顯示相同的事件。過程中不需要使用 Google 帳戶。

這與自架預約頁面的用途不同。預約頁面是提供陌生人使用:公開你的可用時段,讓對方選取其中一個。行事曆伺服器則供你自己的裝置使用:儲存事件,並讓所有用戶端保持同步。許多人會同時使用兩者,讓預約工具從你在此建置的 CalDAV 伺服器讀取可用時間。

安裝內容很精簡。Radicale 是一個 Python 套件,設定檔約十行。決定這套環境能否穩定運作至少一個月的因素,是 TLS、服務探索、每位使用者各自的集合,以及備份。以下內容將主要說明這些項目。

什麼是 CalDAV?為什麼重要?

CalDAV 是透過 HTTP 進行行事曆同步的協定。RFC 4791 將它定義為 WebDAV 的擴充功能。WebDAV 是一組在 RFC 4918 定義的額外 HTTP 方法,用於網路分散式製作與版本控制。行事曆是一個集合,行為類似目錄。每個事件都是其中的一個檔案,使用 iCalendar 文字格式(RFC 5545)儲存,格式與郵件中的 .ics 附件相同。

用戶端使用一般 HTTP,並搭配幾個額外方法。PROPFIND 用來查詢目前有哪些內容及其屬性。REPORT 用來取得篩選後的部分內容,例如指定日期範圍內的所有事件。PUT 用來寫入一個事件,DELETE 用來移除事件。每個事件都包含 UID 行;兩部裝置透過這個識別碼確認彼此查看的是同一個事件,而不是副本。

CalDAV 的主要價值在於可攜性,這也是採用它的完整理由。iOS、macOS、Thunderbird、Evolution,以及透過 DAVx⁵ 使用 CalDAV 的 Android 都支援 CalDAV。資料不會綁定在目前選用的伺服器上。將檔案移至其他 CalDAV 伺服器,再將用戶端指向新的主機名稱即可,其他設定無須變更。

CardDAV 也採用相同的方式。它用於同步聯絡人,定義於 RFC 6352,並以 vCard 檔案取代事件。以下每台伺服器都會使用同一個帳號提供這兩種協定,因此行事曆運作後,只需啟用通訊錄選項即可。

應該執行哪個 CalDAV 伺服器?

Radicale 是能運作的最小方案。它使用 Python,不需要資料庫,資料儲存方式是由純文字檔組成的資料夾。本指南採用它,因為家庭行事曆不需要更多功能,而且凌晨 3 點時出錯的地方很少。

Baikal 是提供網頁管理面板的方案。它使用 PHP 和 sabre/dav library,將使用者與行事曆儲存在 SQLite 或 MySQL 中,讓你可以直接在瀏覽器中新增人員,而不必使用命令列。若帳號經常新增或移除,請選擇它。

Nextcloud 適合需要將行事曆作為多項功能之一的情境。你會取得行事曆、聯絡人、檔案和行動應用程式,但代價是需要 PHP-FPM、資料庫和背景工作執行器。如果這些元件對你的實際需求而言過於複雜,較輕量的 Nextcloud 替代方案可說明取捨,而自架檔案同步則涵蓋人們安裝 Nextcloud 的另一項主要用途。

DAViCal 是長期使用 PostgreSQL 的方案。只有在你已經執行 PostgreSQL,且希望將行事曆資料儲存在其中時,才值得考慮。

在 Ubuntu 24.04 上安裝 Radicale

截至 2026 年 8 月,Radicale 3.5.10 是目前版本。請將它安裝在專用的 virtual environment 中。

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

使用 virtual environment 並非格式偏好。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/collections

hosts 刻意繫結至 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:

[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.target
sudo 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 是編碼,不是加密。若使用純 HTTP,手機與伺服器之間的每個網路節點都能取得密碼,而且會在每次同步時持續傳送。

用戶端會強制執行這項要求。Radicale 文件指出,macOS Calendar.app 可能會靜默拒絕透過未加密的 HTTP 傳送認證資訊,iOS 的行為也相同。帳號看似已完成設定,卻完全不會同步,而且沒有可供查看的錯誤訊息。

先將 cal.example.com 的 A 記錄指向 VPS,因為憑證授權單位會檢查該記錄。接著建立 /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/; }
}

這 4 行代理標頭來自 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 oktest is successful。只有在出現這些輸出後才重新載入,因為設定檔損壞時重新載入會讓舊設定持續執行,直到下次重新啟動才暴露錯誤。Certbot 會直接修改網站設定檔:安裝憑證、將區塊切換至 443 埠,並加入從 80 埠重新導向的設定。最後執行的 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,再要求該 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

接著執行 /etc/radicale/rights,內容可參考 Radicale 文件中的範例:

[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

大寫字母與小寫字母代表不同權限。RW 可讀寫不是行事曆或通訊錄的集合,也就是 principal 資料夾。rw 可讀寫行事曆本身。請將該識別碼替換為上述儲存路徑中,實際行事曆資料夾的名稱。

有一項限制需要注意:只讀取行事曆 home set 的用戶端,不會顯示位於其他使用者路徑下的行事曆,因為探索程序不會進入該路徑。Thunderbird 和 DAVx⁵ 可透過完整 URL 新增該行事曆。iOS 無法使用這種方式,因此共用帳戶模式是最穩定且適用範圍最廣的做法。

設定用戶端,因為自架行事曆通常會在這個環節失效

iPhone 和 iPad。 開啟 Settings,接著依序選取 Calendar(近期 iOS 版本中位於 Apps 下)、Calendar Accounts、Add Account、Other、Add CalDAV Account。Server 填入 cal.example.com,再輸入使用者名稱和密碼。Description 只是一個標籤。如果無法儲存,請重新開啟該帳戶:進階檢視會顯示 Use SSL、連接埠和完整帳戶 URL;貼上 URL 即可完全略過探索程序。

Android。 系統沒有內建 CalDAV 用戶端。請從 F-Droid 或 Google Play 安裝 DAVx⁵,使用基礎 URL https://cal.example.com/ 和使用者名稱新增帳戶,然後勾選要使用的行事曆。DAVx⁵ 會寫入 Android 行事曆提供者,因此事件會顯示在你目前使用的行事曆應用程式中。

Thunderbird。 依序選取 New Calendar、On the Network,然後輸入使用者名稱和位置 https://cal.example.com/。Thunderbird 會列出找到的項目,並詢問要新增哪些行事曆。

macOS。 依序選取 System Settings、Internet Accounts、Add Other Account、CalDAV,將 Account Type 設為 Manual,然後輸入相同的使用者名稱、密碼和伺服器位址。

CalDAV 是輪詢協定。規格沒有 push 機制,因此你在筆電上新增的事件會在下一次同步時抵達手機,而不是在同一秒出現。請在各個用戶端設定可接受的同步間隔,並記住,手機使用較短的同步間隔會消耗更多電力。

僅備份檔案組成的資料

在 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/config

這兩個目錄是網頁伺服器唯一需要寫入的目錄,因此其他位置都不需要設為可寫入。在 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 預設也會在每次登入失敗後等待 1 秒,因此密碼過期的用戶端看起來會變慢,而不是直接遭到拒絕。

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 很容易反解碼。用戶端也會強制要求 TLS: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,並回報無法驗證帳戶。將 location = /.well-known/caldav { return 301 https://$host/; } 加入 nginx,然後使用 curl -sI https://cal.example.com/.well-known/caldav 確認回應為 301,且包含 location 標頭。

兩個人可以共用同一個行事曆嗎?

可以。可靠的方式是使用共用登入帳戶。使用 htpasswd 建立第三個帳戶,將共用行事曆放在該帳戶下,然後在每台裝置上將它新增為第二個 CalDAV 帳戶。Radicale 的 rights 檔案也可以授予指定使用者對另一名使用者路徑下某個集合的讀寫權限,但只讀取自己 calendar home set 的用戶端永遠不會顯示該集合。因此,這種方式適合 Thunderbird 和 DAVx⁵,不適合 iOS。

VPS 故障時,我的事件會怎樣?

Radicale 使用純文字儲存資料:/var/lib/radicale/collections/collection-root/ 下每個事件各有一個 .ics 檔案。你可以使用 tar 備份,並使用 less 讀取。還原時,先解壓縮,再執行 chown -R radicale:radicale,最後啟動服務。每個已同步的用戶端也會保留本機副本,因此在故障前已完成同步的筆記型電腦,會保有完整的第二份行事曆副本。

CalDAV 伺服器也會同步我的聯絡人嗎?

聯絡人使用 CardDAV。這是 RFC 6352 定義的姊妹協定,儲存的是 vCard 檔案,而不是事件。Radicale、Baikal 和 Nextcloud 都能使用相同帳戶及相同主機名稱提供這項服務。在 Android 上,DAVx⁵ 可從同一個帳戶同步行事曆和聯絡人。在 iOS 上,你必須新增一個類型為 CardDAV 且使用相同認證資訊的第二個帳戶,因此 /.well-known/carddav 重新導向應與 CalDAV 重新導向一併放在 nginx 設定中。

#caldav#calendar#radicale#self-hosting#sync