Baikal CalDAV Hosting
By hernil
Oh boy, this one was quite the journey. If you want to skip straight to how to install Baïkal and work around all the quirks missing in the documentation you can click here.
Why
I’m slowly but steadily moving away from as much Google (or rather big tech in general) services as I can. Where sensible I want to take back control all the way back “in house”. Quite literally. As a matter of fact everything I mention here, and the very blog you’re reading this on is hosted on my home server safely living in my living room media console.
So after jumping from Search to Kagi, from Gmail to Proton mail to Migadu, Photos with Photoprism, and a few other such jumps - it was finally time to tackle one of the last actively used Google services; the calendar. And - it turns out - my contacts! Which honestly have not received the care and maintenance they deserve the last ten-ish years due to social media. But as I’m cutting down and deleting accounts there as well that will hopefully change.
A few additional challenges in addition to a simple personal calendar I wanted to make sure I covered was:
- Invites - from time to time I actually schedule events with someone where a calendar invite with RSVP is useful
- Sharing - I use a shared calendar with my wife and while sharing account credentials is possible it could be nice to avoid that. Especially since a personal calendar would be preferred as well so if possible we’d want to avoid juggling two accounts
- No walled garden aka Standards all the way down - I’ve found the move back to Migadu email with IMAP, SMTP and the good old standards very refreshing. I wanted something standard to tie into multiple ecosystems including Android, Linux, iOS, Mac OS and potentially Home Assistant.
What
The solution is CalDAV - an extension to WebDAV. And as all good things, it comes packaged as (a few) RFCs from the IETF. We’re actually going to use Baïkal which is a CalDAV+CardDAV server. From what I’ve gathered it’s probably the most fully featured CalDAV server out there unless you want to go all the way to a solution like Nextcloud which is famously a bit heavy to run - so using it for a side-feature seems stupid. Also, I believe but don’t quote me on that, that Nextcloud uses the same underlying Sabre libraries as Baïkal.
How
Baïkal with Docker
The community has graciously wrapped Baïkal in a Docker image which should tie well into the rest of my homelab1 stack. So that’s what we’re using. I’m not going to go into details of how to set up Traefik in front of it, but I will cover a special config to cover one of the many quirks in this setup.
Without further ado - here is the docker-compose.yml
file
networks:
web:
external: true
services:
baikal:
image: ckulka/baikal:nginx
container_name: baikal
restart: always
expose:
- 80
environment:
# Setup for sending invitations
MSMTPRC: |
defaults
auth on
tls on
tls_starttls off
tls_trust_file system
account default
host smtp.migadu.com
port 465
from ${EMAIL_ACCOUNT}
user ${EMAIL_ACCOUNT}
password ${EMAIL_PASSWORD}
volumes:
- ./config:/var/www/baikal/config
- ./data:/var/www/baikal/Specific
# Patched file for Home Assistant
- ./patched-home-assistant-caldav.php:/var/www/baikal/vendor/sabre/dav/lib/CalDAV/Plugin.php
networks:
- web
labels:
- traefik.http.routers.baikal.rule=Host(`dav.example.com`)
# Traefik middleware required for Apple, see https://github.com/ckulka/baikal-docker/issues/37.
- traefik.http.routers.baikal.middlewares=baikal
- traefik.http.middlewares.baikal.redirectregex.regex=https://(.*)/.well-known/(card|cal)dav
- traefik.http.middlewares.baikal.redirectregex.replacement=https://$$1/dav.php/
- traefik.http.middlewares.baikal.redirectregex.permanent=true
- traefik.http.routers.baikal.tls=true
- traefik.http.routers.baikal.tls.certresolver=lets-encrypt
Navigating to whatever endpoint you set up will bring you to the Baïkal admin panel where you can create users and calendars. This part is very self-explanatory so I’m not going to dive into that here. Set up a testuser or two and make sure to come back before moving on as there’s quite a few more things to put in place for the best possible experience.
SQLite
Baïkal can be used with a few databases. I just went with SQLite for now to get the ball running. Honestly I think you could scale this setup a lot before SQLite becomes your bottleneck but feel free to use MySQL or PostgreSQL instead.
Traefik
Looking for resources in the .well-known
directory is (one of) the way(s) that CalDAV service discovery works. So throw this in your config as that will greatly help the Apple clients out there. The Github issue references iOS, but I ran into this with Mac OSs Calendar application (through “Internet Accounts”) so it looks like they might have consolidated some code in the last few years.
# Traefik middleware required for iOS, see https://github.com/ckulka/baikal-docker/issues/37.
traefik.http.routers.baikal.middlewares: baikal-dav
traefik.http.middlewares.baikal-dav.redirectregex.regex: https://(.*)/.well-known/(card|cal)dav
traefik.http.middlewares.baikal-dav.redirectregex.replacement: https://$$1/dav.php/
Here is a more complete docker-compose example with Traefik. Note that it uses an older version of Traefik.
Note that this is probably a service that can be hosted internally without internet exposure pretty well - especially if you are the only consumer. Word on the street is that CalDAV clients cache and handle delayed updating of the server works fine.
DNS records
CalDAV clients also do Service Discovery through DNS SRV records. This is actually one of the things that are well documented (once you understand that is your problem …). Setting these seemed to help clients discover user calendars as you don’t have to provide a complete “principal” path. Thunderbird seems to still prefer a manual dav.example.com/dav.php
path to the server. That might be something the TXT
records could help with but I didn’t try as the server is hosted at the root.
Anyways, here are the SRV
records I set in my DNS:
_carddavs._tcp 86400 IN SRV 10 20 443 dav.example.com.
_caldavs._tcp 86400 IN SRV 10 20 443 dav.example.com.
These cover the https, but for good measure I set up ones pointing to the http endpoints as well (they get redirected by Traefik).
_carddav._tcp 86400 IN SRV 10 20 80 dav.example.com.
_caldav._tcp 86400 IN SRV 10 20 80 dav.example.com.
Sharing
This one was the “funniest” to crack. I was thrown a bit off target by some references out there (including this one2. At least it confirmed it was possible to do) of manually updating the database to enable sharing. I got some of that working but it as I hadn’t figured out the service discovery I was still operating with complete calendar principal paths looking something like https://dav.example.com/dav.php/calendars/[email protected]/default/
and it got ugly pretty quick.
Turns out it took stumbling over some guy’s tutorial for controling his radiator (I think??) in German to discover that Baïkal has a non-admin user interface at dav.example.com/dav.php
.
Navigating in a browser to https://dav.example.com/dav.php/calendars/[email protected]/family/
(login with your account, not the admin one) exposes a GUI and if you scroll all the way down you can select sharing in this box.
![Baikal calendar sharing](/posts/baikal-caldav-hosting/images/baikal-calendar-sharing.png)
Note the mailto:
prefix. I’m not 100% sure it’s required (as usernames in Baïkal don’t have to be emails) but it works with it so I didn’t explore further.
The GUI seems to do whatever is needed in the background. This together with the settings over makes the shared calendar discoverable and usable for the shared user.
Invites (by email)
The documentation explains how to setup SMTP so that Baïkal can send out invitations. I opted to create a dedicated [email protected]
address that is hosted with Migadu. Tweak the settings for your provider.
environment:
# Setup for sending invitations
MSMTPRC: |
defaults
auth on
tls on
tls_starttls off
tls_trust_file system
account default
host smtp.migadu.com
port 465
from ${EMAIL_ACCOUNT}
user ${EMAIL_ACCOUNT}
password ${EMAIL_PASSWORD}
EMAIL_ACCOUNT
, and EMAIL_PASSWORD
is set in a .env
file
[email protected]
EMAIL_PASSWORD=superSecretPassword123
Home assistant bug
Home Assistant has a CalDAV integration which seems to have a bug. Someone patched it and it seems to be integrated as an option in the Docker image, but before I found that I already found a patched file that I mount specifically into the container. Works great!
- ./patched-home-assistant-caldav.php:/var/www/baikal/vendor/sabre/dav/lib/CalDAV/Plugin.php
Wrapping up
Hopefully this is covering a few holes or inconsistencies in the documentation out there. It surely would have been useful to stumble upon a few days ago if I may say so myself. If something is unclear or no longer correct do feel free to reach out!
On the client side there’s Thunderbird, Apple actually has good support for the CalDAV standard, and for Android DAVx⁵ seems to work well. I’ll probably connect Gnome somehow as well. Haven’t looked into it yet.
Good luck!