DockerでRadicaleを使ったセルフホストCalDAV/CardDAVサーバー:GoogleもiCloudも使わずにカレンダーと連絡先を同期する

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

データ管理の意識を変えた出来事

きっかけは、Googleアカウントの停止通知でした——私のではなく、同僚のものです。彼は目を覚ますと、何年分もの連絡先とカレンダーの予定が「不審なアクティビティ」というフラグで閲覧不能になっているのを発見しました。3日後にGoogleが復元してくれましたが、たった3日で、自分のデータがどこに保存されているかを真剣に考えるきっかけになりました。

私のカレンダーにはサーバーのメンテナンス時間、チームの締め切り、個人の予定が入っていました。連絡先には、他にバックアップしていない電話番号も入っていました。そのすべてがGoogleのサーバーに保存されており、アルゴリズムの一つの判断で、本当に必要な深夜2時に突然アクセスできなくなる可能性があったのです。

その週末、私はRadicale——軽量なCalDAV/CardDAVサーバー——を自宅のホームラボに構築しました。それ以来、ずっと本番環境で稼働しています。iPhone、仕事用のAndroid端末、LinuxデスクトップへのSync(同期)は、1年以上一度も途切れていません。スタック全体のRAM使用量は50MB以下です。その構築方法を詳しく解説します。

設定に入る前にプロトコルを理解しよう

CalDAVは、スマートフォンがカレンダーを同期するために使うプロトコルです。iPhoneでイベントを追加すると、iCloud——または設定したサーバー——にCalDAVリクエストが送信されます。CardDAVは同じことを連絡先に対して行います。どちらもHTTPS上で動作するオープンスタンダードであり、iOS、Android、macOS、Windowsのほか、実用的なカレンダーや連絡先アプリのほぼすべてでネイティブにサポートされています。

Radicaleは、意図的にミニマルに作られたPythonベースのCalDAV/CardDAVサーバーです。Web UI、ダッシュボード、余計な機能は一切なく——プロトコルサーバーとしての仕事を淡々とこなすだけです。動作要素が少ないほど攻撃対象領域が小さくなり、なんでもない火曜日に突然壊れるリスクも減ります。

今回構築するスタック:

  • 永続データボリューム付きのDockerでのRadicale
  • bcryptハッシュによるパスワード認証
  • HTTPS用のNginxリバースプロキシ(クライアントは平文HTTPを拒否する)

ステップバイステップでスタックを構築する

ディレクトリ構成

Dockerコマンドを実行する前に、作業ディレクトリを作成します:

mkdir -p ~/radicale/{data,config}
cd ~/radicale

Radicale設定ファイル

config/configを作成します(拡張子なし——これがRadicaleの慣例です):

[server]
hosts = 0.0.0.0:5232

[auth]
type = htpasswd
htpasswd_filename = /data/users
htpasswd_encryption = bcrypt

[storage]
filesystem_folder = /data/collections

[logging]
level = info

htpasswd認証を使用することで、Radicaleは自分で管理するファイルから認証情報を読み取ります。OAuthトークンも、外部サービスへの依存も、自分のサーバー外に単一障害点も存在しません。

ユーザーの作成

追加のシステムパッケージをインストールせずに、bcryptハッシュパスワードを生成します:

docker run --rm -it python:3.11-slim bash -c \
  "pip install bcrypt -q && python3 -c \"import bcrypt; print(bcrypt.hashpw(b'yourpassword', bcrypt.gensalt()).decode())\""

出力されたハッシュをコピーして、usersファイルを作成します:

echo "yourusername:\$2b\$12\$yourhashedpasswordhere" > ~/radicale/data/users

複数のユーザーを追加する場合は、ユーザーごとにusername:hashの行を1行ずつ追加します。スマートフォンやデスクトップクライアントに入力するのは平文パスワードであり、ハッシュではありません。

Docker Composeファイル

~/radicale/内にdocker-compose.ymlを作成します:

services:
  radicale:
    image: tomsquest/docker-radicale:latest
    container_name: radicale
    restart: unless-stopped
    ports:
      - "5232:5232"
    volumes:
      - ./data:/data
      - ./config/config:/config/config:ro
    environment:
      - RADICALE_CONFIG=/config/config
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:5232/.well-known/caldav"]
      interval: 30s
      timeout: 10s
      retries: 3

read_only: trueno-new-privilegesは、見せかけだけのセキュリティ対策ではありません。ネットワークに公開されたサービスでは、深夜2時に自分が監視していない間に何か問題が起きても、コンテナが自身のファイルシステムを変更できない状態にしておくことが重要です。

docker compose up -d
docker compose logs -f radicale

Radicaleが起動し、ポート5232でリッスンしていることが確認できるはずです。

HTTPSありのNginxリバースプロキシ

クライアント——特にiOS——は平文HTTPでの同期を拒否します。TLSが必須です。すでにホームラボでNginx Proxy ManagerやCaddyを運用している場合は、ポート5232に向ければOKです。そうでない場合は、最小限のNginx設定を以下に示します:

limit_req_zone $binary_remote_addr zone=radicale:10m rate=5r/m;

server {
    listen 443 ssl http2;
    server_name cal.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/cal.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cal.yourdomain.com/privkey.pem;

    location / {
        limit_req zone=radicale burst=20 nodelay;
        proxy_pass http://127.0.0.1:5232;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_set_header X-Script-Name "";
    }
}

レート制限はここで重要な役割を果たします。Radicaleにはブルートフォース攻撃への保護機能が組み込まれていないため、このNginx設定でプロキシ層において不正なリクエストがサーバーに到達する前に処理します。

Certbotで証明書を取得します:

certbot --nginx -d cal.yourdomain.com

ホームラボが内部のみで公開ドメインがない場合、自己署名証明書でも問題ありません。各デバイスで信頼済み証明書として追加する必要があります。

デバイスへの接続設定

iOS(iPhoneとiPad)

設定 → カレンダー → アカウント → アカウントを追加 → その他 → CalDAVアカウントを追加に移動します:

  • サーバー:https://cal.yourdomain.com
  • ユーザー名:あなたのユーザー名
  • パスワード:平文パスワード(bcryptハッシュではない)

連絡先の場合:設定 → 連絡先 → アカウント → アカウントを追加 → その他 → CardDAVアカウントを追加から同じサーバーURLと認証情報を入力します。iOSはルートURLからCalDAVとCardDAVの両エンドポイントを自動検出します——Radicaleは追加設定なしでこれを正しく処理します。

Android

AndroidにはネイティブのCalDAV/CardDAVサポートがありません(本当です)。F-DroidまたはPlay StoreからDAVx⁵をインストールします:

  1. アカウントを追加 → URLとユーザー名
  2. 認証情報とともにhttps://cal.yourdomain.comを入力
  3. DAVx⁵がカレンダーとアドレス帳を自動検出します
  4. 同期するコレクションを選択し、間隔を設定します

DAVx⁵はシステムのカレンダーと連絡先アプリと統合されるため、設定後はネイティブアカウントと同じように見え、動作します。

macOSとLinuxデスクトップ

macOSでは、システム設定 → インターネットアカウント → アカウントを追加 → CalDAVに移動します。連絡先については、連絡先アプリを開き、環境設定からCardDAVアカウントを追加します。どちらも同じサーバーURLを使用します。

Linuxでは、CardBook拡張機能を入れたThunderbirdがCardDAVを確実に処理します。ネイティブアプリを好む場合は、GNOME CalendarとGNOME ContactsがどちらもGNOME Settingsのオンラインアカウント機能でサポートされています。

同期の確認

スマートフォンでテストイベントを作成してから、サーバー上のRadicaleデータディレクトリを確認します:

ls ~/radicale/data/collections/collection-root/yourusername/

各カレンダーとアドレス帳のディレクトリが表示され、イベント用の.icsファイルと連絡先用の.vcfファイルが確認できます。そこにイベントが表示されていれば、エンドツーエンドで同期が機能しています。

長期運用のコツ

本番運用で、ドキュメントには書かれていないことをいくつか学びました:

バックアップはファイルなので簡単です。 Radicaleはすべてをディスク上の平文.ics.vcfファイルとして保存します。シンプルなcronジョブで十分です:

# crontab -e でcrontabに追加する
0 3 * * * tar -czf /backup/radicale-$(date +%Y%m%d).tar.gz ~/radicale/data/ && find /backup/ -name 'radicale-*.tar.gz' -mtime +30 -delete

毎晩バックアップを取り、30日以上前のバックアップを自動削除します。

稼働時間は問題ありません。 Composeファイルのrestart: unless-stoppedポリシーがプロセスのクラッシュを自動的に処理します。Dockerはシステム起動時にコンテナを開始します。1年以上の本番運用で、コンテナが再起動したのはたった1回——ホストのカーネルアップデート後——でしたが、数秒以内に自動で復旧しました。

後から新しいユーザーを追加するには、usersファイルに1行追記するだけです。コンテナの再起動は不要です——Radicaleは認証の試みごとにファイルを読み込みます。

# 新規ユーザーのハッシュを生成する
docker run --rm python:3.11-slim bash -c \
  "pip install bcrypt -q && python3 -c \"import bcrypt; print(bcrypt.hashpw(b'newpassword', bcrypt.gensalt()).decode())\""

# usersファイルに追記する
echo "newuser:\$2b\$12\$newhash" >> ~/radicale/data/users

30分のセットアップ、数年間のデータ所有権

Dockerがすでに動いていてドメインをサーバーに向けてあれば、ゼロから同期完了まで約30分です。その後、データはあなたのものです——利用規約の変更を追う必要もなく、アカウント停止のリスクもなく、サブスクリプション費用も不要です。

予想外だったのは、稼働させると同期がGoogle Calendarとまったく変わらない感覚だということです。スマートフォンで追加したイベントが数秒以内にデスクトップに表示されます。連絡先は何もしなくても双方向で同期されます。iOSとAndroidでは組み込みアカウントのように見えます——DAVx⁵とiOSが文字通りそのように扱うからです。

本当の価値は、他の誰かに何か問題が起きたとき——アカウントが停止された、データポリシーが変更された——に実感します。あなたは肩をすくめるだけです。自分のカレンダーは自分が管理するサーバーにあるのですから。

Share: