Radicale 3.8 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.8.1 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.
Beschrieben für radicale 3.8.1
Mit KI erstellt – redaktionelle Prüfung ausstehend
WerbelinksMit * 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) hatte laut GitHub-API am 30.09.2026 rund 5.063 Sterne, aktuell ist v3.8.1 vom 24.09.2026. Upstream veröffentlicht inzwischen ein eigenes Docker-Image, diese Anleitung nutzt es.
- 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 belegte im Test 26 bis 40 MiB. Mit
mem_limit: 256mbleibt reichlich Reserve. - Speicher: Image 35,7 MB, Testdaten 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 Frei/Gebucht-Abfrage.
| Eckdaten | Wert |
|---|---|
| Image | ghcr.io/kozea/radicale:3.8.1 (am 30.09.2026 identisch mit 3.8, 3 und stable) |
| 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/radicale
latest ist bei Radicale meist ein Nightly-Build (am 30.09.2026 nightly-20260929). Pinnen Sie 3.8.1, damit Updates bewusst erfolgen.
Verifizieren: 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, im Test genutzte 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.8.1
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: 3
Eine .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/.
Verifizieren: 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.8.1 \
-nbB -C 12 anna 'SicheresPasswort1' | sed '/^$/d' > config/users
# Weitere Benutzer anhängen
docker run --rm --entrypoint htpasswd ghcr.io/kozea/radicale:3.8.1 \
-nbB -C 12 bernd 'SicheresPasswort2' | sed '/^$/d' >> config/users
chmod 644 config/users
Das 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 wirkten im Test ohne Neustart.
Verifizieren: 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 ps
Im Log erscheinen Starting Radicale (python=3.14.7 radicale=3.8.1 ...), 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. Auf dem ausgelasteten Testhost dauerte es bis zur ersten Antwort 17 bis 30 Sekunden.
Verifizieren: 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 SUMMARY
Alle vier Schreibaufrufe lieferten 201. GET und REPORT (so synchronisieren Clients) lieferten 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.
Verifizieren: 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.vcf
Im Test kam dreimal 403, im Log stand Access to '/anna/' denied for 'bernd'. Auf /bernd/ selbst bekam 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 waren Termin und Kontakte unverändert da. Der Container lief als uid=1000(radicale), ein Schreibversuch außerhalb der Volumes endete mit Read-only file system.
Verifizieren: 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 weiter, im Test mit 301. Ergänzen Sie Fail2ban oder eine Ratenbegrenzung, Radicale verzögert Fehlversuche nur.
Verifizieren: 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 zeigte git log im Test 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 start
Den Restore hat der Test mit echtem Datenverlust geprüft: Adressbuch per DELETE gelöscht (200, danach 404 auf den Kontakt), dann zurückgespielt:
cd /opt/radicale
docker compose stop
rm -rf data
tar xzf /backup/radicale-2026-09-30.tar.gz data
docker compose start
# Speicher auf Konsistenz prüfen
docker compose run --rm --no-deps radicale --verify-storage
Alle drei Kontakte und der Termin waren wieder da, --verify-storage meldete Verified collect 'anna/kontakte' (items: 3). Nach einem Restore als root prüfen Sie mit ls -ln data den Eigentümer UID 1000.
Verifizieren: 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'
Im Test lieferte docker compose pull denselben Digest, weil 3.8.1 die aktuelle Version war, ein Versionssprung wurde also nicht geprobt. 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.
Verifizieren: 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.8.1
sudo rm -rf /opt/radicale
Verifizieren: docker ps -a --filter name=radicale zeigt keinen Container mehr, ls /opt/radicale meldet, dass das Verzeichnis nicht existiert.
Troubleshooting
| Symptom | Ursache | Lösung |
|---|---|---|
Log CRITICAL ... Failed to load htpasswd file '/etc/radicale/users': [Errno 2] No such file or directory, Container bleibt health: starting | Benutzerdatei fehlt oder falscher Pfad | config/users anlegen, Pfad in htpasswd_filename prüfen |
401 trotz richtigem Passwort, Log with error 'Invalid salt' | Eintrag mit MD5 erzeugt, aber htpasswd_encryption = bcrypt | Benutzer mit htpasswd -B neu anlegen oder autodetect setzen |
403 beim Speichern, Log [Errno 13] Permission denied | data gehö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 Kalender from_file mit Regeldatei nutzen |
Bind for 127.0.0.1:5232 failed: port is already allocated | Port schon belegt | anderen Hostport wählen, mit ss -ltnp den Verursacher suchen |
| macOS fragt immer wieder nach dem Passwort | Zugangsdaten über HTTP | nur über HTTPS anbinden |
Die Meldungen der ersten sechs Zeilen stammen aus dem Testlog.
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 (laut GitHub-API 1.096 Sterne, Release 3.8.1.1 vom 26.09.2026), das offizielle Image kommt aber direkt vom Projekt.
Testumfang
Getestet wurden am 30.09.2026 mit ghcr.io/kozea/radicale:3.8.1 auf x86_64 Start, Healthcheck, bcrypt-Benutzer, Anmeldung mit 401 und 207, Kalender und Adressbuch per MKCALENDAR, MKCOL, PUT, GET und REPORT, Rechtetrennung, Neustart, Git-Hook, Backup und Restore nach Löschung sowie die Fehlerbilder der Tabelle. Nur aus der Dokumentation stammen Reverse Proxy mit TLS, echte Clients wie Thunderbird, DAVx5 und macOS, LDAP, Freigaben per from_file oder Token und ein Update auf eine spätere Version.
Fazit
Radicale ist erstaunlich 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, Popularität laut GitHub-API, Stand 30.09.2026
- Radicale-Dokumentation v3: Konfiguration, Authentifizierung, Rechte, Speicher, Docker
- Offizielle compose.yaml im Repository
- Release v3.8.1 vom 24.09.2026
- Offizielles Container-Image auf GHCR mit Tags
- Reverse-Proxy-Beispiele für nginx, Caddy, Apache
- Community-Image tomsquest/docker-radicale


