SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor

CalDAV на VPS: власний календар із Radicale

Синхронізуйте календарі телефона й ноутбука без Google: налаштуйте Radicale на VPS, TLS, виявлення сервера, окремих користувачів і клієнтів.

Що ви створюєте

Self-hosted календар — це один CalDAV-сервер на VPS, яким ви керуєте, із TLS і окремим обліковим записом для кожної людини. Телефон у кишені та ноутбук на столі показують ті самі події. Те саме бачить і ноутбук вашого партнера. Посередині немає облікового запису Google.

Це інше завдання, ніж self-hosted сторінка бронювання. Сторінка бронювання призначена для сторонніх людей: вона публікує вільні часові проміжки й дає змогу зайняти один із них. Сервер календаря призначений для ваших власних пристроїв: він зберігає події та підтримує узгодженість між усіма клієнтами. Часто використовують обидва компоненти, і тоді інструмент бронювання отримує дані про доступність із CalDAV-сервера, який ви налаштуєте тут.

Інсталяція невелика. Radicale — це один Python-пакет і приблизно десять рядків конфігурації. Від TLS, механізму виявлення, окремих колекцій для користувачів і резервних копій залежить, чи працюватиме система після першого місяця. Нижче основну увагу приділено саме цим аспектам.

Що таке CalDAV і чому це важливо?

CalDAV — це синхронізація календарів через HTTP. Його визначено в RFC 4791 як розширення WebDAV (web distributed authoring and versioning — набір додаткових методів HTTP, визначених у RFC 4918). Календар є колекцією, яка поводиться як каталог. Одна подія — це один файл усередині неї, записаний у текстовому форматі iCalendar (RFC 5545), у тому самому форматі, що й вкладення .ics у вашій пошті.

Клієнти використовують звичайний HTTP із кількома додатковими методами. PROPFIND запитує, які об’єкти доступні та які властивості вони мають. REPORT запитує відфільтрований фрагмент, наприклад усі події за певний діапазон дат. PUT записує одну подію, а DELETE видаляє її. Кожна подія містить рядок UID, і саме цей ідентифікатор дає змогу двом пристроям визначити, що вони працюють з тією самою подією, а не з її копією.

Основна перевага — переносимість, і саме заради неї CalDAV варто використовувати. iOS, macOS, Thunderbird, Evolution і Android через DAVx⁵ підтримують CalDAV. Ваші дані не прив’язані до сервера, який ви обрали сьогодні. Перемістіть файли на інший CalDAV-сервер, укажіть клієнтам нове ім’я хоста — і більше нічого не зміниться.

CardDAV працює за тим самим принципом. Це аналогічний протокол для контактів, визначений у RFC 6352, який зберігає файли vCard замість подій. Кожен сервер нижче обслуговує обидва протоколи з того самого облікового запису, тому після налаштування календаря для адресної книги достатньо встановити один прапорець.

Який CalDAV-сервер запускати?

Radicale — найпростіший варіант, якого достатньо для роботи. Він написаний на Python, не потребує бази даних, а дані зберігає у звичайних файлах у каталозі. У цьому посібнику використовується саме він, оскільки для домашнього календаря більшого не потрібно, а о третій годині ночі тут майже нічому виходити з ладу.

Baikal — варіант із вебпанеллю адміністрування. Він працює на PHP і бібліотеці sabre/dav, зберігає користувачів і календарі в SQLite або MySQL та дає змогу додавати користувача через браузер, а не командний рядок. Обирайте його, якщо облікові записи часто додаються та видаляються.

Nextcloud підходить, коли календар є лише однією з кількох потрібних функцій. Ви отримуєте календар, контакти, файли та мобільний застосунок, але ціною PHP-FPM, бази даних і засобу запуску фонових завдань. Якщо для ваших фактичних потреб це виглядає надто складним, у легших альтернативах Nextcloud описано компроміс, а в матеріалі про self-hosted синхронізацію файлів — іншу основну причину, через яку встановлюють Nextcloud.

DAViCal — перевірений часом варіант на PostgreSQL. Розглядати його варто лише тоді, коли ви вже використовуєте PostgreSQL і хочете зберігати дані календаря в ньому.

Встановлення Radicale в Ubuntu 24.04

Radicale 3.5.10 був актуальним релізом станом на August 2026. Встановіть його в окреме 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 у system 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 з upstream публікує незашифрований сервіс, який приймає паролі. Саме цього потрібно уникати.

Тепер створіть облікові записи. -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 на основі unit-файла з документації 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.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 не є необов’язковим, оскільки клієнти відмовляються працювати через незашифрований HTTP

CalDAV автентифікується через HTTP Basic і надсилає user:password у кодуванні base64 під час кожного запиту. Base64 — це кодування, а не шифрування. Через звичайний HTTP ви передаєте пароль кожній мережі між телефоном і сервером протягом усього дня, під час кожної синхронізації.

Клієнти самі забезпечують цю вимогу. У документації Radicale зазначено, що macOS Calendar.app може мовчки відмовитися надсилати облікові дані через незахищений HTTP. iOS поводиться так само. Обліковий запис виглядає налаштованим, але синхронізація просто не працює, і немає повідомлення про помилку.

Спочатку вкажіть A-запис для cal.example.com на адресу 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/; }
}

Чотири рядки заголовків проксі взято з документації 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 лише після цього, оскільки reload із пошкодженим файлом залишає запущеною стару конфігурацію та приховує помилку до наступного перезапуску. Certbot редагує файл сайту безпосередньо: встановлює сертифікат, перемикає блок на порт 443 і додає перенаправлення з порту 80. Під час виконання фінальної команди curl буде запитано пароль. Команда має повернути 200 — власний вебінтерфейс Radicale. Відповідь 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 цього principal, і лише тоді отримує доступ до ваших календарів. На телефоні для сервера передбачено одне поле, тому всі кроки мають виконуватися автоматично.

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

Коректна відповідь — HTTP/2 301 із заголовком location: https://cal.example.com/. Наявність 404 саме там пояснює, чому iOS повідомляє, що не може перевірити дані облікового запису, тоді як Thunderbird у тій самій мережі працює: Thunderbird використовує повну URL-адресу, яку ви ввели, тому перенаправлення йому не потрібне.

Ціль перенаправлення залежить від сервера. Radicale, розміщений у корені сайту, перенаправляє на /. Baikal постачається із прикладами правил, які перенаправляють на /dav.php зі статусом 308. Nextcloud перенаправляє на /remote.php/dav/.

Створіть календарі та поділіться одним із них із партнером

Багато клієнтів не можуть створювати календарі, а лише підписуватися на них. Відкрийте https://cal.example.com/ у браузері, увійдіть як you і створіть календар там. На диску він буде розміщений у /var/lib/radicale/collections/collection-root/you/, а назвою папки стане згенерований ідентифікатор.

Стандартний бекенд прав Radicale — owner_only: автентифікований обліковий запис читає та записує власні колекції в /USERNAME/ і не має доступу до інших. Для більшості домогосподарств це правильне налаштування. Найпростіший спосіб поділитися календарем — створити третій обліковий запис. Створіть household за допомогою htpasswd, створіть спільний календар у цьому обліковому записі та додайте його на кожному пристрої як другий 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

Великі та малі літери мають різне значення. R і W читають і записують колекції, які не є календарями або адресними книгами. Саме такою є папка principal. r і w читають і записують самі календарі. Замініть цей ідентифікатор на фактичну назву папки календаря зі шляху до сховища, наведеного вище.

Є одне важливе обмеження: клієнт, який читає лише набір домашніх каталогів календарів, не відображатиме календар в іншому користувацькому шляху, оскільки механізм виявлення не переглядає такі каталоги. Thunderbird і DAVx⁵ можуть додати його за повною URL-адресою. iOS цього не підтримує, тому схема зі спільним обліковим записом завжди працює.

Налаштуйте клієнти, оскільки саме на цьому етапі найчастіше виникають проблеми із self-hosted календарями

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-клієнта немає. Встановіть DAVx⁵ з F-Droid або Google Play, додайте обліковий запис із базовою 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 або новіший. Розпакуйте його поза web root і відкрийте доступ лише до його каталогу 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 server записує дані. Тому інші каталоги не потрібно робити доступними для запису. У блоці server nginx специфічні для 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 на host: у матеріалі Docker Compose на VPS описано compose-файл і reverse proxy перед ним, а матеріал що варто self-hosting у 2026 році допоможе вирішити, наскільки далеко ви хочете рухатися в цьому напрямі.

Режими відмов і повідомлення, які ви побачите

Кожна синхронізація повертає 401. Або файл паролів втратив свої облікові записи через другий htpasswd -c, або користувач radicale не може його прочитати. Перевірте це за допомогою sudo -u radicale cat /etc/radicale/users; повідомлення про відмову в доступі підтвердить причину. Виправлення: додайте користувача до групи radicale і встановіть режим 640. Radicale також за замовчуванням очікує одну секунду після кожної невдалої спроби входу. Тому клієнт зі застарілим паролем здається повільним, а не відхиленим.

nginx повертає 405 на PROPFIND. URL обслуговується як статичний файл, тому метод WebDAV не доходить до Radicale. Перевірте endpoint безпосередньо:

curl -u you -X PROPFIND -H "Depth: 0" -i https://cal.example.com/you/

Робоча DAV-колекція повертає 207 Multi-Status. Будь-яка інша відповідь означає, що запит зупинився на web-сервері.

Телефон не може перевірити обліковий запис, а браузер працює. Зазвичай є дві причини. Відсутній redirect 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

Чи справді для self-hosted 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 натомість може надати іменованому користувачу права читання й запису для однієї колекції в шляху іншого користувача. Однак клієнт, який читає лише власний набір домашніх календарів, не відображатиме цю колекцію. Тому такий варіант підходить для 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 має бути в конфігурації nginx поруч із перенаправленням CalDAV.

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