Como configurar CalDAV no VPS com Radicale
Configure um servidor CalDAV no VPS com Radicale, TLS e descoberta para sincronizar calendários no telemóvel e portátil sem depender do Google.
O que você está construindo
Um calendário auto-hospedado é um servidor CalDAV num VPS sob o seu controlo, protegido por TLS, com um login por pessoa. O telemóvel que tem consigo e o portátil que está na sua secretária mostram os mesmos eventos, tal como o portátil do seu parceiro. Não há uma conta Google no meio.
Isto é diferente de uma página de reservas auto-hospedada. Uma página de reservas destina-se a desconhecidos: publica os seus horários livres e permite que alguém reserve um deles. Um servidor de calendários destina-se aos seus próprios dispositivos: armazena os eventos e mantém todos os clientes sincronizados. É comum utilizar ambos; nesse caso, a ferramenta de reservas consulta a disponibilidade no servidor CalDAV que vai configurar aqui.
A instalação é pequena. O Radicale é um pacote Python e requer cerca de dez linhas de configuração. O que determina se a configuração se mantém funcional durante o primeiro mês é o TLS, a descoberta, as coleções por utilizador e as cópias de segurança. A maior parte do texto abaixo é dedicada a esses pontos.
O que é CalDAV e por que é importante?
CalDAV é a sincronização de calendários através de HTTP. É definido na RFC 4791 como um conjunto de extensões ao WebDAV (web distributed authoring and versioning, um conjunto de métodos HTTP adicionais definido na RFC 4918). Um calendário é uma coleção que funciona como um diretório. Um evento é um ficheiro dentro dessa coleção, escrito no formato de texto iCalendar (RFC 5545), o mesmo formato usado nos anexos .ics do seu correio.
Os clientes usam HTTP normal com alguns métodos adicionais. PROPFIND pergunta o que existe e quais são as respetivas propriedades. REPORT pede um subconjunto filtrado, como todos os eventos num intervalo de datas. PUT escreve um evento e DELETE remove-o. Cada evento contém uma linha UID. Esse identificador permite que dois dispositivos confirmem que estão a consultar o mesmo evento, e não uma cópia.
A portabilidade é a principal vantagem e é a razão para usar CalDAV. iOS, macOS, Thunderbird, Evolution e Android através do DAVx⁵ suportam CalDAV. Os seus dados não ficam dependentes do servidor escolhido hoje. Mova os ficheiros para outro servidor CalDAV, aponte os clientes para o novo nome de anfitrião e nada mais muda.
O CardDAV funciona da mesma forma. É o mesmo conceito para contactos, definido na RFC 6352, e armazena ficheiros vCard em vez de eventos. Todos os servidores abaixo disponibilizam os dois protocolos na mesma conta. Assim, depois de o calendário funcionar, o livro de endereços resume-se a ativar uma opção.
Que servidor CalDAV deve utilizar?
Radicale é a opção mais pequena que funciona. É escrito em Python, não utiliza uma base de dados e armazena os dados numa pasta com ficheiros de texto simples. Este guia utiliza-o porque um calendário doméstico não precisa de mais e porque há muito pouco que possa falhar às 3 da manhã.
Baikal é a opção com um painel de administração web. Funciona com PHP e a biblioteca sabre/dav, mantém utilizadores e calendários em SQLite ou MySQL e permite adicionar uma pessoa no browser, em vez de utilizar a linha de comandos. Escolha-o quando as contas são criadas e removidas com frequência.
Nextcloud é adequado quando o calendário é apenas uma entre várias funcionalidades. Inclui calendário, contactos, ficheiros e uma aplicação móvel, mas requer PHP-FPM, uma base de dados e um executor de tarefas em segundo plano. Se isso parecer pesado para o que realmente precisa, as alternativas mais leves ao Nextcloud apresentam as opções, e a sincronização de ficheiros autoalojada cobre a outra metade das utilizações para as quais as pessoas instalam o Nextcloud.
DAViCal é a opção PostgreSQL com mais tempo de existência. Só vale a pena considerá-lo se já utilizar PostgreSQL e quiser manter os dados dos calendários nessa base de dados.
Instalar o Radicale no Ubuntu 24.04
O Radicale 3.8.3, etiquetado em 8 October 2026, é a versão atual em 11 October 2026. Instale-o no seu próprio ambiente virtual, com o extra bcrypt de que o ficheiro de palavras-passe abaixo depende.
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[bcrypt]'O ambiente virtual não é uma questão de estilo. sudo pip install radicale no Python do sistema falha com error: externally-managed-environment, porque o Ubuntu marca o seu Python como gerido pelo apt para impedir que o pip substitua ficheiros instalados por pacotes.
Escreva /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/collectionshosts fica ligado ao loopback de propósito. O nginx termina o TLS e encaminha as ligações para essa porta, por isso o Radicale nunca fica diretamente exposto à Internet. O exemplo original de 0.0.0.0:5232 publica um serviço sem encriptação que aceita palavras-passe. Esse é o erro relevante neste caso.
Agora, as contas. -B seleciona bcrypt, que o Radicale lê com htpasswd_encryption = autodetect quando o módulo bcrypt da instalação radicale[bcrypt] acima estiver disponível:
sudo htpasswd -B -c /etc/radicale/users you
sudo htpasswd -B /etc/radicale/users partner
sudo chown root:radicale /etc/radicale/users
sudo chmod 640 /etc/radicale/users-c cria o ficheiro e trunca o conteúdo existente. Use-o apenas para o primeiro utilizador. Executar htpasswd -B -c novamente meses depois elimina todas as contas adicionadas após a primeira. O sintoma é uma pessoa conseguir sincronizar normalmente enquanto todas as outras recebem um pedido de palavra-passe que nunca termina. SHA-512 crypt (-5) não é uma opção aqui: o htpasswd ganhou suporte para SHA-2 no Apache 2.4.59, mas o Ubuntu 24.04 inclui o apache2-utils 2.4.58, cujo htpasswd não tem a opção -5.
Crie /etc/systemd/system/radicale.service, adaptado da unidade presente na documentação do 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.targetsudo systemctl daemon-reload
sudo systemctl enable --now radicale
curl -i http://127.0.0.1:5232/Um resultado correto é 401 Unauthorized com um cabeçalho WWW-Authenticate: o serviço está a escutar e a autenticação está ativa. Connection refused significa que não existe nenhum processo a escutar nesse endereço, normalmente porque o serviço falhou ao iniciar ou terminou entretanto, e journalctl -u radicale -n 50 identifica a opção que rejeitou. ProtectSystem=strict monta o sistema de ficheiros como somente leitura para este serviço. Por isso, ReadWritePaths=/var/lib/radicale/ é a linha que permite guardar qualquer evento. Se remover essa linha, as leituras continuam a funcionar, mas todas as escritas falham.
TLS não é opcional, porque os clientes recusam texto simples
Esta configuração autentica com HTTP Basic, o único esquema aceite pelo Radicale, e o Basic envia user:password codificado em base64 em cada pedido. Base64 é uma codificação, não uma encriptação. Em HTTP simples, a palavra-passe é enviada para todas as redes entre o telefone e o servidor, durante todo o dia e em cada sincronização.
Os clientes aplicam esta exigência automaticamente. A documentação do Radicale indica que o macOS Calendar.app pode recusar silenciosamente o envio de credenciais através de HTTP não seguro, e o iOS comporta-se da mesma forma. A conta parece estar configurada, mas nunca sincroniza, sem apresentar qualquer erro.
Aponte primeiro um registo A de cal.example.com para o VPS, porque a autoridade de certificação verifica esse registo. Em seguida, crie /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/; }
}As quatro linhas de cabeçalho do proxy vêm da documentação do Radicale. Mantenha-as como estão.
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 apresenta syntax is ok e test is successful. Só faça o reload depois disso, porque um reload com um ficheiro inválido mantém a configuração antiga em execução e oculta o erro até ao próximo restart. O Certbot edita o ficheiro do site no local: instala o certificado, muda o bloco para a porta 443 e adiciona um redirecionamento da porta 80. O comando curl final pede a palavra-passe e deve devolver 200, que é a própria interface web do Radicale. Um 502 Bad Gateway significa que o nginx está em execução, mas não recebeu uma resposta válida da porta 5232, e a causa habitual é o Radicale não estar a escutar nessa porta.
Por que a adição da conta falha num telefone?
A causa é a descoberta. O RFC 6764 descreve como um cliente transforma um nome de host num URL de calendário. Procura um registo SRV _caldavs._tcp, depois solicita https://cal.example.com/.well-known/caldav e espera um redirecionamento para a raiz DAV. A partir daí, solicita current-user-principal, depois o calendar-home-set desse principal, e só então encontra os seus calendários. Um telefone fornece um único campo para o servidor, por isso cada etapa tem de funcionar sem intervenção.
curl -sI https://cal.example.com/.well-known/caldavA resposta correta é HTTP/2 301 com um cabeçalho location: https://cal.example.com/. Um 404 nesse ponto é uma causa comum para o iOS indicar que não consegue verificar as informações da conta, enquanto o Thunderbird funciona na mesma rede: o Thunderbird usa o URL completo que introduziu, por isso não precisa do redirecionamento.
O destino do redirecionamento depende do servidor. O Radicale servido na raiz do site redireciona para /, e o Radicale responde a esse caminho com um 301 quando o pedido chega até ele. Por isso, as regras do nginx tornam o redirecionamento explícito em vez de o criar. O Baikal inclui regras de exemplo que redirecionam para /dav.php com o estado 308. O Nextcloud redireciona para /remote.php/dav/.
Crie os calendários e partilhe um deles com o seu parceiro
Muitos clientes não conseguem criar um calendário; apenas conseguem subscrever um. Abra https://cal.example.com/ num navegador, inicie sessão como you e crie o calendário nesse local. No disco, ele fica em /var/lib/radicale/collections/collection-root/you/, com um identificador gerado para o nome da pasta.
O backend de permissões predefinido do Radicale é owner_only: uma conta autenticada lê e escreve as próprias coleções em /USERNAME/, e nada mais. Para a maioria das famílias, esta é a configuração correta. A forma mais simples de partilhar um calendário é usar uma terceira conta. Crie household com htpasswd, crie o calendário partilhado nessa conta e adicione-o em cada dispositivo como uma segunda conta CalDAV. Funciona em todos os clientes, incluindo iOS, porque o calendário fica na home própria dessa conta.
Quando precisar de um controlo mais granular, mude para permissões baseadas em regras. Adicione isto a /etc/radicale/config:
[rights]
type = from_file
file = /etc/radicale/rightsDepois, /etc/radicale/rights, com base no exemplo da documentação do 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: rwAs letras maiúsculas e minúsculas têm significados diferentes. R e W leem e escrevem coleções que não são calendários nem livros de endereços, que é o caso de uma pasta principal. r e w leem e escrevem os próprios calendários. Substitua esse identificador pelo nome real da pasta do seu calendário, obtido no caminho de armazenamento acima.
Há uma limitação importante: um cliente que apenas lê o conjunto de homes de calendários não apresenta um calendário localizado no caminho de outro utilizador, porque a descoberta nunca percorre esse caminho. O Thunderbird e o DAVx⁵ conseguem adicioná-lo através do URL completo. O iOS não consegue. Por isso, o padrão com uma conta partilhada é o que funciona sempre.
Configure os clientes, porque é aqui que os calendários self-hosted deixam de funcionar
iPhone e iPad. Abra Settings e selecione Calendar (em versões recentes do iOS, encontra-se em Apps), depois Calendar Accounts, Add Account, Other e Add CalDAV Account. O servidor é cal.example.com, seguido do nome de utilizador e da palavra-passe. Description é apenas uma etiqueta. Se não for possível guardar a conta, abra-a novamente: a vista avançada mostra Use SSL, a porta e o URL completo da conta. Colar o URL evita totalmente a descoberta automática.
Android. Não existe um cliente CalDAV integrado. Instale o DAVx⁵ a partir do F-Droid ou do Google Play e adicione uma conta utilizando o URL base https://cal.example.com/ e o seu nome de utilizador. Em seguida, selecione os calendários pretendidos. O DAVx⁵ escreve no fornecedor de calendários do Android, por isso os eventos aparecem na aplicação de calendário que já utiliza.
Thunderbird. Selecione New Calendar, On the Network e introduza o seu nome de utilizador e a localização https://cal.example.com/. O Thunderbird apresenta o que encontrou e pergunta quais os calendários que pretende adicionar.
macOS. Abra System Settings, Internet Accounts, Add Other Account e CalDAV. Defina Account Type como Manual e introduza o mesmo nome de utilizador, palavra-passe e endereço do servidor.
O CalDAV é um protocolo baseado em polling. A especificação não inclui push, por isso um evento adicionado no portátil chega ao telefone na sincronização seguinte, e não necessariamente no mesmo segundo. Defina em cada cliente um intervalo que seja aceitável e lembre-se de que um intervalo mais curto num telefone consome mais bateria.
Faça backup da store, que contém apenas ficheiros
No Radicale, o seu calendário é um diretório de ficheiros .ics, um ficheiro por evento, além de um pequeno ficheiro de propriedades por coleção. Qualquer ferramenta que copie um diretório faz o backup. Pode abrir um backup com less para confirmar que contém eventos reais. Esta é uma vantagem concreta em relação a um dump de base de dados que não pode ler.
sudo systemctl stop radicale
sudo tar czf /root/radicale-$(date +%F).tar.gz -C /var/lib/radicale collections
sudo systemctl start radicalePare o serviço durante os poucos segundos necessários para criar o arquivo. Assim, nenhum cliente fica a meio de uma operação de escrita enquanto os ficheiros são lidos. Depois, copie o arquivo para fora do servidor. Um backup no mesmo VPS não sobrevive à falha que está a tentar evitar. A restauração é o processo inverso: extraia os ficheiros, sudo chown -R radicale:radicale /var/lib/radicale/collections e inicie o serviço. Cada cliente também mantém uma cópia local dos calendários. Assim, um portátil que não tenha sincronizado desde a falha constitui uma segunda cópia dos seus dados.
Quando Baikal ou Nextcloud é a melhor opção
O Baikal 0.12.1 foi lançado em 5 August 2026 e requer PHP 8.2 ou posterior. Extraia-o fora da raiz web e exponha apenas o diretório 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/configEsses dois diretórios são os únicos nos quais o servidor web escreve, portanto mais nada precisa de permissões de escrita. Dentro do bloco de servidor do nginx, as partes específicas do Baikal são estas:
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; }Recarregue o nginx, abra o site num navegador e o assistente de configuração criará a conta de administrador e a base de dados SQLite. A configuração do cliente é idêntica à do Radicale, com https://cal.example.com/ como endereço do servidor, porque a regra well-known encaminha a descoberta para /dav.php.
O Nextcloud só compensa se também quiser ficheiros e uma aplicação para telemóvel com o mesmo início de sessão. A raiz DAV é /remote.php/dav/ e aplicam-se as mesmas regras de descoberta. Para qualquer uma destas opções, executar o serviço num contentor mantém as versões do PHP fora do host: Docker Compose numa VPS explica o ficheiro Compose e o reverse proxy à frente dele, e o que vale a pena alojar por conta própria em 2026 é um bom ponto para decidir até onde quer avançar.
Modos de falha e mensagens que verá
Cada sincronização devolve 401. O ficheiro de palavras-passe perdeu as contas durante uma segunda execução de htpasswd -c, ou o utilizador radicale não consegue lê-lo. Verifique com sudo -u radicale cat /etc/radicale/users; um erro de permissão nesse comando confirma o problema. A correção é usar o grupo radicale e o modo 640. Por predefinição, o Radicale também espera um segundo depois de cada início de sessão falhado. Por isso, um cliente com uma palavra-passe antiga parece lento em vez de ser rejeitado.
O nginx responde 405 a PROPFIND. O URL está a ser servido como ficheiro estático, por isso o método WebDAV nunca chega ao Radicale. Teste o endpoint diretamente:
curl -u you -X PROPFIND -H "Depth: 0" -i https://cal.example.com/you/Uma coleção DAV funcional responde 207 Multi-Status. Qualquer outro resultado significa que o pedido foi interrompido no servidor Web.
O telefone não consegue validar a conta, mas o browser funciona. Há duas causas comuns. Falta o redirecionamento well-known, que pode ser testado com o comando curl acima. Ou a cadeia de certificados está incompleta. Os browsers contornam este problema ao obter o certificado intermédio em falta, mas o iOS não o faz. Verifique a partir da shell:
openssl s_client -connect cal.example.com:443 -servername cal.example.com </dev/nullProcure Verify return code: 0 (ok). Se falhar, a configuração do nginx aponta para cert.pem quando deveria apontar para fullchain.pem.
Eventos duplicados depois de uma importação. Cada evento contém um UID, que os clientes usam como identidade. Se importar o mesmo ficheiro duas vezes com uma ferramenta que gera identificadores novamente, obterá dois eventos que nunca serão unidos. Elimine as cópias adicionais num dispositivo e deixe a eliminação ser sincronizada.
Tudo deixa de funcionar depois de um reboot. O serviço foi iniciado manualmente. sudo systemctl is-enabled radicale mostra disabled, e sudo systemctl enable --now radicale corrige o problema de forma permanente.
FAQ
Preciso mesmo de TLS para um servidor CalDAV alojado por mim?
Sim. O Radicale autentica com HTTP Basic, por isso a palavra-passe é enviada codificada em base64 em cada pedido, e base64 é trivialmente reversível. Os clientes também impõem esta exigência: a aplicação Calendar.app do macOS pode recusar silenciosamente o envio de credenciais por HTTP não seguro, e o iOS comporta-se da mesma forma. A conta parece ser guardada, mas nunca sincroniza. sudo certbot --nginx -d cal.example.com trata de todo o processo.
Por que motivo o meu telemóvel não consegue adicionar a conta quando o Thunderbird funciona?
O Thunderbird usa o URL completo que introduziu. Um telemóvel fornece apenas um campo de servidor, por isso depende da deteção definida pela RFC 6764. Sem um registo SRV, faz um pedido a https://cal.example.com/.well-known/caldav e espera um redirecionamento para a raiz DAV. Se esse pedido devolver 404, o telemóvel normalmente indica que não consegue verificar a conta. Adicione location = /.well-known/caldav { return 301 https://$host/; } ao nginx e confirme com curl -sI https://cal.example.com/.well-known/caldav que obtém um 301 e um cabeçalho location. Colar o URL completo da conta nas definições avançadas do iOS ignora totalmente a deteção.
Duas pessoas podem partilhar um calendário?
Sim. A forma mais fiável é usar uma conta partilhada. Crie uma terceira conta com htpasswd, coloque o calendário partilhado nessa conta e adicione-a como segunda conta CalDAV em cada dispositivo. O ficheiro de permissões do Radicale pode, em alternativa, conceder a um utilizador identificado acesso de leitura e escrita a uma coleção no caminho de outro utilizador. No entanto, um cliente que leia apenas o seu próprio conjunto de calendários nunca a apresentará. Por isso, essa opção é adequada para o Thunderbird e o DAVx⁵, mas não para o iOS.
O que acontece aos meus eventos se o VPS ficar indisponível?
No Radicale, o armazenamento é composto por texto simples: existe um ficheiro .ics por evento em /var/lib/radicale/collections/collection-root/. Pode fazer uma cópia de segurança com tar e lê-la com less. Para restaurar, extraia os ficheiros, execute chown -R radicale:radicale e inicie o serviço. Cada cliente sincronizado também mantém uma cópia local. Assim, um portátil que estivesse atualizado antes da falha contém uma segunda cópia completa do calendário.
Um servidor CalDAV também sincroniza os meus contactos?
Os contactos usam CardDAV, um protocolo relacionado definido na RFC 6352 que armazena ficheiros vCard em vez de eventos. Radicale, Baikal e Nextcloud disponibilizam-no através da mesma conta e do mesmo nome de anfitrião. No Android, o DAVx⁵ sincroniza calendários e contactos a partir de uma conta. No iOS, adicione uma segunda conta do tipo CardDAV com as mesmas credenciais. É por isso que o redirecionamento /.well-known/carddav deve estar na configuração do nginx, junto ao redirecionamento CalDAV.