Zelf een CalDAV-server draaien op uw eigen VPS
Beheer uw eigen agenda zonder Google. Leer hoe u Radicale instelt op een VPS, inclusief TLS-beveiliging, discovery-configuratie en het koppelen van uw telefoon en laptop.
Wat u bouwt
Een zelfgehoste agenda is één CalDAV-server op een VPS die u beheert, beveiligd met TLS, met één login per persoon. De telefoon in uw zak en de laptop op uw bureau tonen dezelfde afspraken, net als de laptop van uw partner. Er staat geen Google-account tussen.
Dit is een andere taak dan een zelfgehoste boekingspagina. Een boekingspagina is bedoeld voor vreemden: deze publiceert uw vrije tijdstippen en laat iemand er een claimen. Een agendaserver is bedoeld voor uw eigen apparaten: deze slaat de afspraken op en houdt elke client gesynchroniseerd. Mensen draaien vaak beide; de boekingstool leest dan de beschikbaarheid uit de CalDAV-server die u hier bouwt.
De installatie is klein. Radicale is één Python-pakket en ongeveer tien regels configuratie. Wat bepaalt of de setup de eerste maand overleeft, zijn TLS, discovery, collecties per gebruiker en back-ups. Die krijgen hieronder de meeste aandacht.
Wat is CalDAV en waarom is het relevant?
CalDAV is kalendersynchronisatie via HTTP. Het is gedefinieerd in RFC 4791 als uitbreidingen op WebDAV (web distributed authoring and versioning, een verzameling extra HTTP-methoden gedefinieerd in RFC 4918). Een kalender is een collectie die zich gedraagt als een map. Eén afspraak is één bestand daarbinnen, geschreven in het iCalendar-tekstformaat (RFC 5545), hetzelfde formaat als de .ics-bijlagen in uw e-mail.
Clients gebruiken standaard HTTP met enkele toegevoegde methoden. PROPFIND vraagt wat er aanwezig is en welke eigenschappen dit heeft. REPORT vraagt om een gefilterd overzicht, zoals elke afspraak binnen een datumbereik. PUT schrijft één afspraak en DELETE verwijdert deze. Elke afspraak bevat een UID-regel; aan de hand van deze identificatie stemmen twee apparaten af dat zij naar dezelfde afspraak kijken in plaats van naar een kopie.
Portabiliteit is het voordeel en de enige reden om dit te implementeren. iOS, macOS, Thunderbird, Evolution en Android via DAVx⁵ ondersteunen allemaal CalDAV. Uw gegevens zijn niet gebonden aan de server die u vandaag heeft gekozen. Verplaats de bestanden naar een andere CalDAV-server, wijs de clients naar de nieuwe hostnaam en er verandert verder niets.
CardDAV loopt hierin mee. Het is hetzelfde concept voor contactpersonen, gedefinieerd in RFC 6352, waarbij vCard-bestanden worden opgeslagen in plaats van afspraken. Elke onderstaande server biedt beide protocollen aan vanuit hetzelfde account, dus zodra de kalender werkt, is het adresboek slechts een kwestie van een vinkje zetten.
Welke CalDAV-server moet u draaien?
Radicale is de meest compacte oplossing die functioneert. Het is gebaseerd op Python, vereist geen database en slaat gegevens op als een map met platte tekstbestanden. Deze handleiding gebruikt Radicale omdat een huishoudelijke agenda niet meer nodig heeft en omdat er om drie uur 's nachts zeer weinig mis kan gaan.
Baikal is de optie met een webgebaseerd beheerderspaneel. Het draait op PHP en de sabre/dav-bibliotheek, beheert gebruikers en agenda's in SQLite of MySQL en stelt u in staat om via een browser een persoon toe te voegen in plaats van via de command line. Kies voor deze optie wanneer accounts regelmatig worden toegevoegd of verwijderd.
Nextcloud is de juiste keuze wanneer de agenda slechts één van de vele functies is. U krijgt agenda, contacten, bestanden en een mobiele app, maar dit vereist PHP-FPM, een database en een achtergrondproces voor taken. Als dit te zwaar klinkt voor uw werkelijke behoeften, behandelen de lichtere Nextcloud-alternatieven het compromis, en behandelt self-hosted bestandssynchronisatie de andere helft van de redenen waarvoor mensen Nextcloud installeren.
DAViCal is de gevestigde optie voor PostgreSQL. Het is alleen het overwegen waard als u al PostgreSQL draait en wilt dat agendagegevens daarin worden opgeslagen.
Radicale installeren op Ubuntu 24.04
Radicale 3.5.10 was de huidige release per augustus 2026. Installeer het in een eigen virtuele omgeving.
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 radicaleDe virtuele omgeving is geen kwestie van stijl. sudo pip install radicale in de systeem-Python stopt met error: externally-managed-environment, omdat Ubuntu zijn Python markeert als beheerd door apt, waardoor pip geen pakketbestanden kan overschrijven.
Schrijf /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 bindt bewust aan loopback. nginx handelt TLS-termination af en stuurt door naar die poort, waardoor Radicale nooit direct aan het internet wordt blootgesteld. Het upstream-voorbeeld van 0.0.0.0:5232 publiceert een onversleutelde service die wachtwoorden accepteert; dit is de enige fout die hier van belang is.
Nu de accounts. -5 selecteert SHA-512 crypt, wat Radicale leest met htpasswd_encryption = autodetect zonder extra module:
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 maakt het bestand aan en overschrijft de bestaande inhoud. Gebruik dit alleen voor de eerste gebruiker. Het opnieuw uitvoeren van htpasswd -5 -c maanden later verwijdert elk account dat na de eerste is toegevoegd. Het symptoom is dat één persoon probleemloos synchroniseert, terwijl alle anderen in een oneindige wachtwoordprompt terechtkomen. Bcrypt werkt ook, maar vereist de extra installatie radicale[bcrypt].
Maak /etc/systemd/system/radicale.service, aangepast aan de unit in de Radicale-documentatie:
[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/Een gezond resultaat is 401 Unauthorized met een WWW-Authenticate-header: de service luistert en authenticatie is ingeschakeld. Connection refused betekent dat het nooit is gestart, en journalctl -u radicale -n 50 benoemt de optie die werd geweigerd. ProtectSystem=strict koppelt het bestandssysteem alleen-lezen voor deze service, dus ReadWritePaths=/var/lib/radicale/ is de regel die het toestaat om überhaupt een event op te slaan. Verwijder die regel en leesacties blijven werken, terwijl elke schrijfactie faalt.
TLS is niet optioneel, omdat clients plaintext weigeren
CalDAV authenticeert met HTTP Basic, waarbij user:password base64-gecodeerd wordt meegestuurd bij elk afzonderlijk verzoek. Base64 is een codering, geen versleuteling. Via onbeveiligde HTTP geeft u het wachtwoord door aan elk netwerk tussen de telefoon en de server, de hele dag door, bij elke synchronisatie.
De clients dwingen dit voor u af. De documentatie van Radicale vermeldt dat macOS Calendar.app stilzwijgend kan weigeren om inloggegevens te versturen over onbeveiligde HTTP, en iOS gedraagt zich op dezelfde manier. Het account lijkt geconfigureerd, maar synchroniseert simpelweg nooit, zonder dat er een foutmelding wordt getoond.
Wijs eerst een A-record voor cal.example.com naar de VPS, omdat de certificaatautoriteit dit controleert. Maak vervolgens /etc/nginx/sites-available/cal.example.com aan:
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/; }
}De vier proxy-headerregels zijn afkomstig uit de documentatie van Radicale. Behoud deze zoals ze zijn.
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 geeft syntax is ok en test is successful weer. Voer pas een reload uit nadat dit is gebeurd; een reload met een defect bestand laat namelijk de oude configuratie actief en verbergt de fout tot de volgende herstart. Certbot bewerkt het site-bestand op de locatie zelf: het installeert het certificaat, schakelt het blok over naar poort 443 en voegt een redirect toe vanaf poort 80. De uiteindelijke curl vraagt om het wachtwoord en hoort 200 terug te geven, wat de webinterface van Radicale zelf is. Een 502 Bad Gateway betekent dat nginx draait en Radicale niet luistert op 5232.
Waarom mislukt het toevoegen van het account op een telefoon?
Dit komt door discovery. RFC 6764 beschrijft hoe een client een hostnaam omzet in een agenda-URL. Deze zoekt naar een _caldavs._tcp SRV-record, vraagt vervolgens https://cal.example.com/.well-known/caldav op en verwacht een redirect naar de DAV-root. Vanaf daar vraagt de client om current-user-principal, daarna om de calendar-home-set van die principal, en pas dan worden uw agenda's zichtbaar. Een telefoon biedt slechts één veld voor de server, dus elke stap moet automatisch verlopen.
curl -sI https://cal.example.com/.well-known/caldavHet juiste antwoord is HTTP/2 301 met een location: https://cal.example.com/-header. Een 404 op die plek is de reden waarom iOS aangeeft dat de accountgegevens niet kunnen worden geverifieerd, terwijl Thunderbird op hetzelfde netwerk wel werkt: Thunderbird gebruikt de volledige URL die u heeft ingevoerd, waardoor de redirect niet nodig is.
Het doel van de redirect hangt af van de server. Radicale, indien geserveerd vanaf de root van de site, verwijst door naar /. Baikal levert voorbeeldregels mee die doorverwijzen naar /dav.php met status 308. Nextcloud verwijst door naar /remote.php/dav/.
Maak de agenda's aan en deel er een met uw partner
Veel clients kunnen zelf geen agenda aanmaken, maar enkel op een bestaande agenda abonneren. Open https://cal.example.com/ in een browser, log in als you en maak de agenda daar aan. Op de schijf wordt deze opgeslagen onder /var/lib/radicale/collections/collection-root/you/, waarbij een gegenereerde identificatie wordt gebruikt voor de mapnaam.
De standaard rechten-backend van Radicale is owner_only: een geauthenticeerd account leest en schrijft zijn eigen collecties onder /USERNAME/ en niets anders. Voor de meeste huishoudens is dit de juiste instelling, en de eenvoudigste manier om een agenda te delen is via een derde account. Maak household aan met htpasswd, maak de gedeelde agenda aan onder die inloggegevens en voeg deze op elk apparaat toe als een tweede CalDAV-account. Dit werkt op elke client, inclusief iOS, omdat de agenda zich in de eigen thuismap van dat account bevindt.
Wanneer u fijnmazigere controle wenst, schakelt u over naar op regels gebaseerde rechten. Voeg dit toe aan /etc/radicale/config:
[rights]
type = from_file
file = /etc/radicale/rightsVervolgens /etc/radicale/rights, gebaseerd op het voorbeeld in de documentatie van 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: rwDe hoofdletters en kleine letters betekenen verschillende dingen. R en W lezen en schrijven collecties die geen agenda's of adresboeken zijn; dit is wat een principal-map is. r en w lezen en schrijven de agenda's zelf. Vervang die identificatie door de werkelijke mapnaam van uw agenda uit het opslagpad hierboven.
Eén eerlijke beperking: een client die alleen de agenda-thuismap leest, zal geen agenda weergeven die onder het pad van een andere gebruiker staat, omdat de discovery-functie daar niet zoekt. Thunderbird en DAVx⁵ kunnen deze toevoegen via de volledige URL. iOS kan dit niet, en daarom is het patroon met een gedeeld account de methode die altijd werkt.
De clients instellen, want hier gaan zelfgehoste agenda's vaak mis
iPhone en iPad. Open Instellingen, ga naar Agenda (onder Apps in recente iOS-versies), vervolgens Agenda-accounts, Voeg account toe, Anders, Voeg CalDAV-account toe. De server is cal.example.com, gevolgd door de gebruikersnaam en het wachtwoord. De beschrijving is slechts een label. Als het opslaan niet lukt, open het account dan opnieuw: in de geavanceerde weergave ziet u Gebruik SSL, de poort en de volledige account-URL. Door de URL hier te plakken, wordt de automatische detectie volledig overgeslagen.
Android. Er is geen ingebouwde CalDAV-client. Installeer DAVx⁵ via F-Droid of Google Play, voeg een account toe met de basis-URL https://cal.example.com/ en uw gebruikersnaam, en vink vervolgens de agenda's aan die u wilt gebruiken. DAVx⁵ schrijft gegevens naar de Android-agendaprovider, waardoor de afspraken verschijnen in de agenda-app die u al gebruikt.
Thunderbird. Nieuwe agenda, Op het netwerk, en voer vervolgens uw gebruikersnaam en de locatie https://cal.example.com/ in. De applicatie toont wat er is gevonden en vraagt welke agenda's u wilt toevoegen.
macOS. Systeeminstellingen, Internetaccounts, Voeg ander account toe, CalDAV, zet Accounttype op Handmatig, en voer vervolgens dezelfde gebruikersnaam, het wachtwoord en het serveradres in.
CalDAV is een polling-protocol. Er is geen push-functionaliteit in de specificatie aanwezig, dus een afspraak die u op de laptop toevoegt, verschijnt pas op de telefoon bij de volgende synchronisatie, niet direct op hetzelfde moment. Stel in elke client een interval in waar u mee kunt leven, en houd er rekening mee dat een korter interval op een telefoon extra batterij verbruikt.
Maak een back-up van de opslag, die enkel uit bestanden bestaat
Bij Radicale is uw agenda een map met .ics bestanden, één bestand per gebeurtenis, aangevuld met een klein eigenschappenbestand per collectie. Alles wat een map kopieert, fungeert als back-up. U kunt een back-up openen met less om te bevestigen dat deze daadwerkelijk gebeurtenissen bevat. Dit is een aanzienlijk voordeel ten opzichte van een database-dump die u niet kunt lezen.
sudo systemctl stop radicale
sudo tar czf /root/radicale-$(date +%F).tar.gz -C /var/lib/radicale collections
sudo systemctl start radicaleStop de service gedurende de enkele seconden die het archiveren in beslag neemt, zodat geen enkele client halverwege een schrijfactie is wanneer de bestanden worden gelezen. Kopieer het archief daarna van de server af, aangezien een back-up op dezelfde VPS niet bestand is tegen het falen waar u zich op voorbereidt. Herstellen is het omgekeerde proces: uitpakken, sudo chown -R radicale:radicale /var/lib/radicale/collections, en de service starten. Elke client beschikt bovendien over een lokale kopie van de agenda's, waardoor een laptop die sinds het falen niet meer heeft gesynchroniseerd, een tweede kopie van uw gegevens vormt.
Wanneer Baikal of Nextcloud de betere keuze is
Baikal 0.12.1 werd uitgebracht op 5 augustus 2026 en vereist PHP 8.2 of nieuwer. Pak het uit buiten de web root en stel alleen de map html bloot:
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/configDeze twee mappen zijn de enige waar de webserver naar schrijft, dus niets anders hoeft schrijfbaar te zijn. Binnen uw nginx server block zijn de Baikal-specifieke onderdelen als volgt:
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; }Herlaad nginx, open de site in een browser en de installatiewizard maakt het beheerdersaccount en de SQLite database aan. De client-configuratie is identiek aan die van Radicale, met https://cal.example.com/ als serveradres, omdat de well-known regel de discovery doorstuurt naar /dav.php.
Nextcloud is alleen de moeite waard als u ook bestanden en een telefoon-app onder dezelfde login wilt gebruiken. De DAV root is /remote.php/dav/ en dezelfde discovery-regels zijn van toepassing. Voor elk van deze opties geldt dat het draaien van de service in een container PHP-versies buiten uw host houdt: Docker Compose op een VPS behandelt het compose-bestand en de reverse proxy ervoor, en wat is het waard om in 2026 zelf te hosten is een goed startpunt om te bepalen hoe ver u hierin wilt gaan.
Foutmodi en de meldingen die u zult zien
Elke synchronisatie geeft een 401-fout. Ofwel het wachtwoordbestand is zijn accounts kwijtgeraakt door een tweede htpasswd -c, of de gebruiker radicale kan het bestand niet lezen. Controleer dit met sudo -u radicale cat /etc/radicale/users; een 'permission denied' is hier het antwoord, en de oplossing is groep radicale met modus 640. Radicale wacht standaard ook één seconde na elke mislukte inlogpoging, waardoor een client met een verouderd wachtwoord traag lijkt in plaats van direct geweigerd.
nginx antwoordt met 405 op PROPFIND. De URL wordt geserveerd als een statisch bestand, waardoor de WebDAV-methode Radicale nooit bereikt. Test het eindpunt direct:
curl -u you -X PROPFIND -H "Depth: 0" -i https://cal.example.com/you/Een werkende DAV-collectie antwoordt met 207 Multi-Status. Alles wat anders is, betekent dat het verzoek is gestopt bij de webserver.
De telefoon kan het account niet verifiëren, terwijl de browser wel werkt. Er zijn twee gebruikelijke oorzaken. De 'well-known' redirect ontbreekt, wat u kunt testen met de bovenstaande curl-opdracht. Of de certificaatketen is incompleet; browsers lossen dit op door het ontbrekende tussenliggende certificaat zelf op te halen, terwijl iOS dat niet doet. Controleer dit vanaf de shell:
openssl s_client -connect cal.example.com:443 -servername cal.example.com </dev/nullZoek naar Verify return code: 0 (ok). Als dit mislukt, verwijst de nginx-configuratie naar cert.pem, terwijl deze naar fullchain.pem zou moeten verwijzen.
Dubbele gebeurtenissen na een import. Elke gebeurtenis bevat een UID, en clients beschouwen dit als de identiteit. Als u hetzelfde bestand twee keer importeert via een tool die identifiers opnieuw genereert, krijgt u twee gebeurtenissen die nooit samengevoegd zullen worden. Verwijder de extra kopieën op één apparaat en laat de verwijdering synchroniseren.
Alles stopt met werken na een herstart. De service is handmatig gestart. sudo systemctl is-enabled radicale geeft disabled weer, en sudo systemctl enable --now radicale lost dit definitief op.
FAQ
Heb ik echt TLS nodig voor een zelfgehoste CalDAV-server?
Ja. CalDAV authenticeert met HTTP Basic, waardoor het wachtwoord bij elk verzoek base64-gecodeerd wordt verzonden, en base64 is eenvoudig omkeerbaar. Clients dwingen dit ook af: macOS Calendar.app kan zonder melding weigeren om inloggegevens over onbeveiligde HTTP te versturen, en iOS gedraagt zich op dezelfde manier, waardoor het account lijkt te worden opgeslagen maar vervolgens nooit synchroniseert. sudo certbot --nginx -d cal.example.com is de volledige taak.
Waarom kan mijn telefoon het account niet toevoegen terwijl Thunderbird wel werkt?
Thunderbird gebruikt de volledige URL die u heeft ingevoerd. Een telefoon biedt slechts één serverveld, dus volgt deze de RFC 6764 discovery: het verzoekt https://cal.example.com/.well-known/caldav en verwacht een redirect naar de DAV-root. Zonder die redirect krijgt de telefoon een 404 en meldt deze dat het account niet kan worden geverifieerd. Voeg location = /.well-known/caldav { return 301 https://$host/; } toe aan nginx en bevestig vervolgens met curl -sI https://cal.example.com/.well-known/caldav dat u een 301 en een location-header ontvangt.
Kunnen twee personen één agenda delen?
Ja, en de betrouwbare manier is een gedeelde login. Maak een derde account aan met htpasswd, plaats de gedeelde agenda daaronder en voeg deze op elk apparaat toe als een tweede CalDAV-account. Het rechtenbestand van Radicale kan in plaats daarvan een genoemde gebruiker lees- en schrijfrechten geven op een collectie onder het pad van een andere gebruiker, maar een client die alleen zijn eigen 'calendar home set' leest, zal deze nooit weergeven. Die route is daarom geschikter voor Thunderbird en DAVx⁵ dan voor iOS.
Wat gebeurt er met mijn afspraken als de VPS uitvalt?
Bij Radicale is de opslag platte tekst: één .ics-bestand per afspraak onder /var/lib/radicale/collections/collection-root/, waarvan u een back-up kunt maken met tar en die u kunt lezen met less. Herstel is uitpakken, chown -R radicale:radicale, en de service starten. Elke gesynchroniseerde client houdt ook een lokale kopie bij, dus een laptop die vóór de uitval up-to-date was, bevat een volledige tweede kopie van uw agenda.
Synchroniseert een CalDAV-server ook mijn contacten?
Contacten gebruiken CardDAV, een verwant protocol gedefinieerd in RFC 6352 dat vCard-bestanden opslaat in plaats van afspraken. Radicale, Baikal en Nextcloud serveren dit allemaal vanuit hetzelfde account en dezelfde hostnaam. Op Android synchroniseert DAVx⁵ agenda's en contacten vanuit één account. Op iOS voegt u een tweede account van het type CardDAV toe met dezelfde inloggegevens; dit is de reden waarom de /.well-known/carddav-redirect naast de CalDAV-configuratie in uw nginx-configuratie thuishoort.