Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Docker 30.09.2026 · 11 min Lesezeit

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

Schematische Grafik: Radicale-Server verbunden mit Smartphone, Laptop und Tablet, dazu Karten für CalDAV, CardDAV sowie Backup und Restore

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/amd64 und linux/arm64, also auch für Raspberry Pi 4 und 5.
  • RAM: Der Container belegte im Test 26 bis 40 MiB. Mit mem_limit: 256m bleibt 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.1 gebunden. Für Zugriff von außen einen Reverse Proxy mit TLS auf Port 443 und eine eigene Subdomain, etwa dav.example.com.
  • Grenzen: kein Groupware-Server, keine Einladungen per Mail, keine Frei/Gebucht-Abfrage.
EckdatenWert
Imageghcr.io/kozea/radicale:3.8.1 (am 30.09.2026 identisch mit 3.8, 3 und stable)
Port5232 im Container
Volumes/etc/radicale (Konfiguration, Benutzer), /var/lib/radicale (Daten)
Benutzer im Containerradicale, UID und GID 1000
Umgebungsvariablenkeine 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, autodetect erkennt 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

SymptomUrsacheLösung
Log CRITICAL ... Failed to load htpasswd file '/etc/radicale/users': [Errno 2] No such file or directory, Container bleibt health: startingBenutzerdatei fehlt oder falscher Pfadconfig/users anlegen, Pfad in htpasswd_filename prüfen
401 trotz richtigem Passwort, Log with error 'Invalid salt'Eintrag mit MD5 erzeugt, aber htpasswd_encryption = bcryptBenutzer mit htpasswd -B neu anlegen oder autodetect setzen
403 beim Speichern, Log [Errno 13] Permission denieddata gehört nicht UID 1000chown -R 1000:1000 data
409 mit <CR:no-uid-conflict />gleiche vCard- oder iCal-UID unter anderem DateinamenUID im Eintrag eindeutig machen oder vorhandene Datei überschreiben
403 für einen Benutzer auf fremde Pfadegewollt durch owner_onlyfür gemeinsame Kalender from_file mit Regeldatei nutzen
Bind for 127.0.0.1:5232 failed: port is already allocatedPort schon belegtanderen Hostport wählen, mit ss -ltnp den Verursacher suchen
macOS fragt immer wieder nach dem PasswortZugangsdaten über HTTPnur ü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

RadicaleCalDAVCardDAVDocker ComposeKalenderKontakteSelfhostingBackup