Your Family Calendar Is Held Together by Group Chat
“Wait, is the dentist Tuesday or Thursday?” If that question lives in your house, you need a shared calendar. You do not need Nextcloud, a database cluster, and a weekend of your life to get one.
Run Radicale. It is one small Python process, it stores plain files, and the Docker image is happy in 256 MB of RAM. Pick Baikal only if you want a web admin panel for creating users and calendars and you are fine running PHP. Both speak CalDAV and CardDAV, so your phones cannot tell the difference. What decides whether the thing “actually syncs” is the reverse proxy and the client setup, and that is where this article spends most of its time.
Full example: Clone the working files at github.com/KingPin/sumguy-examples/self-hosting/radicale-vs-baikal-caldav
Radicale vs Baikal in Five Lines
| Radicale 3.8.1 | Baikal 0.12.1 | |
|---|---|---|
| Language | Python | PHP, built on sabre/dav |
| Storage | Plain files on disk | SQLite, MySQL or PostgreSQL |
| User management | htpasswd file | Web admin panel |
| First run | Edit two config files | Web installer |
| Docker image | tomsquest/docker-radicale | ckulka/baikal (community image) |
Both images are community builds, so read their READMEs before you pin anything.
One version gotcha: the ckulka/baikal repo builds Baikal 0.10.1 at the time of writing, while upstream Baikal is at 0.12.1. The image README calls the nginx tag worth a look: it is under half the size of the Apache one. Check the image tag list before you assume you are running the latest Baikal.
Radicale: The Setup
Radicale’s Docker image wants three things from you: a config file, a users file, and a data directory. Here is the compose file.
services: radicale: image: tomsquest/docker-radicale:3.8.1.1 container_name: radicale ports: - 127.0.0.1:5232:5232 init: true read_only: true security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - SETUID - SETGID - CHOWN - KILL deploy: resources: limits: memory: 256M pids: 50 healthcheck: test: curl -f http://127.0.0.1:5232 || exit 1 interval: 30s retries: 3 restart: unless-stopped volumes: - ./data:/data - ./config:/config:roThe hardening block (read-only filesystem, dropped capabilities) comes from the image’s own example compose file. It binds to 127.0.0.1 because the reverse proxy is the only thing that should talk to it.
Radicale image tags follow [Radicale version].[image revision], so 3.8.1.1 is Radicale 3.8.1 with the first image revision.
Users and passwords
Create the htpasswd file with bcrypt hashes. The -B flag selects bcrypt, and the first command’s -c creates the file.
mkdir -p config datahtpasswd -B -c config/users alicehtpasswd -B config/users bobNo htpasswd binary? Run it from the Apache image: docker run --rm -it -v "$PWD/config:/config" httpd:2 htpasswd -B -c /config/users alice. Drop the -c for every user after the first, or you wipe the file.
The config
[server]hosts = 0.0.0.0:5232
[auth]type = htpasswdhtpasswd_filename = /config/usershtpasswd_encryption = bcrypt
[rights]type = from_filefile = /config/rights
[storage]filesystem_folder = /data/collectionsRadicale’s docs list plain, bcrypt, and md5 among the htpasswd methods, and the default is autodetect. Pick bcrypt explicitly so a typo in the file fails loudly instead of quietly matching something weaker.
Sharing One Calendar Between Two Users
Radicale’s default rights type is owner_only: every user sees /username/ and nothing else. That is what you want for a multi-user server. It is a problem for a household, where Alice and Bob both need to write to one calendar.
The docs warn about the catch up front: if you grant access to collections outside the user’s own directory, clients will not detect them automatically. You add the shared calendar by URL. With the from_file rights type, you describe who can touch what in a small file.
# Everyone logged in can read the root[root]user: .+collection:permissions: R
# Each user owns their own principal collection[principal]user: .+collection: {user}permissions: RW
# Each user owns their calendars and address books[calendars]user: .+collection: {user}/[^/]+permissions: rw
# Alice and Bob share everything under /household/[household]user: alice|bobcollection: household(/[^/]+)?permissions: RWrwThree rules make this work.
- Section titles are ignored but must be unique.
userandcollectionare regular expressions, and the first matching section wins. If nothing matches, access is denied.- Capital
RandWcover plain collections. Lowercaserandwcover calendars and address books. The shared section needs both because/household/is a plain collection and/household/family/is a calendar.
I ran this against Radicale 3.8.1: both users could create and write under /household/, and an anonymous request got a 401. Create the shared calendar once. The parent collection has to exist first, or you get a 409.
curl -u alice -X MKCOL https://calendar.example.com/household/curl -u alice -X MKCOL https://calendar.example.com/household/family --data \'<?xml version="1.0"?><create xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"><set><prop><resourcetype><collection/><C:calendar/></resourcetype></prop></set></create>'Each person then adds https://calendar.example.com/household/family/ as a calendar. Their personal calendar lives at /alice/ or /bob/.
The newer option: Radicale’s built-in sharing
Radicale 3.7 added a [sharing] section with a web UI extension, and 3.8 added sharing by group and by realm. Map-based sharing requires authentication and extends the shared list under the user’s own collection listing, which is the behavior you want for client auto-discovery. I used the rights file for the tested setup above because it is a plain text file you can read at 2 AM. If you want shares to appear in each client without pasting URLs, read Radicale’s SHARING.md and try type = csv with collection_by_map = true. I have not run that path.
Baikal: The Setup
Baikal is the friendlier install. You get a web installer on first run and an admin panel afterwards.
services: baikal: image: ckulka/baikal:nginx restart: unless-stopped ports: - 127.0.0.1:8080:80 volumes: - config:/var/www/baikal/config - data:/var/www/baikal/Specific
volumes: config: data:The image README says those two paths, /var/www/baikal/Specific and /var/www/baikal/config, hold everything persistent and belong in your backups. The container listens on port 80.
Open the port in a browser, finish the installer, pick SQLite unless you already run a database server, and create users and calendars from the admin panel. Baikal’s CalDAV and CardDAV endpoint is /dav.php.
Sharing in Baikal
I could not find a sharing screen in Baikal’s docs, so the pattern for a household is simple: create one account called household, create the shared calendar under it, and add that account to every device next to each person’s own account.
It works, with one cost. Every phone holds a second set of credentials, and every event belongs to one shared identity. For two adults and a fridge calendar, that is fine. For a house with five people and a custody schedule, use Radicale rights or per-user accounts.
The Reverse Proxy: Where Syncing Actually Breaks
Apple devices and most clients try https://yourdomain/.well-known/caldav first. If that URL returns a 404, setup fails with a generic “cannot connect” error, and you will blame the server for an hour.
The sabre/dav docs say these must be real redirects, a 301, 303, or 307, pointing at the root of your DAV server. Here is the Caddyfile for Radicale at the root of a hostname.
calendar.example.com { redir /.well-known/caldav / 301 redir /.well-known/carddav / 301 reverse_proxy 127.0.0.1:5232}Baikal serves everything at /dav.php, so the redirects point there.
calendar.example.com { redir /.well-known/caldav /dav.php 301 redir /.well-known/carddav /dav.php 301 reverse_proxy 127.0.0.1:8080}Baikal’s own nginx config in the community image does the same with rewrite ^/.well-known/caldav /dav.php redirect;. If you already have a reverse proxy in front, you need your own redirects there too, because the one inside the container does not see requests that your proxy answers itself.
The sabre/dav docs also recommend running the server at the root of a hostname. A server on /radicale/ forces every client to ask for a full path, and some of them cannot. Give the calendar its own subdomain and your 2 AM self will thank you.
Client Setup, Platform by Platform
Android with DAVx5
DAVx5’s own docs say the base URL can be the server root if well-known URLs work (recommended), or a CalDAV URL such as a calendar URL. So:
- Install DAVx5 (F-Droid or Google Play).
- Add an account, choose login with URL and username, and enter
https://calendar.example.com. - Select the calendars to sync and open them in your calendar app.
For the shared Radicale calendar, add a second DAVx5 account with the full URL https://calendar.example.com/household/family/ as the base URL. DAVx5 accepts a calendar URL there. DAVx5 follows the server’s WebDAV permissions, so a read-only share shows as read-only.
Now the part that burns people. Android runs DAVx5’s sync, and Android decides when. DAVx5’s manual says scheduled sync only runs with automatic sync enabled system-wide, permissions granted, a network connection, and battery saving disabled for DAVx5. Turn off battery optimization for the app or your phone will quietly stop syncing until you open it.
Sync intervals under 15 minutes are not allowed on Android 7 and later. The manual suggests an hour with “Synchronize now” in your calendar app for urgent updates. Setting the interval to one minute does nothing except make you feel productive.
iPhone, iPad and Mac
Apple’s calendar clients want HTTPS. Radicale’s docs note that macOS Calendar may silently refuse to send credentials over plain HTTP, which shows up as unexplained auth failures. Put the Caddy block in front first and test with the real hostname.
- Open Settings, then Calendar, then Accounts, then Add Account, then Other.
- Choose “Add CalDAV Account” (and “Add CardDAV Account” for contacts).
- Enter
calendar.example.com, your username, and your password.
With the .well-known redirects in place, the hostname is enough. The sabre/dav docs say Apple products check ports 443 and 8443 for HTTPS first, then the HTTP ports.
If the username is an email address and logins fail, look at Radicale’s urldecode_username option (added in 3.5.3). The docs say iOS devices can send the username URL-encoded, so [email protected] becomes user%40example.com and authentication breaks. Setting it to True under [auth] fixes that.
For a Radicale shared calendar outside the user’s own path, add it as a second CalDAV account pointing at the full collection URL.
Thunderbird
Thunderbird’s calendar tab can add a network calendar. Per Radicale’s client notes, enter your username and the server URL, enter the password when asked, and it lists the existing calendars. For contacts, Thunderbird has subscribed to CardDAV address books natively since version 102. Radicale’s docs still point to the CardBook add-on, which you only need for extra features.
The Troubleshooting Table
| Symptom | Likely cause |
|---|---|
| iPhone says “cannot connect” instantly | Missing or non-redirect .well-known URL, or plain HTTP |
| DAVx5 finds nothing | Base URL points at the wrong path, or the proxy rewrites the path |
| Calendar updates arrive hours late on Android | Battery optimization is on for DAVx5 |
| Shared calendar missing from the list | Radicale rights allow it, but clients do not auto-discover outside /user/. Add the URL |
| 409 when creating a shared calendar | The parent collection (/household/) does not exist yet |
Check .well-known yourself before you blame a client:
curl -sI https://calendar.example.com/.well-known/caldav | head -3You want a redirect and a Location header. Caddy as configured above sends a 301, and the Baikal image’s own nginx sends a 302. A 404 or a 200 means your proxy is the problem.
The SumGuy Take
Run Radicale for a household. Two files of config, plain-text storage you can rsync without a database dump, and a rights file that gives you a real shared calendar. Run Baikal if the web admin panel matters more to you than the footprint, and use the shared-account pattern.
Whichever server you pick, spend your effort on three things: HTTPS, the two .well-known redirects, and disabling battery optimization for DAVx5. Picking the server is the easy half. Obsessing over it is like buying a forklift and then parking it at the wrong end of the warehouse.
Common Questions
Can I share one Radicale calendar between two users?
Yes. Set [rights] type = from_file and add a rights section that matches both usernames to a shared path such as household. Radicale does not auto-discover collections outside /username/, so each person adds the shared calendar URL by hand. Radicale 3.7 and later also has a separate [sharing] feature.
Does Baikal work with iPhone?
Yes. Baikal speaks CalDAV and CardDAV through sabre/dav, and iOS adds it as a CalDAV account. You need HTTPS and a .well-known/caldav redirect to /dav.php so the hostname alone works. Baikal’s community Docker image ships that redirect, and a proxy in front needs its own.
How do I back up a Radicale or Baikal calendar?
Copy the data directories. Radicale stores plain files under /data/collections, so rsync of that folder plus the config directory is a full backup. Baikal’s image keeps state in /var/www/baikal/Specific and /var/www/baikal/config, and its README says to back up both.
Do I need Nextcloud instead of Radicale or Baikal?
No, unless you also want files, photos, or office documents. Nextcloud includes a calendar, but it is a large PHP application with a database. Radicale or Baikal only does calendars and contacts, and the same DAVx5, iOS, and Thunderbird setups work against either.