$linuxjunkies
>

Self-Host CalDAV/CardDAV with Radicale

Set up Radicale as a self-hosted CalDAV/CardDAV server with htpasswd auth, Nginx reverse proxy, and sync to Evolution, Thunderbird, iOS, and Android.

IntermediateUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • A domain name pointed at your server with a valid TLS certificate
  • Python 3.8+ and pip installed
  • Nginx installed and able to serve HTTPS
  • Root or sudo access on the server

Radicale is a lightweight CalDAV and CardDAV server written in Python. It handles calendars, contacts, and to-do lists without a database engine, storing everything as plain files on disk. If you want a self-hosted alternative to Google Calendar and Contacts that you can run on a $5 VPS or a Raspberry Pi, Radicale is the right tool. This guide covers installation, htpasswd-based authentication, TLS termination via a reverse proxy, and syncing with Evolution, Thunderbird, iOS, and Android.

Prerequisites

  • A server with a public domain name or internal hostname (e.g. cal.example.com)
  • Python 3.8 or newer and pip available
  • Nginx or Caddy already installed (for the reverse proxy step)
  • A valid TLS certificate — Let's Encrypt via Certbot or Caddy's automatic ACME works well

Install Radicale

The cleanest approach is a dedicated system user plus a Python virtual environment. This keeps Radicale isolated from system packages.

Create a system user

sudo useradd --system --home /var/lib/radicale --create-home --shell /usr/sbin/nologin radicale

Install into a virtual environment

sudo python3 -m venv /opt/radicale
sudo /opt/radicale/bin/pip install --upgrade pip radicale bcrypt

The bcrypt package enables secure password hashing. Install it now so you do not have to restart the service later.

Configure Radicale

sudo mkdir -p /etc/radicale
sudo nano /etc/radicale/config

Paste the following, adjusting paths as needed:

[server]
hosts = 127.0.0.1:5232

[auth]
type = htpasswd
htpasswd_filename = /etc/radicale/htpasswd
htpasswd_encryption = bcrypt

[storage]
filesystem_folder = /var/lib/radicale/collections

[logging]
level = warning

Radicale listens only on localhost because Nginx will handle public TLS traffic. Never expose port 5232 directly to the internet without TLS.

Create User Credentials

Radicale reads credentials from a standard htpasswd file. The htpasswd utility ships with Apache's tools package.

Debian / Ubuntu

sudo apt install apache2-utils

Fedora / RHEL / Rocky

sudo dnf install httpd-tools

Arch

sudo pacman -S apache

Create the file and add your first user (replace alice with your username):

sudo htpasswd -B -c /etc/radicale/htpasswd alice

The -B flag forces bcrypt hashing, matching the config. For every additional user, drop -c to avoid overwriting the file:

sudo htpasswd -B /etc/radicale/htpasswd bob
sudo chown radicale:radicale /etc/radicale/htpasswd
sudo chmod 640 /etc/radicale/htpasswd

Run Radicale as a systemd Service

sudo nano /etc/systemd/system/radicale.service
[Unit]
Description=Radicale CalDAV/CardDAV server
After=network.target

[Service]
ExecStart=/opt/radicale/bin/python -m radicale
Restart=on-failure
User=radicale
Group=radicale
ReadWritePaths=/var/lib/radicale /etc/radicale

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now radicale

Verify it started cleanly:

sudo systemctl status radicale

Output should show active (running). If it failed, run journalctl -u radicale -n 50 for detail.

Reverse Proxy with Nginx

Add a server block for your domain. If you already have a site config, add the location block inside your existing HTTPS server context.

sudo nano /etc/nginx/sites-available/radicale
server {
    listen 443 ssl http2;
    server_name cal.example.com;

    ssl_certificate     /etc/letsencrypt/live/cal.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cal.example.com/privkey.pem;

    location / {
        proxy_pass         http://127.0.0.1:5232;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Script-Name   "";
        proxy_set_header   Host            $host;
        proxy_pass_header  Authorization;
    }
}

server {
    listen 80;
    server_name cal.example.com;
    return 301 https://$host$request_uri;
}

Important: proxy_pass_header Authorization is required — without it, Nginx strips the Authorization header and every request returns 401.

sudo ln -s /etc/nginx/sites-available/radicale /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

If you prefer Caddy, its configuration is simpler — a reverse_proxy localhost:5232 directive inside your site block handles TLS automatically.

Verify the Server Is Working

curl -u alice:yourpassword https://cal.example.com/.web/

A 200 response with HTML content confirms authentication and proxying are working. An empty 401 means the Authorization header is being dropped — check the proxy_pass_header line.

Client Setup

GNOME Evolution

  1. Open Evolution → Edit → Accounts → Add.
  2. For calendar: choose CalDAV. URL: https://cal.example.com/alice/. Enter credentials when prompted.
  3. For contacts: choose CardDAV, same base URL.
  4. Evolution will auto-discover collections under that path on first sync.

Thunderbird (with TbSync + Provider for CalDAV & CardDAV)

  1. Install the TbSync add-on and Provider for CalDAV & CardDAV from addons.thunderbird.net.
  2. Tools → TbSync → Add account → CalDAV & CardDAV.
  3. Choose Manual configuration. Set server URL to https://cal.example.com, enter username and password.
  4. Click Synchronize; available calendars and address books will appear.

iOS

  1. Settings → Calendar → Accounts → Add Account → Other → Add CalDAV Account.
  2. Server: cal.example.com, User: alice, Password, Description: anything.
  3. For contacts: Settings → Contacts → Accounts → Add Account → Other → Add CardDAV Account. Same fields.
  4. iOS performs well-known autodiscovery. If it fails, use the full path: https://cal.example.com/alice/.

Android (DAVx⁵)

  1. Install DAVx⁵ from F-Droid (preferred) or the Play Store.
  2. Add account → Login with URL and username.
  3. Base URL: https://cal.example.com/alice/, enter credentials.
  4. DAVx⁵ discovers all calendars and address books and lets you select which to sync. Enable background sync in Android battery settings for DAVx⁵ to avoid sync interruptions.

Firewall

Only ports 80 and 443 should be publicly reachable. Port 5232 must stay local.

ufw (Ubuntu/Debian default)

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 5232/tcp

firewalld (Fedora/RHEL/Rocky)

sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --permanent --remove-port=5232/tcp
sudo firewall-cmd --reload

Troubleshooting

  • 401 on every request: Confirm proxy_pass_header Authorization; is present in your Nginx config and that you reloaded Nginx after editing.
  • Collections not found (404): Radicale creates a user's collection directory on first login. Try POSTing a calendar via the client rather than expecting a bare directory to exist. The path format is /username/collection-name/.
  • Service fails to start: Run sudo -u radicale /opt/radicale/bin/python -m radicale manually to see Python tracebacks directly.
  • iOS/Android won't discover: Add /.well-known/caldav and /.well-known/carddav redirects in Nginx pointing to https://cal.example.com/ with a 301 redirect. Many mobile clients rely on these well-known URIs.
  • Permission errors on storage: Check that /var/lib/radicale/collections is owned by the radicale user: sudo chown -R radicale:radicale /var/lib/radicale.
tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

Frequently asked questions

Can Radicale handle multiple users sharing a calendar?
Not natively — Radicale's default rights model gives each user access only to their own collections. You can set 'type = owner_write' or 'type = authenticated' in the [rights] section to allow broader access, but shared calendars with fine-grained ACLs are not a built-in feature.
How do I back up Radicale data?
Everything is stored as plain RFC 4791/6352 files under /var/lib/radicale/collections. A simple rsync, tar, or restic backup of that directory is sufficient. No database dump is needed.
Does Radicale support two-factor authentication?
Not directly. Radicale's auth system is pluggable (htpasswd, IMAP, LDAP via custom scripts), but CalDAV/CardDAV protocols do not carry TOTP tokens. If you need 2FA, place Radicale behind an SSO proxy such as Authelia.
Will this work on a server with an IP address only and no domain name?
You can use an IP address, but TLS becomes awkward — Let's Encrypt won't issue certificates for bare IPs. You would need a self-signed certificate and configure each client to trust it, which is cumbersome on iOS and Android. A free subdomain from a dynamic DNS provider is the easier path.
How do I upgrade Radicale after installation?
Run 'sudo /opt/radicale/bin/pip install --upgrade radicale' then 'sudo systemctl restart radicale'. The flat-file storage format has been stable across Radicale 3.x releases, but check the changelog before upgrading across major versions.

Related guides