Radicale 3.9 mit Docker Compose: eigener CalDAV- und CardDAV-Server
Radicale ist ein schlanker CalDAV- und CardDAV-Server für Kalender und Kontakte. Die Anleitung zeigt das offizielle Docker-Image 3.9.0 mit gehärteter compose.yaml, bcrypt-Benutzern, Rechtetrennung per owner_only, Tests mit curl, Git-Versionierung, Reverse Proxy sowie praktisch geprüftes Backup und Restore.
Werbung Mit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Wer im kleinen Unternehmen nur Termine, ein Firmenadressbuch und Synchronisation mit Smartphone und Thunderbird braucht, kommt mit einem eigenen CalDAV- und CardDAV-Server aus. Radicale ist dafür die schlankste verbreitete Lösung: ein kleiner Python-Server ohne Datenbank, der alles als Dateien ablegt. Diese Anleitung zeigt den Betrieb mit dem offiziellen Docker-Image, gehärteter compose.yaml, bcrypt-Passwörtern, getrennten Benutzerbereichen sowie Backup und Restore.
Voraussetzungen
Radicale (Kozea/Radicale, GPL-3.0) liegt aktuell als v3.9.0 vom 25.10.2026 vor. Upstream veröffentlicht ein eigenes Docker-Image, das diese Anleitung nutzt.
- CPU: 1 Kern genügt, auch ein kleiner VPS oder Mini-PC. Das Image gibt es laut Doku für
linux/amd64undlinux/arm64, also auch für Raspberry Pi 4 und 5. - RAM: Der Container belegt 26 bis 40 MiB. Mit
mem_limit: 256mbleibt reichlich Reserve. - Speicher: Image rund 157 MB, Beispieldaten mit Git-Historie 236 KB.
- Software: Linux mit Docker Engine und Compose-Plugin.
- Port: intern 5232, auf dem Host nur an
127.0.0.1gebunden. Für Zugriff von außen einen Reverse Proxy mit TLS auf Port 443 und eine eigene Subdomain, etwadav.example.com. - Grenzen: kein Groupware-Server, keine Einladungen per Mail, keine Ressourcenbuchung. Eine einfache Frei/Gebucht-Ansicht gibt es seit 3.8.2.
| Eckdaten | Wert |
|---|---|
| Image | ghcr.io/kozea/radicale:3.9.0 |
| Port | 5232 im Container |
| Volumes | /etc/radicale (Konfiguration, Benutzer), /var/lib/radicale (Daten) |
| Benutzer im Container | radicale, UID und GID 1000 |
| Umgebungsvariablen | keine nötig, Konfiguration per Datei |
Schritt 1: Verzeichnisse anlegen und Image-Tag wählen
Legen Sie einen Projektordner mit Unterordnern für Konfiguration und Daten an. Weil der Container als UID 1000 läuft, muss data diesem Benutzer gehören, sonst kann Radicale nichts speichern.
$ sudo mkdir -p /opt/radicale/config /opt/radicale/data
$ sudo chown -R 1000:1000 /opt/radicale/data
$ cd /opt/radicalelatest ist bei Radicale meist ein Nightly-Build. Pinnen Sie 3.9.0, damit Updates bewusst erfolgen.
ls -ln /opt/radicale zeigt data mit Eigentümer 1000 1000.
Schritt 2: compose.yaml und Konfiguration schreiben
Die Vorlage aus dem Repository veröffentlicht Port 5232 auf allen Schnittstellen. Die folgende Fassung ist gehärtet: Port nur lokal, schreibgeschütztes Dateisystem, keine Capabilities, Healthcheck mit dem im Image enthaltenen curl.
services:
radicale:
image: ghcr.io/kozea/radicale:3.9.0
container_name: radicale
restart: unless-stopped
ports:
# Nur lokal, der Reverse Proxy greift darauf zu
- "127.0.0.1:5232:5232"
volumes:
- ./config:/etc/radicale:ro
- ./data:/var/lib/radicale
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: 256m
healthcheck:
test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:5232/.web/"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 3Eine .env braucht Radicale nicht. Die Konfiguration steht in config/config:
[server]
hosts = 0.0.0.0:5232, [::]:5232
[auth]
type = htpasswd
htpasswd_filename = /etc/radicale/users
htpasswd_encryption = bcrypt
delay = 1
[rights]
type = owner_only
[storage]
filesystem_folder = /var/lib/radicale/collections
[logging]
level = info
mask_passwords = True[auth] type = htpasswd: Ohne diesen Abschnitt lehnt Radicale seit 3.5.0 jede Anmeldung ab.htpasswd_encryption = bcrypt: nur bcrypt-Hashes,autodetecterkennt das Verfahren pro Zeile.delay = 1: Verzögerung nach Fehlversuchen.owner_only: Jeder Benutzer liest und schreibt nur unter/BENUTZERNAME/.
docker compose config --quiet endet ohne Ausgabe und mit Exit-Code 0.
Schritt 3: Benutzer mit bcrypt anlegen
Das Image enthält htpasswd aus den Apache-Werkzeugen. So brauchen Sie auf dem Host nichts zu installieren. Die Option -B wählt bcrypt, -C 12 den Kostenfaktor.
cd /opt/radicale
# Erster Benutzer, Datei neu schreiben
docker run --rm --entrypoint htpasswd ghcr.io/kozea/radicale:3.9.0 \
-nbB -C 12 anna 'SicheresPasswort1' | sed '/^$/d' > config/users
# Weitere Benutzer anhängen
docker run --rm --entrypoint htpasswd ghcr.io/kozea/radicale:3.9.0 \
-nbB -C 12 bernd 'SicheresPasswort2' | sed '/^$/d' >> config/users
chmod 644 config/usersDas sed entfernt die angehängte Leerzeile. Ohne -b und mit docker run -it fragt htpasswd das Passwort interaktiv ab, dann landet es nicht in der Shell-Historie. Neue Einträge wirken ohne Neustart.
cut -c1-12 config/users zeigt pro Benutzer eine Zeile, die Hashes beginnen mit $2y$12$.
Schritt 4: Starten und Healthcheck prüfen
$ docker compose up -d
$ docker compose logs --no-log-prefix | grep -E 'Starting Radicale|entries|server ready'
$ docker compose psIm Log erscheinen Starting Radicale (python=3.15.1 radicale=3.9.0 ...), die Zeile Read content of htpasswd file done ... (entries: 2, duplicates: 0, errors: 0) und Radicale server ready. Die Warnung does not exist, creating now beim ersten Start ist normal. Bis zur ersten Antwort können 30 Sekunden vergehen.
docker compose ps meldet (healthy), und curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5232/.web/ liefert 200.
Schritt 5: Anmeldung, Kalender und Adressbuch testen
Die Weboberfläche unter /.web/ legt Sammlungen per Klick an, reproduzierbar testen Sie mit curl. Zuerst die Anmeldung ohne, mit falschem und mit richtigem Passwort:
$ U=http://127.0.0.1:5232
$ curl -s -o /dev/null -w '%{http_code}\n' -X PROPFIND $U/anna/
$ curl -s -o /dev/null -w '%{http_code}\n' -u anna:falsch -X PROPFIND $U/anna/
$ curl -s -o /dev/null -w '%{http_code}\n' -u anna:SicheresPasswort1 -X PROPFIND -H 'Depth: 0' $U/anna/Erwartet sind 401, 401 und 207. Danach Kalender und Adressbuch anlegen und je einen Eintrag speichern:
A="-u anna:SicheresPasswort1"
# Kalender anlegen
curl -s -o /dev/null -w '%{http_code}\n' $A -X MKCALENDAR $U/anna/firma-kalender/
# Adressbuch anlegen
curl -s -o /dev/null -w '%{http_code}\n' $A -X MKCOL $U/anna/kontakte/ \
-H 'Content-Type: application/xml' --data '<?xml version="1.0"?>
<D:mkcol xmlns:D="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav"><D:set><D:prop>
<D:resourcetype><D:collection/><CR:addressbook/></D:resourcetype>
<D:displayname>Firmenkontakte</D:displayname></D:prop></D:set></D:mkcol>'
# Termin und Kontakt speichern
curl -s -o /dev/null -w '%{http_code}\n' $A -X PUT -H 'Content-Type: text/calendar' \
--data-binary @termin.ics $U/anna/firma-kalender/teammeeting.ics
curl -s -o /dev/null -w '%{http_code}\n' $A -X PUT -H 'Content-Type: text/vcard' \
--data-binary @kontakt.vcf $U/anna/kontakte/max.vcf
# Zurücklesen
curl -s $A $U/anna/firma-kalender/teammeeting.ics | grep SUMMARYAlle vier Schreibaufrufe liefern 201. GET und REPORT (so synchronisieren Clients) liefern SUMMARY:Teammeeting Montag und FN:Max Mustermann zurück.
In Thunderbird, DAVx5, GNOME oder KDE genügt laut Doku meist Server-URL und Benutzername. macOS verlangt praktisch HTTPS.
PROPFIND mit Depth: 1 auf /anna/ listet Firmenkalender und Firmenkontakte.
Schritt 6: Rechte und Persistenz prüfen
Die wichtigste Gegenprobe: Ein zweiter Benutzer darf die Daten des ersten weder lesen noch verändern.
B="-u bernd:SicheresPasswort2"
curl -s -o /dev/null -w '%{http_code}\n' $B $U/anna/firma-kalender/teammeeting.ics
curl -s -o /dev/null -w '%{http_code}\n' $B -X PROPFIND -H 'Depth: 1' $U/anna/
curl -s -o /dev/null -w '%{http_code}\n' $B -X PUT -H 'Content-Type: text/vcard' \
--data-binary @kontakt.vcf $U/anna/kontakte/fremd.vcfAlle drei Aufrufe liefern 403, im Log steht Access to '/anna/' denied for 'bernd'. Auf /bernd/ selbst bekommt bernd 207. Gemeinsame Firmenkalender brauchen statt owner_only die Variante from_file mit Regeldatei oder die Freigaben aus Version 3.7.
Die Daten liegen als Dateien unter data/collections/collection-root/BENUTZER/SAMMLUNG/. Nach docker compose restart sind Termin und Kontakte unverändert da. Der Container läuft als uid=1000(radicale), ein Schreibversuch außerhalb der Volumes endet mit Read-only file system.
fremder Zugriff liefert 403, und nach docker compose restart gibt GET auf Termin und Kontakt weiterhin 200 zurück.
Schritt 7: Reverse Proxy und TLS einrichten
Am einfachsten ist eine eigene Subdomain, dann entfällt der Header X-Script-Name. Mit Caddy genügt:
dav.example.com {
reverse_proxy 127.0.0.1:5232
}Unter einem Pfad wie /radicale/ muss der Proxy den Pfad entfernen und X-Script-Name /radicale setzen, Beispiele liegen im Ordner contrib. Die Pfade /.well-known/caldav und /.well-known/carddav leitet Radicale selbst mit 301 weiter. Ergänzen Sie Fail2ban oder eine Ratenbegrenzung, Radicale verzögert Fehlversuche nur.
curl -I https://dav.example.com/.web/ liefert 200 mit gültigem Zertifikat, curl -I https://dav.example.com/.well-known/caldav eine 301.
Schritt 8: Versionierung, Backup und Restore
Mit dem im Image enthaltenen git protokolliert Radicale jede Änderung. Einmalig initialisieren:
docker compose exec radicale sh -c 'cd /var/lib/radicale/collections && git init -q \
&& git config user.name radicale && git config user.email radicale@localhost \
&& printf ".Radicale.cache\n.Radicale.lock\n.Radicale.tmp-*\n" > .gitignore \
&& git add -A && git commit -q -m init'Dann im Abschnitt [storage] ergänzen und neu starten:
hook = git add -A && (git diff --cached --quiet || git commit -q -m "Changes by \"%(user)s\"")Nach dem nächsten Speichern zeigt git log Changes by "anna". Git ersetzt kein Backup. Dafür genügt ein kaltes Archiv:
$ cd /opt/radicale
$ docker compose stop
$ tar czf /backup/radicale-$(date +%F).tar.gz config data
$ docker compose startDer Restore lässt sich mit echtem Datenverlust prüfen: Adressbuch per DELETE löschen (200, danach 404 auf den Kontakt), dann zurückspielen:
cd /opt/radicale
docker compose stop
rm -rf data
tar xzf /backup/radicale-JJJJ-MM-TT.tar.gz data
docker compose start
# Speicher auf Konsistenz prüfen
docker compose run --rm --no-deps radicale --verify-storageKontakte und Termin sind wieder da, --verify-storage meldet unter anderem Verified collect 'anna/kontakte' (items: 3). Nach einem Restore als root prüfen Sie mit ls -ln data den Eigentümer UID 1000.
tar tzf listet config/users und die .ics- und .vcf-Dateien, nach dem Restore liefert GET auf einen gelöschten Kontakt wieder 200.
Schritt 9: Updates und Rollback
Für ein Update erhöhen Sie das Tag in der compose.yaml, lesen die Release-Notes und starten neu:
$ cd /opt/radicale
$ tar czf /backup/radicale-vor-update-$(date +%F).tar.gz config data
$ docker compose pull
$ docker compose up -d
$ docker compose logs --no-log-prefix | grep 'Starting Radicale'Rollback heißt: altes Tag eintragen und das Backup vor dem Update zurückspielen. Achten Sie in Release-Notes auf geänderte Standardwerte, 3.5.0 hat etwa die Anmeldung ohne Konfiguration abgeschaltet.
Update von 3.8 auf 3.9: Version 3.9.0 behebt laut Release-Notes mehrere Fehler in der Thread-Sicherheit des Login-Caches und der Gruppen- und Freigabeverwaltung. Wer Gruppen oder Freigaben nutzt, sollte das Update deshalb zeitnah einspielen. Dazu kommen Korrekturen bei Zeitraumfiltern, ein neues Recht l, das Eigenschaften gegen Änderungen sperrt, und optional HTTP/2 über Hypercorn ([server] type), das im offiziellen Image nicht enthalten ist. Neue Pflichtoptionen gibt es nicht. Nach dem Update von 3.8.3 auf 3.9.0 mit dieser Compose-Datei ist der Container nach wenigen Sekunden healthy, das Log meldet radicale=3.9.0 und zwei gelesene Benutzer, Termin und Kontakt bleiben unverändert abrufbar, ohne oder mit falschem Passwort kommt 401, mit Passwort 207, und bernd bekommt auf die Daten von anna weiter dreimal 403. --verify-storage läuft danach ohne Fehler.
Die Logzeile Starting Radicale nennt die neue Version, und ein Client synchronisiert ohne Fehlermeldung.
Schritt 10: Radicale entfernen
Achtung, Datenverlust: Die folgenden Befehle löschen alle Kalender, Kontakte und Benutzer unwiderruflich. Ziehen Sie vorher ein Backup und exportieren Sie wichtige Kalender, wenn Sie den Dienst wechseln.
$ cd /opt/radicale
$ docker compose down -v --remove-orphans
$ docker rmi ghcr.io/kozea/radicale:3.9.0
$ sudo rm -rf /opt/radicaledocker ps -a --filter name=radicale zeigt keinen Container mehr, ls /opt/radicale meldet, dass das Verzeichnis nicht existiert.
Grundlagen zum Betrieb finden Sie in Docker Compose unter Linux: Grundlage für Selfhosting, Docker Compose absichern: Secrets, Healthchecks, Non-Root und Linux-Server absichern mit UFW und Fail2ban.
Troubleshooting
- Log
CRITICAL ... Failed to load htpasswd file '/etc/radicale/users': [Errno 2] No such file or directory, Container bleibthealth: starting Benutzerdatei fehlt oder falscher Pfad:
config/usersanlegen, Pfad inhtpasswd_filenameprüfen.- 401 trotz richtigem Passwort, Log
with error 'Invalid salt' Eintrag mit MD5 erzeugt, aber
htpasswd_encryption = bcrypt: Benutzer mithtpasswd -Bneu anlegen oderautodetectsetzen.- 403 beim Speichern, Log
[Errno 13] Permission denied datagehört nicht UID 1000:chown -R 1000:1000 data- 409 mit
<CR:no-uid-conflict /> Gleiche vCard- oder iCal-UID unter anderem Dateinamen: UID im Eintrag eindeutig machen oder vorhandene Datei überschreiben.
- 403 für einen Benutzer auf fremde Pfade
Gewollt durch
owner_only: für gemeinsame Kalenderfrom_filemit Regeldatei nutzen.Bind for 127.0.0.1:5232 failed: port is already allocatedPort schon belegt: anderen Hostport wählen, mit
ss -ltnpden Verursacher suchen.- macOS fragt immer wieder nach dem Passwort
Zugangsdaten über HTTP: nur über HTTPS anbinden.
Häufige Fragen
Kann Radicale Benutzer aus LDAP oder Active Directory nehmen?
Ja, seit 3.3.0 gibt es type = ldap, seit 3.8.0 auch Gruppen daraus. Außerdem möglich sind IMAP, Dovecot, PAM und die Übernahme eines Benutzernamens vom Reverse Proxy.
Reicht Radicale als Ersatz für Exchange-Kalender?
Für persönliche und kleine gemeinsame Kalender ja, für Ressourcenbuchung und Einladungsabläufe eher nicht.
Warum nicht das Image von tomsquest?
Es wird weiter gepflegt, das offizielle Image kommt aber direkt vom Projekt.
Fazit
Radicale ist pflegeleicht: ein Container mit wenigen Dutzend Megabyte RAM, eine Konfigurationsdatei und Daten als lesbare Dateien, was Backup und Restore einfach macht. Grenzen liegen bei Groupware-Funktionen und gemeinsamen Kalendern. Hinter einem Reverse Proxy mit TLS, mit bcrypt und täglichem Backup ist es ein solider Kalenderserver mit kleiner Angriffsfläche.
Weiterführende Anleitungen und Quellen
- Caddy als Reverse Proxy mit automatischem HTTPS: der passende Weg, um Radicale per TLS erreichbar zu machen.
- borgmatic mit Docker für automatische Server-Backups: das tägliche
tar-Archiv verschlüsselt und versioniert auslagern. - lldap mit Docker Compose: zentrales Benutzerverzeichnis, an das Radicale per
type = ldapandocken kann. - Radicale auf GitHub
- Radicale-Dokumentation v3: Konfiguration, Authentifizierung, Rechte, Speicher, Docker
- Offizielle compose.yaml im Repository
- Release v3.9.0 vom 25.10.2026
- Offizielles Container-Image auf GHCR mit Tags
- Reverse-Proxy-Beispiele für nginx, Caddy, Apache
- Community-Image tomsquest/docker-radicale


