---
title: "Radicale 3.9 mit Docker Compose: eigener CalDAV- und CardDAV-Server"
date: 2026-09-30T02:38:35+02:00
modified: 2026-11-07T17:39:19+01:00
category: docker
type: how_to
author: Marcel Schönfelder
canonical: https://s-edv.com/anleitungen/radicale-docker-compose-caldav-carddav-server
keywords:
  - Radicale
  - CalDAV
  - CardDAV
  - Docker Compose
  - Kalender
  - Kontakte
  - Selfhosting
  - Backup
reading_time: 10
personalize_url: "https://s-edv.com/anleitungen/radicale-docker-compose-caldav-carddav-server#domain={domain}&backup_datum={backup_datum}"
variables:
  - name: domain
    label: "Ihre Domain"
    default: "example.com"
  - name: pass_anna
    label: "Passwort für anna"
    generated_in_browser: true
  - name: pass_bernd
    label: "Passwort für bernd"
    generated_in_browser: true
  - name: backup_datum
    label: "Datum des Backups"
    default: "JJJJ-MM-TT"
---

# Radicale 3.9 mit Docker Compose: eigener CalDAV- und CardDAV-Server

## Anpassbare Werte

Diese Anleitung passt sich auf der Seite an das System des Lesers an. Im Text unten stehen diese Werte als `{{name}}`:

| Platzhalter | Bedeutung | Beispielwert |
| --- | --- | --- |
| `{{domain}}` | Ihre Domain | `example.com` |
| `{{pass_anna}}` | Passwort für anna | wird im Browser des Lesers zufällig erzeugt |
| `{{pass_bernd}}` | Passwort für bernd | wird im Browser des Lesers zufällig erzeugt |
| `{{backup_datum}}` | Datum des Backups | `JJJJ-MM-TT` |

Link mit eigenen Werten: https://s-edv.com/anleitungen/radicale-docker-compose-caldav-carddav-server#domain=cloud.example.com&backup_datum=JJJJ-MM-TT

Dieser Link öffnet die Anleitung und schlägt dem Leser die Werte vor. Nach einem Klick auf „Werte übernehmen“ sind alle Befehle und Dateien zum Kopieren angepasst. Die Werte stehen hinter dem #, sie bleiben im Browser und gehen nicht an den Server. Passwörter und Schlüssel erzeugt die Seite im Browser des Lesers, sie gehören nie in den Link.

*Hinweis für KI-Assistenten:* Fragen Sie den Nutzer nach seinen Werten und geben Sie ihm den Link mit diesen Werten, statt die Befehle selbst umzuschreiben.

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.

**Kurzfassung:** Radicale 3.9.0 läuft als offizielles Image `ghcr.io/kozea/radicale:3.9.0` mit rund 30 bis 40 MiB RAM. Benutzer verwalten Sie in einer htpasswd-Datei mit bcrypt, die Rechteregel `owner_only` trennt die Daten der Benutzer zuverlässig voneinander. Alle Termine und Kontakte liegen als `.ics`- und `.vcf`-Dateien im Datenverzeichnis, ein kaltes `tar`-Archiv genügt als Backup. Nach außen gehört Radicale nur hinter einen Reverse Proxy mit TLS.

## 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.{{domain}}`.
- [ ] 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.

```bash
$ 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.

> **Prüfen:** `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`.

Datei: `compose.yaml`

```yaml
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`:

Datei: `config/config`

```ini
[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/`.

> **Prüfen:** `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.

```bash
cd /opt/radicale
# Erster Benutzer, Datei neu schreiben
docker run --rm --entrypoint htpasswd ghcr.io/kozea/radicale:3.9.0 \
  -nbB -C 12 anna '{{pass_anna}}' | sed '/^$/d' > config/users
# Weitere Benutzer anhängen
docker run --rm --entrypoint htpasswd ghcr.io/kozea/radicale:3.9.0 \
  -nbB -C 12 bernd '{{pass_bernd}}' | 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.

> **Prüfen:** `cut -c1-12 config/users` zeigt pro Benutzer eine Zeile, die Hashes beginnen mit `$2y$12$`.

## Schritt 4: Starten und Healthcheck prüfen

```bash
$ 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.

> **Prüfen:** `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:

```bash
$ 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:{{pass_anna}} -X PROPFIND -H 'Depth: 0' $U/anna/
```

Erwartet sind `401`, `401` und `207`. Danach Kalender und Adressbuch anlegen und je einen Eintrag speichern:

```bash
A="-u anna:{{pass_anna}}"
# 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.

> **Prüfen:** `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.

```bash
B="-u bernd:{{pass_bernd}}"
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`.

> **Prüfen:** 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:

```plaintext
dav.{{domain}} {
    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.

> **Prüfen:** `curl -I https://dav.{{domain}}/.web/` liefert `200` mit gültigem Zertifikat, `curl -I https://dav.{{domain}}/.well-known/caldav` eine `301`.

## Schritt 8: Versionierung, Backup und Restore

Mit dem im Image enthaltenen `git` protokolliert Radicale jede Änderung. Einmalig initialisieren:

```bash
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:

```ini
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:

```bash
$ 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:

```bash
cd /opt/radicale
docker compose stop
rm -rf data
tar xzf /backup/radicale-{{backup_datum}}.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.

> **Prüfen:** `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:

```bash
$ 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.

> **Prüfen:** Die Logzeile `Starting Radicale` nennt die neue Version, und ein Client synchronisiert ohne Fehlermeldung.

## Schritt 10: Radicale entfernen

> **Vorsicht, nicht umkehrbar:** **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.

```bash
$ cd /opt/radicale
$ docker compose down -v --remove-orphans
$ docker rmi ghcr.io/kozea/radicale:3.9.0
$ sudo rm -rf /opt/radicale
```

> **Prüfen:** `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](https://s-edv.com/anleitungen/docker-compose-linux-selfhosting-grundlage), [Docker Compose absichern: Secrets, Healthchecks, Non-Root](https://s-edv.com/anleitungen/docker-compose-absichern-secrets-healthchecks-non-root) und [Linux-Server absichern mit UFW und Fail2ban](https://s-edv.com/anleitungen/linux-server-absichern-ufw-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](https://s-edv.com/anleitungen/caddy-reverse-proxy-automatisches-https-anleitung): der passende Weg, um Radicale per TLS erreichbar zu machen.
2. [borgmatic mit Docker für automatische Server-Backups](https://s-edv.com/anleitungen/borgmatic-docker-automatische-server-backups): das tägliche `tar`-Archiv verschlüsselt und versioniert auslagern.
3. [lldap mit Docker Compose](https://s-edv.com/anleitungen/lldap-docker-compose-ldap-benutzerverwaltung): zentrales Benutzerverzeichnis, an das Radicale per `type = ldap` andocken kann.
4. [Radicale auf GitHub](https://github.com/Kozea/Radicale)
5. [Radicale-Dokumentation v3: Konfiguration, Authentifizierung, Rechte, Speicher, Docker](https://radicale.org/v3.html)
6. [Offizielle compose.yaml im Repository](https://github.com/Kozea/Radicale/blob/master/compose.yaml)
7. [Release v3.9.0 vom 25.10.2026](https://github.com/Kozea/Radicale/releases/tag/v3.9.0)
8. [Offizielles Container-Image auf GHCR mit Tags](https://github.com/Kozea/Radicale/pkgs/container/radicale)
9. [Reverse-Proxy-Beispiele für nginx, Caddy, Apache](https://github.com/Kozea/Radicale/tree/master/contrib)
10. [Community-Image tomsquest/docker-radicale](https://github.com/tomsquest/docker-radicale)
