SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

VPSでCalDAVサーバーを構築して予定を同期する方法

Googleを使わず、スマートフォンとノートパソコンの予定を同期できます。RadicaleをVPSに導入し、TLS、検出、ユーザー設定、クライアント接続まで解説します。

構築するもの

自己ホスト型カレンダーは、管理下の VPS 上で動作する 1 台の CalDAV サーバーです。TLS の背後に置き、ユーザーごとに 1 つのログインを用意します。手元のスマートフォンと机上のノートパソコンに同じ予定が表示され、パートナーのノートパソコンにも同じ予定が表示されます。Google アカウントを介在させる必要はありません。

これは 自己ホスト型の予約ページ とは異なる用途です。予約ページは見知らぬ人向けです。空いている時間帯を公開し、そのうちの 1 つを予約できるようにします。カレンダーサーバーは自分のデバイス向けです。予定を保存し、すべてのクライアントの内容を一致させます。両方を運用するケースもよくあります。その場合、予約ツールはここで構築する CalDAV サーバーから空き状況を読み取ります。

インストールは小規模です。Radicale は 1 つの Python パッケージと約 10 行の設定で構成できます。構成を最初の 1 か月間安定して運用できるかどうかを左右するのは、TLS、検出、ユーザーごとのコレクション、バックアップです。以下では、これらを中心に説明します。

CalDAV とは何か、なぜ重要なのか

CalDAV は HTTP を使用したカレンダー同期のプロトコルです。RFC 4791 で、WebDAV(RFC 4918 で定義された追加の HTTP メソッド群である、Web 分散オーサリングとバージョン管理)の拡張として定義されています。カレンダーはコレクションであり、ディレクトリのように動作します。1 件のイベントはその中の 1 つのファイルで、iCalendar テキスト形式(RFC 5545)で記述されます。これは、メールに添付される .ics と同じ形式です。

クライアントは、いくつかのメソッドを追加した通常の HTTP を使用します。PROPFIND は、何が存在し、どのようなプロパティを持つかを問い合わせます。REPORT は、日付範囲内のすべてのイベントなど、条件で絞り込んだ範囲を取得します。PUT は 1 件のイベントを書き込み、DELETE はそのイベントを削除します。すべてのイベントには UID 行が含まれます。この識別子によって、2 台のデバイスは同じイベントを参照しているのか、コピーを参照しているのかを判断できます。

利点は移植性です。CalDAV を利用する理由は、まさにこの点にあります。iOS、macOS、Thunderbird、Evolution、そして DAVx⁵ 経由の Android は、すべて CalDAV に対応しています。データが、その時点で選択したサーバーに縛られることはありません。ファイルを別の CalDAV サーバーへ移し、クライアントの接続先を新しいホスト名に変更すれば、それ以外の変更は必要ありません。

CardDAV も同様に利用できます。これは連絡先向けの同じ仕組みで、RFC 6352 で定義され、イベントの代わりに vCard ファイルを保存します。以下の各サーバーは、同じアカウントから両方のプロトコルを提供します。そのため、カレンダーが動作すれば、アドレス帳はチェックボックスを 1 つ有効にするだけで利用できます。

運用する CalDAV サーバーを選ぶ

Radicale は、動作に必要な最小構成です。Python で動作し、データベースは不要で、データはプレーンファイルのフォルダーに保存されます。このガイドでは、家庭用カレンダーにこれ以上の機能は必要なく、深夜でも問題が発生しにくいため、Radicale を使用します。

Baikal は、Web 管理パネルを備えた選択肢です。PHP と sabre/dav ライブラリで動作し、ユーザーとカレンダーを SQLite または MySQL に保存します。コマンドラインではなく、ブラウザーからユーザーを追加できます。アカウントの追加や削除が頻繁に発生する場合に適しています。

Nextcloud は、カレンダーを複数ある機能の 1 つとして利用する場合に適しています。カレンダー、連絡先、ファイル、モバイルアプリを利用できますが、PHP-FPM、データベース、バックグラウンドジョブの実行環境が必要です。実際に必要な機能に対して構成が重い場合は、より軽量な Nextcloud の代替製品で選択肢を比較できます。また、Nextcloud を導入するもう 1 つの主な目的については、セルフホスト型ファイル同期で扱います。

DAViCal は、長年利用されている PostgreSQL 対応の選択肢です。すでに PostgreSQL を運用しており、カレンダーデータも PostgreSQL に保存したい場合に限り、検討する価値があります。

Ubuntu 24.04 に Radicale をインストールする

Radicale 3.5.10 は、2026 年 8 月時点の最新リリースです。専用の仮想環境にインストールします。

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/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 を再度実行すると、それ以降に追加したすべてのアカウントが削除されます。その結果、1 人だけは正常に同期できる一方で、他の全員にはパスワードプロンプトが繰り返し表示され続けます。Bcrypt も使用できますが、追加インストール radicale[bcrypt] が必要です。

Radicale のドキュメントにある unit を基に、/etc/systemd/system/radicale.service を作成します。

[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 UnauthorizedWWW-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 の 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 -tsyntax is oktest is successful を出力します。出力を確認してからのみ reload してください。壊れたファイルで reload すると、古い設定のまま動作し、次回の再起動まで誤りが隠れるためです。Certbot はサイトファイルを直接編集します。証明書をインストールし、ブロックを port 443 に切り替え、port 80 からのリダイレクトを追加します。最後の curl はパスワードを要求し、Radicale 自身の Web インターフェースである 200 を返すはずです。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 を要求します。カレンダーが表示されるのはその後です。スマートフォンにはサーバーを入力するフィールドが 1 つしかないため、すべての手順が自動的に完了する必要があります。

curl -sI https://cal.example.com/.well-known/caldav

正常な応答は HTTP/2 301 で、location: https://cal.example.com/ ヘッダーが含まれます。そこに 404 があると、同じネットワーク上で Thunderbird が動作する一方、iOS がアカウント情報を確認できないと表示する原因になります。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/ の下にある自分自身のコレクションだけを読み書きでき、それ以外にはアクセスできません。多くの家庭ではこの設定が適切です。カレンダーを共有する最も簡単な方法は、3 つ目のアカウントを使うことです。htpasswdhousehold を作成し、そのアカウントで共有カレンダーを作成します。次に、各デバイスで 2 つ目の 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

大文字と小文字は異なる意味を持ちます。RW は、カレンダーでもアドレス帳でもないコレクションを読み書きします。これは principal フォルダーに該当します。rw は、カレンダー自体を読み書きします。その識別子を、前述のストレージパスにある実際のカレンダーのフォルダー名に置き換えます。

注意すべき制限が 1 つあります。カレンダーのホームセットだけを読み取るクライアントは、別のユーザーのパスにあるカレンダーを表示できません。検出処理がそこまで辿らないためです。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/ を入力します。検出した内容が一覧表示され、追加するカレンダーを選択するよう求められます。

macOS。 System Settings、Internet Accounts、Add Other Account、CalDAV の順に選択します。Account Type を Manual に設定し、同じユーザー名、パスワード、サーバーアドレスを入力します。

CalDAV はポーリング方式のプロトコルです。仕様に push はないため、ノートパソコンで追加した予定は、同じ秒にスマートフォンへ届くのではなく、次回の同期時に届きます。各クライアントで、許容できる間隔を設定してください。スマートフォンでは、間隔を短くするとバッテリー消費が増える点にも注意してください。

ファイルだけで構成されたストアをバックアップする

Radicale では、カレンダーは .ics ファイルのディレクトリとして保存されます。イベントごとに1ファイルがあり、各コレクションには小さなプロパティファイルもあります。ディレクトリをコピーできるツールなら、どれでもバックアップを作成できます。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 を実行してから、サービスを起動します。すべてのクライアントはカレンダーのローカルコピーも保持しています。そのため、障害発生後にまだ同期していないノート PC に、データの2つ目のコピーが残っている場合があります。

Baikal と Nextcloud のどちらが適しているか

Baikal 0.12.1 は 2026 年 8 月 5 日にリリースされ、PHP 8.2 以降が必要です。Web ルートの外側に展開し、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

Web サーバーが書き込むのはこの 2 つのディレクトリだけなので、それ以外を書き込み可能にする必要はありません。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 を reload し、ブラウザーでサイトを開くと、セットアップウィザードによって管理者アカウントと SQLite データベースが作成されます。クライアントの設定は Radicale と同じです。サーバーアドレスには https://cal.example.com/ を指定します。well-known ルールが検出用のアクセスを /dav.php に転送するためです。

同じログインでファイルとスマートフォンアプリも使いたい場合に限り、Nextcloud を導入する価値があります。DAV のルートは /remote.php/dav/ で、同じ検出ルールが適用されます。これらのサービスはいずれも、コンテナで実行するとホスト側に PHP のバージョンを用意せずに済みます。VPS 上の Docker Compose では compose ファイルと、その前段に置くリバースプロキシを扱っています。また、2026 年にセルフホストする価値があるもの は、この方針をどこまで進めるか判断する際の参考になります。

障害の状態と表示される文字列

すべての同期が 401 を返す。 パスワードファイルのアカウントが 2 つ目の htpasswd -c によって失われたか、radicale ユーザーがそのファイルを読み取れません。sudo -u radicale cat /etc/radicale/users で確認してください。そこで permission denied が表示される場合は原因が判明しています。group radicale と mode 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 サーバー内で止まっています。

スマートフォンではアカウントを検証できないが、ブラウザーでは問題ない。 主な原因は 2 つあります。curl で上記のようにテストした、well-known redirect が設定されていない可能性があります。もう 1 つは、証明書チェーンが不完全な可能性です。ブラウザーは不足している中間証明書を取得して補完しますが、iOS は補完しません。shell から確認します。

openssl s_client -connect cal.example.com:443 -servername cal.example.com </dev/null

Verify return code: 0 (ok) を探してください。失敗する場合、nginx の設定が、本来 fullchain.pem を指定すべきところで cert.pem を指定しています。

インポート後にイベントが重複する。 各イベントには UID が含まれており、クライアントはこれを識別子として扱います。識別子を再生成するツールで同じファイルを 2 回インポートすると、どのクライアントも統合できない 2 つのイベントが作成されます。1 台のデバイスで余分なコピーを削除し、その削除を同期させてください。

再起動後にすべて動作しなくなる。 サービスを手動で起動していました。sudo systemctl is-enabled radicaledisabled を表示し、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 を使用します。スマートフォンではサーバーフィールドが 1 つしかないため、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 ヘッダーが返ることを確認してください。

2 人で 1 つのカレンダーを共有できますか?

はい。確実な方法は、共有用のログインを作成することです。htpasswd で 3 つ目のアカウントを作成し、そのアカウントの下に共有カレンダーを配置して、各デバイスに 2 つ目の CalDAV アカウントとして追加します。Radicale の rights ファイルを使えば、別のユーザーのパスにある 1 つのコレクションに対して、指定したユーザーへ読み取り・書き込み権限を付与することもできます。ただし、自分のカレンダーホームセットだけを読み取るクライアントでは、そのカレンダーは表示されません。そのため、この方法は iOS よりも Thunderbird や DAVx⁵ に適しています。

VPS が停止した場合、イベントはどうなりますか?

Radicale のデータストアはプレーンテキストです。/var/lib/radicale/collections/collection-root/ の下に、イベントごとに 1 つの .ics ファイルが保存されます。tar でバックアップし、less で読み取れます。復元するには、展開し、chown -R radicale:radicale を実行して、サービスを起動します。同期済みの各クライアントにもローカルコピーが保存されます。そのため、障害前に最新状態だったラップトップには、カレンダーの完全な 2 つ目のコピーがあります。

CalDAV サーバーは連絡先も同期しますか?

連絡先には CardDAV を使用します。これは RFC 6352 で定義された関連プロトコルで、イベントではなく vCard ファイルを保存します。Radicale、Baikal、Nextcloud はいずれも、同じアカウントと同じホスト名から CardDAV を提供できます。Android では、DAVx⁵ が 1 つのアカウントからカレンダーと連絡先を同期します。iOS では、同じ認証情報を使う CardDAV タイプの 2 つ目のアカウントを追加します。そのため、/.well-known/carddav リダイレクトを CalDAV の設定と併せて nginx の設定に追加します。

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