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.
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
- Open Evolution → Edit → Accounts → Add.
- For calendar: choose CalDAV. URL:
https://cal.example.com/alice/. Enter credentials when prompted. - For contacts: choose CardDAV, same base URL.
- Evolution will auto-discover collections under that path on first sync.
Thunderbird (with TbSync + Provider for CalDAV & CardDAV)
- Install the TbSync add-on and Provider for CalDAV & CardDAV from addons.thunderbird.net.
- Tools → TbSync → Add account → CalDAV & CardDAV.
- Choose Manual configuration. Set server URL to
https://cal.example.com, enter username and password. - Click Synchronize; available calendars and address books will appear.
iOS
- Settings → Calendar → Accounts → Add Account → Other → Add CalDAV Account.
- Server:
cal.example.com, User:alice, Password, Description: anything. - For contacts: Settings → Contacts → Accounts → Add Account → Other → Add CardDAV Account. Same fields.
- iOS performs well-known autodiscovery. If it fails, use the full path:
https://cal.example.com/alice/.
Android (DAVx⁵)
- Install DAVx⁵ from F-Droid (preferred) or the Play Store.
- Add account → Login with URL and username.
- Base URL:
https://cal.example.com/alice/, enter credentials. - 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 radicalemanually to see Python tracebacks directly. - iOS/Android won't discover: Add
/.well-known/caldavand/.well-known/carddavredirects in Nginx pointing tohttps://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/collectionsis owned by theradicaleuser:sudo chown -R radicale:radicale /var/lib/radicale.
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
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.