Calendario self-hosted: configura CalDAV su un VPS
Sincronizza telefono e laptop senza Google: configura Radicale su un VPS con TLS, discovery, account separati e client CalDAV funzionanti.
Cosa stai realizzando
Un calendario self-hosted è un unico server CalDAV su un VPS sotto il tuo controllo, protetto da TLS, con un account per ogni persona. Il telefono che hai in tasca e il laptop sulla scrivania mostrano gli stessi eventi, così come il laptop del tuo partner. Nessun account Google fa da tramite.
È un’attività diversa da una pagina di prenotazione self-hosted. Una pagina di prenotazione è destinata a persone esterne: pubblica le fasce orarie disponibili e consente di prenotarne una. Un server di calendario è destinato ai tuoi dispositivi: archivia gli eventi e mantiene sincronizzati tutti i client. Spesso si usano entrambe le soluzioni; in questo caso lo strumento di prenotazione legge la disponibilità dal server CalDAV che configuri qui.
L’installazione è ridotta. Radicale è un pacchetto Python e richiede circa dieci righe di configurazione. La durata della configurazione dipende soprattutto da TLS, discovery, raccolte separate per utente e backup. Questi aspetti occupano la maggior parte dello spazio seguente.
Che cos'è CalDAV e perché è importante?
CalDAV è la sincronizzazione dei calendari tramite HTTP. È definito nella RFC 4791 come un'estensione di WebDAV (web distributed authoring and versioning, un insieme di metodi HTTP aggiuntivi definiti nella RFC 4918). Un calendario è una collection che si comporta come una directory. Un evento è un file al suo interno, scritto nel formato testuale iCalendar (RFC 5545), lo stesso formato usato dagli allegati .ics nelle email.
I client usano il normale HTTP con alcuni metodi aggiuntivi. PROPFIND chiede quali elementi sono presenti e quali proprietà hanno. REPORT chiede un sottoinsieme filtrato, ad esempio tutti gli eventi compresi in un intervallo di date. PUT scrive un evento e DELETE lo rimuove. Ogni evento contiene una riga UID. Questo identificatore consente a due dispositivi di stabilire che stanno visualizzando lo stesso evento, anziché una copia.
Il vantaggio è la portabilità, ed è l'intero motivo per cui CalDAV è utile. iOS, macOS, Thunderbird, Evolution e Android tramite DAVx⁵ supportano tutti CalDAV. I dati non sono vincolati al server scelto oggi. Spostate i file su un altro server CalDAV, configurate i client con il nuovo hostname e non cambia altro.
CardDAV funziona allo stesso modo. È lo stesso concetto applicato ai contatti, definito nella RFC 6352 e basato su file vCard invece che sugli eventi. Ogni server descritto di seguito fornisce entrambi i protocolli tramite lo stesso account. Dopo aver configurato il calendario, la rubrica richiede soltanto l'attivazione di un'opzione.
Quale server CalDAV scegliere?
Radicale è la soluzione minima che funziona. È scritto in Python, non usa un database e archivia i dati in una directory contenente file normali. Questa guida lo utilizza perché un calendario domestico non richiede altro e perché offre pochissimi punti di errore anche durante la notte.
Baikal è l'opzione con un pannello di amministrazione web. Funziona su PHP e sulla libreria sabre/dav, archivia utenti e calendari in SQLite o MySQL e consente di aggiungere una persona dal browser invece che dalla riga di comando. Sceglilo quando gli account cambiano spesso.
Nextcloud è adatto quando il calendario è una delle varie funzionalità necessarie. Include calendario, contatti, file e un'app mobile, ma richiede PHP-FPM, un database e un processo per l'esecuzione dei job in background. Se questa configurazione è eccessiva rispetto alle tue esigenze, le alternative più leggere a Nextcloud illustrano il compromesso, mentre la sincronizzazione di file self-hosted tratta l'altra esigenza per cui viene spesso installato Nextcloud.
DAViCal è l'opzione PostgreSQL storicamente più consolidata. Vale la pena considerarlo soltanto se utilizzi già PostgreSQL e vuoi archiviare i dati del calendario al suo interno.
Installare Radicale su Ubuntu 24.04
Radicale 3.5.10 era la release corrente ad agosto 2026. Installalo in un ambiente virtuale dedicato.
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 radicaleL'ambiente virtuale non è una scelta di stile. sudo pip install radicale nel Python di sistema restituisce error: externally-managed-environment, perché Ubuntu contrassegna il proprio Python come gestito da apt, impedendo a pip di sovrascrivere i file dei pacchetti.
Scrivi /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 si associa intenzionalmente all'interfaccia di loopback. nginx termina TLS e inoltra le richieste a quella porta, quindi Radicale non è mai esposto direttamente a Internet. L'esempio upstream di 0.0.0.0:5232 pubblica un servizio non cifrato che accetta password. È l'unico errore davvero rilevante in questo caso.
Ora crea gli account. -5 seleziona crypt SHA-512, che Radicale legge con htpasswd_encryption = autodetect senza moduli aggiuntivi:
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 crea il file e tronca il contenuto esistente. Usalo solo per il primo utente. Eseguendo di nuovo htpasswd -5 -c mesi dopo, elimini tutti gli account aggiunti successivamente. Il sintomo è che una persona riesce a sincronizzare correttamente, mentre per tutti gli altri la richiesta della password continua senza fine. Anche Bcrypt funziona, ma richiede l'installazione aggiuntiva radicale[bcrypt].
Crea /etc/systemd/system/radicale.service, adattato dall'unità riportata nella documentazione di 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/Un risultato corretto è 401 Unauthorized con un'intestazione WWW-Authenticate: il servizio è in ascolto e l'autenticazione è attiva. Connection refused indica che il servizio non è mai stato avviato, mentre journalctl -u radicale -n 50 mostra l'opzione rifiutata. ProtectSystem=strict monta il filesystem in sola lettura per questo servizio, quindi ReadWritePaths=/var/lib/radicale/ è la riga che gli consente di salvare anche un solo evento. Se rimuovi quella riga, le letture continuano a funzionare, mentre ogni scrittura fallisce.
TLS non è facoltativo, perché i client rifiutano il testo in chiaro
CalDAV esegue l'autenticazione tramite HTTP Basic e invia user:password codificato in base64 a ogni singola richiesta. Base64 è una codifica, non una cifratura. Con HTTP non protetto, la password viene trasmessa a ogni rete tra il telefono e il server, per tutta la giornata e durante ogni sincronizzazione.
I client applicano questo requisito automaticamente. La documentazione di Radicale specifica che macOS Calendar.app può rifiutarsi silenziosamente di inviare le credenziali tramite HTTP non protetto; iOS si comporta nello stesso modo. L'account risulta configurato, ma la sincronizzazione non avviene mai e non viene visualizzato alcun errore.
Prima crea un record A per cal.example.com che punti al VPS, perché l'autorità di certificazione lo verifica. Quindi crea /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/; }
}Le quattro righe degli header del proxy provengono dalla documentazione di Radicale. Lasciale invariate.
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 visualizza syntax is ok e test is successful. Esegui il reload solo dopo questo controllo, perché un reload con un file non valido lascia in esecuzione la configurazione precedente e nasconde l'errore fino al riavvio successivo. Certbot modifica direttamente il file del sito: installa il certificato, sposta il blocco sulla porta 443 e aggiunge un redirect dalla porta 80. Il comando curl finale richiede la password e dovrebbe restituire 200, che è l'interfaccia web di Radicale. Un 502 Bad Gateway indica che nginx è in esecuzione, ma Radicale non è in ascolto sulla porta 5232.
Perché l'aggiunta dell'account non riesce su un telefono?
Il problema è il rilevamento automatico. RFC 6764 descrive come un client converte un hostname in un URL di calendario. Cerca un record SRV _caldavs._tcp, quindi richiede https://cal.example.com/.well-known/caldav e si aspetta un redirect alla root DAV. Da lì richiede current-user-principal, quindi il calendar-home-set di quel principal, e solo a quel punto visualizza i calendari. Il telefono mette a disposizione un solo campo per il server, quindi ogni passaggio deve funzionare senza intervento dell'utente.
curl -sI https://cal.example.com/.well-known/caldavLa risposta corretta è HTTP/2 301 con un header location: https://cal.example.com/. Un 404 in quel punto spiega perché iOS segnala che non riesce a verificare le informazioni dell'account, mentre Thunderbird sulla stessa rete funziona: Thunderbird usa l'URL completo inserito dall'utente, quindi non ha bisogno del redirect.
La destinazione del redirect dipende dal server. Radicale pubblicato nella root del sito esegue il redirect verso /. Baikal include regole di esempio che eseguono il redirect verso /dav.php con stato 308. Nextcloud esegue il redirect verso /remote.php/dav/.
Crea i calendari e condividine uno con il partner
Molti client non possono creare un calendario e consentono soltanto di sottoscriverne uno. Apri https://cal.example.com/ in un browser, accedi come you e crea il calendario da lì. Sul disco viene salvato in /var/lib/radicale/collections/collection-root/you/, usando un identificatore generato come nome della directory.
Il backend predefinito per i diritti di Radicale è owner_only: un account autenticato legge e modifica le proprie raccolte in /USERNAME/, e nient'altro. Per la maggior parte dei nuclei familiari questa è l'impostazione corretta. Il modo più semplice per condividere un calendario consiste nell'usare un terzo account. Crea household con htpasswd, crea il calendario condiviso utilizzando quell'account e aggiungilo su ogni dispositivo come secondo account CalDAV. Funziona con tutti i client, incluso iOS, perché il calendario si trova nella home del relativo account.
Quando serve un controllo più preciso, passa ai diritti basati su regole. Aggiungi questo contenuto a /etc/radicale/config:
[rights]
type = from_file
file = /etc/radicale/rightsQuindi /etc/radicale/rights, basandoti sull'esempio nella documentazione di 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: rwLe lettere maiuscole e minuscole hanno significati diversi. R e W leggono e modificano le raccolte che non sono calendari o rubriche, cioè quelle che corrispondono a una cartella principal. r e w leggono e modificano i calendari. Sostituisci quell'identificatore con il nome reale della directory del tuo calendario, ricavato dal percorso dello storage indicato sopra.
C'è un limite importante: un client che legge soltanto l'insieme di home dei calendari non visualizza un calendario che si trova nel percorso di un altro utente, perché la fase di rilevamento non esplora quel percorso. Thunderbird e DAVx⁵ possono aggiungerlo usando l'URL completo. iOS non può farlo. Per questo il modello con account condiviso funziona sempre.
Configurare i client, perché è qui che i calendari self-hosted smettono di funzionare
iPhone e iPad. Apri Impostazioni, quindi Calendario (nelle versioni recenti di iOS si trova sotto App), poi Account calendario, Aggiungi account, Altro, Aggiungi account CalDAV. Il server è cal.example.com, seguito da nome utente e password. La descrizione è soltanto un'etichetta. Se il salvataggio non riesce, riapri l'account: la vista avanzata mostra Usa SSL, la porta e l'URL completo dell'account; incollando l'URL puoi saltare completamente il rilevamento.
Android. Non esiste un client CalDAV integrato. Installa DAVx⁵ da F-Droid o Google Play, aggiungi un account usando l'URL di base https://cal.example.com/ e il tuo nome utente, quindi seleziona i calendari desiderati. DAVx⁵ scrive nel provider del calendario Android, quindi gli eventi vengono visualizzati nell'app calendario che usi già.
Thunderbird. Nuovo calendario, In rete, quindi inserisci il tuo nome utente e la posizione https://cal.example.com/. Thunderbird elenca gli elementi trovati e chiede quali calendari aggiungere.
macOS. Impostazioni di sistema, Account Internet, Aggiungi altro account, CalDAV, imposta Tipo di account su Manuale, quindi inserisci lo stesso nome utente, la password e l'indirizzo del server.
CalDAV è un protocollo basato sul polling. La specifica non prevede il push, quindi un evento aggiunto sul laptop arriva sul telefono alla sincronizzazione successiva, non nello stesso secondo. Imposta in ogni client un intervallo compatibile con le tue esigenze e ricorda che un intervallo più breve sul telefono consuma più batteria.
Esegui il backup del negozio, che contiene solo file
In Radicale, il calendario è una directory di file .ics, uno per evento, più un piccolo file delle proprietà per ogni raccolta. Qualsiasi strumento che copia una directory esegue il backup anche dei relativi dati. Puoi aprire un backup con less per verificare che contenga eventi reali. Questo è un vantaggio concreto rispetto a un dump del database che non puoi leggere.
sudo systemctl stop radicale
sudo tar czf /root/radicale-$(date +%F).tar.gz -C /var/lib/radicale collections
sudo systemctl start radicaleArresta il servizio per i pochi secondi necessari alla creazione dell'archivio, così nessun client si trova a metà di una scrittura mentre i file vengono letti. Copia quindi l'archivio fuori dal server, perché un backup sullo stesso VPS non sopravvive al guasto che stai cercando di gestire. Il ripristino segue l'ordine inverso: estrai l'archivio, sudo chown -R radicale:radicale /var/lib/radicale/collections, quindi avvia il servizio. Ogni client conserva inoltre una copia locale dei propri calendari. Un laptop che non si è sincronizzato dall'ultimo guasto costituisce quindi una seconda copia dei dati.
Quando Baikal o Nextcloud sono la scelta più adatta
Baikal 0.12.1 è stato rilasciato il 5 agosto 2026 e richiede PHP 8.2 o versioni successive. Decomprimitelo al di fuori della web root ed esponete soltanto la directory 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/configQueste due directory sono le uniche in cui il web server deve poter scrivere, quindi non è necessario rendere scrivibile altro. Nel blocco server di nginx, le parti specifiche per Baikal sono queste:
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; }Ricaricate nginx, aprite il sito in un browser e la procedura guidata di configurazione creerà l'account amministratore e il database SQLite. La configurazione dei client è identica a quella di Radicale, con https://cal.example.com/ come indirizzo del server, perché la regola well-known inoltra il rilevamento a /dav.php.
Nextcloud è utile solo se volete avere anche i file e un'app per il telefono con lo stesso accesso. La sua radice DAV è /remote.php/dav/ e si applicano le stesse regole di rilevamento. Per tutti questi servizi, l'esecuzione in un container evita di installare versioni di PHP sull'host: Docker Compose su un VPS descrive il file compose e il reverse proxy che lo precede, mentre cosa conviene eseguire in self-hosting nel 2026 è un punto di riferimento ragionevole per decidere fin dove volete spingervi.
Modalità di errore e stringhe visualizzate
Ogni sincronizzazione restituisce 401. Il file delle password ha perso gli account durante un secondo htpasswd -c oppure l'utente radicale non può leggerlo. Verifica con sudo -u radicale cat /etc/radicale/users; se restituisce un errore di autorizzazione negata, hai individuato la causa. Correggi assegnando il file al gruppo radicale e impostando la modalità 640. Per impostazione predefinita, Radicale attende inoltre un secondo dopo ogni accesso non riuscito. Per questo un client con una password obsoleta appare lento invece di ricevere subito un rifiuto.
nginx restituisce 405 su PROPFIND. L'URL viene servito come file statico, quindi il metodo WebDAV non raggiunge Radicale. Verifica direttamente l'endpoint:
curl -u you -X PROPFIND -H "Depth: 0" -i https://cal.example.com/you/Una raccolta DAV funzionante restituisce 207 Multi-Status. Qualsiasi altro risultato indica che la richiesta si è fermata nel web server.
Il telefono non riesce a verificare l'account, ma il browser funziona. Le cause usuali sono due. Manca il redirect well-known, verificato con il comando curl precedente. Oppure la catena dei certificati è incompleta: i browser compensano recuperando l'intermedio mancante, mentre iOS non lo fa. Verificala dalla shell:
openssl s_client -connect cal.example.com:443 -servername cal.example.com </dev/nullCerca Verify return code: 0 (ok). Se il controllo non riesce, la configurazione di nginx punta a cert.pem invece che a fullchain.pem.
Eventi duplicati dopo un'importazione. Ogni evento contiene un UID, che i client usano come identificativo. Se importi due volte lo stesso file tramite uno strumento che rigenera gli identificativi, ottieni due eventi che non verranno mai uniti. Elimina le copie aggiuntive su un dispositivo e lascia che la cancellazione venga sincronizzata.
Dopo un riavvio smette di funzionare tutto. Il servizio è stato avviato manualmente. sudo systemctl is-enabled radicale restituisce disabled e sudo systemctl enable --now radicale risolve il problema in modo permanente.
FAQ
Mi serve davvero TLS per un server CalDAV self-hosted?
Sì. CalDAV usa l'autenticazione HTTP Basic, quindi la password viaggia codificata in base64 a ogni richiesta; base64 è facilmente reversibile. Anche i client lo impongono: macOS Calendar.app può rifiutarsi in modo silenzioso di inviare le credenziali tramite HTTP non sicuro, e iOS si comporta allo stesso modo. L'account sembra essere salvato, ma non esegue mai la sincronizzazione. sudo certbot --nginx -d cal.example.com svolge l'intero compito.
Perché il telefono non riesce ad aggiungere l'account mentre Thunderbird funziona?
Thunderbird usa l'URL completo immesso. Un telefono dispone di un solo campo per il server, quindi segue il meccanismo di rilevamento definito da RFC 6764: richiede https://cal.example.com/.well-known/caldav e si aspetta un redirect verso la radice DAV. Senza quel redirect, il telefono riceve un 404 e segnala di non poter verificare l'account. Aggiungi location = /.well-known/caldav { return 301 https://$host/; } a nginx, quindi verifica con curl -sI https://cal.example.com/.well-known/caldav di ricevere un 301 e un header location.
Due persone possono condividere un calendario?
Sì. Il metodo più affidabile consiste nell'usare un account condiviso. Crea un terzo account con htpasswd, inserisci il calendario condiviso al suo interno e aggiungilo come secondo account CalDAV su ogni dispositivo. In alternativa, il file dei permessi di Radicale può concedere a un utente specifico l'accesso in lettura e scrittura a una raccolta nel percorso di un altro utente. Tuttavia, un client che legge soltanto il proprio insieme di calendari non la visualizzerà mai. Questo metodo è quindi adatto a Thunderbird e DAVx⁵, non a iOS.
Cosa accade agli eventi se il VPS si guasta?
Con Radicale, l'archivio è costituito da testo semplice: un file .ics per ogni evento in /var/lib/radicale/collections/collection-root/. Puoi eseguirne il backup con tar e leggerlo con less. Per ripristinarlo, estrai l'archivio, esegui chown -R radicale:radicale e avvia il servizio. Ogni client sincronizzato conserva inoltre una copia locale. Un portatile aggiornato prima del guasto contiene quindi una seconda copia completa del calendario.
Un server CalDAV sincronizza anche i miei contatti?
I contatti usano CardDAV, un protocollo correlato definito in RFC 6352 che memorizza file vCard invece degli eventi. Radicale, Baikal e Nextcloud lo espongono usando lo stesso account e lo stesso hostname. Su Android, DAVx⁵ sincronizza calendari e contatti dallo stesso account. Su iOS devi aggiungere un secondo account di tipo CardDAV con le stesse credenziali. Per questo il redirect /.well-known/carddav deve essere presente nella configurazione nginx accanto a quello CalDAV.