SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

Настройка своего CalDAV-сервера на VPS с Radicale

Создайте личный сервер календарей для синхронизации устройств без участия Google. Инструкция по установке Radicale, настройке TLS и параметров discovery для корректной работы.

Что вы создаете

Собственный календарь — это CalDAV-сервер на VPS под вашим управлением, защищенный TLS, с отдельной учетной записью для каждого пользователя. Смартфон в вашем кармане и ноутбук на столе отображают одни и те же события, как и ноутбук вашего партнера. Учетная запись Google не выступает посредником.

Это задача, отличная от собственной страницы бронирования. Страница бронирования предназначена для посторонних: она публикует ваши свободные слоты и позволяет кому-либо занять один из них. Календарный сервер предназначен для ваших собственных устройств: он хранит события и синхронизирует все клиенты между собой. Часто пользователи запускают оба решения, и тогда инструмент бронирования считывает доступность из CalDAV-сервера, который вы создадите здесь.

Установка невелика. Radicale — это один пакет Python и около десяти строк конфигурации. То, что определяет, проработает ли установка первый месяц, — это TLS, обнаружение (discovery), коллекции для каждого пользователя и резервное копирование. Им уделено основное внимание ниже.

Что такое CalDAV и почему это важно?

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

Клиенты используют обычный HTTP с несколькими дополнительными методами. PROPFIND запрашивает содержимое и свойства объекта. REPORT запрашивает отфильтрованный срез данных, например, все события в заданном диапазоне дат. PUT записывает событие, а DELETE удаляет его. Каждое событие содержит строку UID, и этот идентификатор позволяет двум устройствам «понимать», что они работают с одним и тем же событием, а не с его копией.

Преимущество заключается в переносимости, и именно ради этого стоит использовать протокол. 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, а самостоятельно хостируемая синхронизация файлов поможет решить вторую половину задач, ради которых обычно устанавливают Nextcloud.

DAViCal — проверенный временем вариант на базе PostgreSQL. Его стоит рассматривать только в том случае, если вы уже используете PostgreSQL и хотите хранить данные календарей именно в этой СУБД.

Установка Radicale на Ubuntu 24.04

Radicale 3.5.10 был актуальным релизом по состоянию на август 2026 года. Установите его в отдельное виртуальное окружение.

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 termination и пересылает запросы на этот порт, поэтому Radicale никогда не взаимодействует с Интернетом напрямую. Пример upstream в 0.0.0.0:5232 публикует незашифрованный сервис, принимающий пароли, что является критической ошибкой в данном случае.

Теперь учетные записи. -5 выбирает хеширование SHA-512, которое 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.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 с каждым запросом. Base64 — это способ кодирования, а не шифрование. При использовании обычного HTTP вы передаете пароль всем узлам сети между телефоном и сервером при каждой синхронизации.

Клиенты принудительно требуют TLS. В документации 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. Выполняйте перезагрузку только после этого, так как перезагрузка с поврежденным файлом конфигурации сохранит работу старой версии и скроет ошибку до следующего перезапуска. Certbot редактирует файл конфигурации сайта на месте: он устанавливает сертификат, переключает блок на порт 443 и добавляет редирект с порта 80. Финальная команда curl запросит пароль и должна вернуть 200 — это веб-интерфейс самого Radicale. Ошибка 502 Bad Gateway означает, что nginx запущен, а Radicale не слушает порт 5232.

Почему добавление учетной записи на телефоне завершается ошибкой?

Причина кроется в механизме обнаружения. RFC 6764 описывает, как клиент преобразует имя хоста в URL календаря. Он ищет SRV-запись _caldavs._tcp, затем запрашивает 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 поставляется с примерами правил, которые перенаправляют на /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. Откройте «Настройки», затем «Календарь» (в новых версиях iOS находится в разделе «Приложения»), далее «Учетные записи», «Добавить учетную запись», «Другое», «Добавить учетную запись CalDAV». В поле «Сервер» введите cal.example.com, затем укажите имя пользователя и пароль. Описание — это просто метка. Если сохранить настройки не удается, откройте учетную запись снова: в расширенном представлении доступны параметры «Использовать SSL», порт и полный URL учетной записи. Вставка полного URL позволяет полностью пропустить этап автоматического обнаружения.

Android. В системе нет встроенного клиента CalDAV. Установите DAVx⁵ из F-Droid или Google Play, добавьте учетную запись, используя базовый URL https://cal.example.com/ и ваше имя пользователя, затем отметьте нужные календари. DAVx⁵ записывает данные в системный календарный провайдер Android, поэтому события появятся в любом приложении-календаре, которое вы используете.

Thunderbird. «Новый календарь», «В сети», затем введите имя пользователя и адрес https://cal.example.com/. Приложение покажет найденные календари и предложит выбрать те, которые нужно добавить.

macOS. «Системные настройки», «Учетные записи в Интернете», «Добавить другую учетную запись», «CalDAV», установите тип учетной записи «Вручную», затем введите те же имя пользователя, пароль и адрес сервера.

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 августа 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

Эти две директории — единственные, в которые веб-сервер должен иметь право записи, поэтому остальные файлы не требуют прав на запись. Внутри вашего блока 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 в основной системе: Docker Compose на VPS описывает файл compose и работу reverse proxy перед ним, а что стоит хостить самостоятельно в 2026 году — подходящий материал, чтобы решить, насколько глубоко вы хотите погрузиться в этот процесс.

Типовые сбои и сообщения об ошибках

Каждая синхронизация возвращает 401. Либо файл паролей потерял учетные записи из-за второго htpasswd -c, либо пользователь radicale не может его прочитать. Проверьте с помощью sudo -u radicale cat /etc/radicale/users; ошибка permission denied укажет на проблему, решение — группа radicale с правами 640. Radicale по умолчанию делает паузу в одну секунду после каждой неудачной попытки входа, поэтому клиент с устаревшим паролем кажется медленным, а не отклоненным.

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

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

Рабочая коллекция DAV отвечает 207 Multi-Status. Любой другой ответ означает, что запрос был остановлен на уровне веб-сервера.

Телефон не может подтвердить учетную запись, а браузер работает корректно. Две типичные причины. Отсутствует well-known редирект, что проверяется командой curl выше. Либо цепочка сертификатов неполная: браузеры скрывают это, самостоятельно догружая недостающий промежуточный сертификат, в то время как iOS этого не делает. Проверьте из командной строки:

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

Действительно ли мне нужен TLS для собственного CalDAV-сервера?

Да. 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