Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Docker 30.09.2026 · 10 min Lesezeit Geprüft 26.10.2026

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

Titelbild: Radicale mit Docker Compose, CalDAV, CardDAV, Backup

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/amd64 und linux/arm64, also auch für Raspberry Pi 4 und 5.
  • RAM: Der Container belegt 26 bis 40 MiB. Mit mem_limit: 256m bleibt 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.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 Ressourcenbuchung. Eine einfache Frei/Gebucht-Ansicht gibt es seit 3.8.2.
EckdatenWert
Imageghcr.io/kozea/radicale:3.9.0
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. 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: 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
  1. [auth] type = htpasswd: Ohne diesen Abschnitt lehnt Radicale seit 3.5.0 jede Anmeldung ab.
  2. htpasswd_encryption = bcrypt: nur bcrypt-Hashes, autodetect erkennt das Verfahren pro Zeile.
  3. delay = 1: Verzögerung nach Fehlversuchen.
  4. 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/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 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 ps

Im 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 SUMMARY

Alle 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.vcf

Alle 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 start

Der 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-storage

Kontakte 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/radicale

docker 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 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.

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

  1. Caddy als Reverse Proxy mit automatischem HTTPS: der passende Weg, um Radicale per TLS erreichbar zu machen.
  2. borgmatic mit Docker für automatische Server-Backups: das tägliche tar-Archiv verschlüsselt und versioniert auslagern.
  3. lldap mit Docker Compose: zentrales Benutzerverzeichnis, an das Radicale per type = ldap andocken kann.
  4. Radicale auf GitHub
  5. Radicale-Dokumentation v3: Konfiguration, Authentifizierung, Rechte, Speicher, Docker
  6. Offizielle compose.yaml im Repository
  7. Release v3.9.0 vom 25.10.2026
  8. Offizielles Container-Image auf GHCR mit Tags
  9. Reverse-Proxy-Beispiele für nginx, Caddy, Apache
  10. Community-Image tomsquest/docker-radicale
RadicaleCalDAVCardDAVDocker ComposeKalenderKontakteSelfhostingBackup