History of Radicale Cribsheet
<h2>Radicale CalDAV/CardDAV Server - Key Points</h2>
<h3>Installing</h3>
<pre>
zypper install radicale
# Creates radicale:radicale system user automatically
</pre>
<h3>Configuration `/etc/radicale/config`</h3>
<p>Key active settings:</p>
<pre>
[server]
hosts = localhost:5232
[auth]
type = htpasswd
htpasswd_filename = /etc/radicale/users
htpasswd_encryption = autodetect
[storage]
filesystem_folder = /srv/radicale/collections
</pre>
<h3>Critical systemd override</h3>
<p>Default service restricts writes to `/var/lib/radicale/` — must add `/srv/radicale/` to allowed paths. Copy service file to webstack and add:</p>
<pre>
ReadWritePaths=/var/lib/radicale/collections /srv/radicale/
</pre>
<h3>Storage setup</h3>
<pre>
mkdir -p /srv/radicale/collections
chown -R radicale:radicale /srv/radicale
chmod 755 /srv/radicale
chmod 755 /srv/radicale/collections
</pre>
<p>**Critical — directories need 755 not 644!**</p>
<h3>Collection structure</h3>
<pre>
# Create directory structure
mkdir -p /srv/radicale/collections/collection-root/lsces/calendar
mkdir -p /srv/radicale/collections/collection-root/lsces/contacts
# Props files &mdash; collection-root needs one too!
echo &#39;{}&#39; &gt; /srv/radicale/collections/collection-root/.Radicale.props
cat &gt; /srv/radicale/collections/collection-root/lsces/.Radicale.props &lt;&lt; &#39;EOF&#39;
{&quot;D:displayname&quot;: &quot;lsces&quot;}
EOF
cat &gt; /srv/radicale/collections/collection-root/lsces/calendar/.Radicale.props &lt;&lt; &#39;EOF&#39;
{&quot;C:supported-calendar-component-set&quot;: &quot;VEVENT,VJOURNAL,VTODO&quot;, &quot;D:displayname&quot;: &quot;Personal Calendar&quot;, &quot;tag&quot;: &quot;VCALENDAR&quot;}
EOF
cat &gt; /srv/radicale/collections/collection-root/lsces/contacts/.Radicale.props &lt;&lt; &#39;EOF&#39;
{&quot;D:displayname&quot;: &quot;Personal Contacts&quot;, &quot;tag&quot;: &quot;VADDRESSBOOK&quot;}
EOF
chown -R radicale:radicale /srv/radicale/collections
</pre>
<h3>Create user</h3>
<pre>
htpasswd -B -c /etc/radicale/users lsces
</pre>
<h3>Enable and start</h3>
<pre>
systemctl daemon-reload
systemctl enable radicale
systemctl start radicale
systemctl status radicale
</pre>
<h3>nginx reverse proxy</h3>
<pre>
location /dav/ {
proxy_pass http://localhost:5232/;
proxy_set_header X-Script-Name /dav;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass_header Authorization;
proxy_set_header Authorization $http_authorization;
}
</pre>
<p><strong>Both Authorization headers needed — nginx strips it without explicit passthrough!</strong></p>
<h3>Migrating from Nextcloud</h3>
<p><strong>Calendar export </strong>— Nextcloud UI:<br />
Calendar → Edit → Export → saves `.ics`<br />
Radicale requires **one event per file** — split the export:</p>
<pre>
import re, os
with open(&#39;personal.ics&#39;, &#39;r&#39;) as f:
content = f.read()
header = content[:content.index(&#39;BEGIN:VEVENT&#39;)]
footer = &#39;END:VCALENDARn&#39;
events = re.findall(&#39;BEGIN:VEVENT.*?END:VEVENT&#39;, content, re.DOTALL)
os.makedirs(&#39;/srv/radicale/split&#39;, exist_ok=True)
for event in events:
uid = re.search(&#39;UID:(.*?)n&#39;, event).group(1).strip()
with open(f&#39;/srv/radicale/split/{uid}.ics&#39;, &#39;w&#39;) as f:
f.write(header)
f.write(event + &#39;n&#39;)
f.write(footer)
print(f&#39;Split {len(events)} events&#39;)
</pre>
<p>**Contacts export** — Nextcloud UI: Contacts → Settings → Export → saves `.vcf` Same split needed for contacts:</p>
<pre>
import re, os
with open(&#39;contacts.vcf&#39;, &#39;r&#39;) as f:
content = f.read()
contacts = re.findall(&#39;BEGIN:VCARD.*?END:VCARD&#39;, content, re.DOTALL)
os.makedirs(&#39;/srv/radicale/split_contacts&#39;, exist_ok=True)
for contact in contacts:
uid = re.search(&#39;UID:(.*?)n&#39;, contact)
uid = uid.group(1).strip() if uid else f&#39;contact_{contacts.index(contact)}&#39;
with open(f&#39;/srv/radicale/split_contacts/{uid}.vcf&#39;, &#39;w&#39;) as f:
f.write(contact + &#39;n&#39;)
print(f&#39;Split {len(contacts)} contacts&#39;)
</pre>
<p>**Import via curl:**</p>
<pre>
# Calendar events
for f in /srv/radicale/split/*.ics; do
uid=$(basename &quot;$f&quot; .ics)
curl -s -u lsces:password
-X PUT
-H &quot;Content-Type: text/calendar&quot;
--data-binary @&quot;$f&quot;
&quot;https://rdm1.uk/dav/lsces/calendar/${uid}.ics&quot;
echo &quot;$uid&quot;
done
# Contacts
for f in /srv/radicale/split_contacts/*.vcf; do
uid=$(basename &quot;$f&quot; .vcf)
curl -s -u lsces:password
-X PUT
-H &quot;Content-Type: text/vcard&quot;
--data-binary @&quot;$f&quot;
&quot;https://rdm1.uk/dav/lsces/contacts/${uid}.vcf&quot;
echo &quot;$uid&quot;
done
</pre>
<h3>Client configuration</h3>
<ul>
<li><strong>Thunderbird</strong> — Add new calendar/contacts:</li>
<li>Network → CalDAV/CardDAV</li>
<li>URL: https://yourdomain/dav/username/calendar/</li>
</ul>
<ul>
<li><strong>DAVx⁵ (Android)</strong> — Add account:</li>
<li>Base URL:https://yourdomain/dav/username/ </li>
<li>Auto-discovers both calendar and contacts</li>
</ul>
<p><em>Developed with <a href="https://claude.ai">Claude AI</a>
assistance - Anthropic - April 2026</em></p>
<h3>Installing</h3>
<pre>
zypper install radicale
# Creates radicale:radicale system user automatically
</pre>
<h3>Configuration `/etc/radicale/config`</h3>
<p>Key active settings:</p>
<pre>
[server]
hosts = localhost:5232
[auth]
type = htpasswd
htpasswd_filename = /etc/radicale/users
htpasswd_encryption = autodetect
[storage]
filesystem_folder = /srv/radicale/collections
</pre>
<h3>Critical systemd override</h3>
<p>Default service restricts writes to `/var/lib/radicale/` — must add `/srv/radicale/` to allowed paths. Copy service file to webstack and add:</p>
<pre>
ReadWritePaths=/var/lib/radicale/collections /srv/radicale/
</pre>
<h3>Storage setup</h3>
<pre>
mkdir -p /srv/radicale/collections
chown -R radicale:radicale /srv/radicale
chmod 755 /srv/radicale
chmod 755 /srv/radicale/collections
</pre>
<p>**Critical — directories need 755 not 644!**</p>
<h3>Collection structure</h3>
<pre>
# Create directory structure
mkdir -p /srv/radicale/collections/collection-root/lsces/calendar
mkdir -p /srv/radicale/collections/collection-root/lsces/contacts
# Props files &mdash; collection-root needs one too!
echo &#39;{}&#39; &gt; /srv/radicale/collections/collection-root/.Radicale.props
cat &gt; /srv/radicale/collections/collection-root/lsces/.Radicale.props &lt;&lt; &#39;EOF&#39;
{&quot;D:displayname&quot;: &quot;lsces&quot;}
EOF
cat &gt; /srv/radicale/collections/collection-root/lsces/calendar/.Radicale.props &lt;&lt; &#39;EOF&#39;
{&quot;C:supported-calendar-component-set&quot;: &quot;VEVENT,VJOURNAL,VTODO&quot;, &quot;D:displayname&quot;: &quot;Personal Calendar&quot;, &quot;tag&quot;: &quot;VCALENDAR&quot;}
EOF
cat &gt; /srv/radicale/collections/collection-root/lsces/contacts/.Radicale.props &lt;&lt; &#39;EOF&#39;
{&quot;D:displayname&quot;: &quot;Personal Contacts&quot;, &quot;tag&quot;: &quot;VADDRESSBOOK&quot;}
EOF
chown -R radicale:radicale /srv/radicale/collections
</pre>
<h3>Create user</h3>
<pre>
htpasswd -B -c /etc/radicale/users lsces
</pre>
<h3>Enable and start</h3>
<pre>
systemctl daemon-reload
systemctl enable radicale
systemctl start radicale
systemctl status radicale
</pre>
<h3>nginx reverse proxy</h3>
<pre>
location /dav/ {
proxy_pass http://localhost:5232/;
proxy_set_header X-Script-Name /dav;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass_header Authorization;
proxy_set_header Authorization $http_authorization;
}
</pre>
<p><strong>Both Authorization headers needed — nginx strips it without explicit passthrough!</strong></p>
<h3>Migrating from Nextcloud</h3>
<p><strong>Calendar export </strong>— Nextcloud UI:<br />
Calendar → Edit → Export → saves `.ics`<br />
Radicale requires **one event per file** — split the export:</p>
<pre>
import re, os
with open(&#39;personal.ics&#39;, &#39;r&#39;) as f:
content = f.read()
header = content[:content.index(&#39;BEGIN:VEVENT&#39;)]
footer = &#39;END:VCALENDARn&#39;
events = re.findall(&#39;BEGIN:VEVENT.*?END:VEVENT&#39;, content, re.DOTALL)
os.makedirs(&#39;/srv/radicale/split&#39;, exist_ok=True)
for event in events:
uid = re.search(&#39;UID:(.*?)n&#39;, event).group(1).strip()
with open(f&#39;/srv/radicale/split/{uid}.ics&#39;, &#39;w&#39;) as f:
f.write(header)
f.write(event + &#39;n&#39;)
f.write(footer)
print(f&#39;Split {len(events)} events&#39;)
</pre>
<p>**Contacts export** — Nextcloud UI: Contacts → Settings → Export → saves `.vcf` Same split needed for contacts:</p>
<pre>
import re, os
with open(&#39;contacts.vcf&#39;, &#39;r&#39;) as f:
content = f.read()
contacts = re.findall(&#39;BEGIN:VCARD.*?END:VCARD&#39;, content, re.DOTALL)
os.makedirs(&#39;/srv/radicale/split_contacts&#39;, exist_ok=True)
for contact in contacts:
uid = re.search(&#39;UID:(.*?)n&#39;, contact)
uid = uid.group(1).strip() if uid else f&#39;contact_{contacts.index(contact)}&#39;
with open(f&#39;/srv/radicale/split_contacts/{uid}.vcf&#39;, &#39;w&#39;) as f:
f.write(contact + &#39;n&#39;)
print(f&#39;Split {len(contacts)} contacts&#39;)
</pre>
<p>**Import via curl:**</p>
<pre>
# Calendar events
for f in /srv/radicale/split/*.ics; do
uid=$(basename &quot;$f&quot; .ics)
curl -s -u lsces:password
-X PUT
-H &quot;Content-Type: text/calendar&quot;
--data-binary @&quot;$f&quot;
&quot;https://rdm1.uk/dav/lsces/calendar/${uid}.ics&quot;
echo &quot;$uid&quot;
done
# Contacts
for f in /srv/radicale/split_contacts/*.vcf; do
uid=$(basename &quot;$f&quot; .vcf)
curl -s -u lsces:password
-X PUT
-H &quot;Content-Type: text/vcard&quot;
--data-binary @&quot;$f&quot;
&quot;https://rdm1.uk/dav/lsces/contacts/${uid}.vcf&quot;
echo &quot;$uid&quot;
done
</pre>
<h3>Client configuration</h3>
<ul>
<li><strong>Thunderbird</strong> — Add new calendar/contacts:</li>
<li>Network → CalDAV/CardDAV</li>
<li>URL: https://yourdomain/dav/username/calendar/</li>
</ul>
<ul>
<li><strong>DAVx⁵ (Android)</strong> — Add account:</li>
<li>Base URL:https://yourdomain/dav/username/ </li>
<li>Auto-discovers both calendar and contacts</li>
</ul>
<p><em>Developed with <a href="https://claude.ai">Claude AI</a>
assistance - Anthropic - April 2026</em></p>
