Calendar beginner ⏱ 30 minutes

Radicale: Lightweight CalDAV/CardDAV Server

Simple, resource-efficient calendar and contacts server. Perfect for small homelabs.

By Homelab Team • 2025-01-15
#radicale#caldav#carddav#calendar#contacts

In the modern digital landscape, our calendars and contacts are often stored in the cloud by major tech companies. While convenient, this means surrendering control and privacy of your personal scheduling data. Radicale offers an elegant solution: a lightweight, Python-based CalDAV and CardDAV server that you can run on minimal hardware while maintaining complete control over your data.

This comprehensive guide will walk you through deploying Radicale in your homelab, from basic setup to advanced configurations with authentication, reverse proxies, and client configurations across all major platforms.

What is Radicale?

Radicale is an open-source CalDAV (calendars) and CardDAV (contacts) server designed with simplicity and minimalism in mind. Unlike heavyweight solutions, Radicale is incredibly lightweight, requiring minimal system resources while providing full standards compliance.

Why Choose Radicale?

1. Minimal Resource Footprint

Radicale is written in Python and designed to be lean. It runs smoothly on:

  • Raspberry Pi (even older models)
  • Docker containers with 64MB RAM limit
  • Shared hosting environments
  • Alongside other homelab services

2. Simple Configuration

No complex database setup required. Radicale stores data in simple filesystem structures, making backups and migration straightforward. The configuration file is intuitive and well-documented.

3. Standards Compliant

Full support for CalDAV and CardDAV standards means compatibility with virtually every calendar and contacts application:

  • iOS and macOS (native support)
  • Android (with DAVx5)
  • Thunderbird
  • Evolution
  • Outlook (with plugins)

4. Security First

Built-in authentication options including:

  • HTTP Basic/Digest authentication
  • htpasswd file support
  • LDAP integration
  • Custom authentication backends

Radicale vs. Alternatives

FeatureRadicaleBaikalNextcloud
RAM Usage50-100MB200-300MB512MB+
Setup ComplexityVery SimpleSimpleComplex
Web InterfaceMinimalFull Admin PanelFull Suite
Database RequiredNoSQLite/MySQLMySQL/PostgreSQL
Best ForMinimalistsBalanceFull Features

Architecture Overview

Our Radicale deployment will include:

Internet
   |
   v
Reverse Proxy (Traefik/Nginx)
   |
   v
Radicale Container
   |
   v
Volume Storage (Calendar/Contact Data)

This setup provides:

  • HTTPS encryption via reverse proxy
  • HTTP Basic authentication
  • Persistent data storage
  • Easy backup and restore

Prerequisites

Before starting, ensure you have:

  • A Linux server or VM (1 CPU, 512MB RAM minimum)
  • Docker and Docker Compose installed
  • Basic understanding of terminal commands
  • A domain name with DNS access (for SSL)
  • Port 80/443 access for reverse proxy

Deployment: Getting Radicale Running

Let’s deploy Radicale with Docker Compose, which provides the most flexible and maintainable setup.

Step 1: Create Project Directory

# Create project directory
mkdir -p ~/radicale && cd ~/radicale

# Create directories for data and configuration
mkdir -p data config

Step 2: Create Configuration File

Create a configuration file at config/config:

[server]
# Bind to all interfaces (Docker networking)
hosts = 0.0.0.0:5232

# Maximum number of parallel connections
max_connections = 20

# Maximum size for request body (10MB)
max_content_length = 10000000

# Timeout for client connections (seconds)
timeout = 30

[encoding]
# Encoding for requests and responses
request = utf-8
stock = utf-8

[auth]
# Authentication type: none, htpasswd, remote_user, http_x_remote_user
type = htpasswd

# Path to htpasswd file
htpasswd_filename = /config/users
htpasswd_encryption = bcrypt

[rights]
# Rights management: none, owner_only, owner_write, from_file
type = owner_only

[storage]
# Storage backend
type = multifs

# Filesystem folder for collections
filesystem_folder = /data/collections

[web]
# Web interface: none or internal
type = internal

[logging]
# Logging level: debug, info, warning, error, critical
level = info

# Enable full environment on crash
full_environment = false

Step 3: Create User Password File

# Install apache2-utils for htpasswd command
sudo apt update && sudo apt install apache2-utils -y

# Create htpasswd file with your first user
# Replace 'username' with your desired username
htpasswd -B -c config/users username

# Add additional users (without -c flag)
htpasswd -B config/users anotheruser

Step 4: Create Docker Compose Configuration

Create docker-compose.yml:

version: '3.8'

services:
  radicale:
    image: tomsquest/docker-radicale:latest
    container_name: radicale
    restart: unless-stopped
    ports:
      - "5232:5232"
    volumes:
      - ./data:/data
      - ./config:/config:ro
    environment:
      - TZ=America/New_York
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5232"]
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - radicale-net

networks:
  radicale-net:
    driver: bridge

Step 5: Launch Radicale

# Start the service
docker-compose up -d

# Check logs
docker-compose logs -f

# Verify it's running
curl http://localhost:5232

You should see a basic web interface at http://YOUR-SERVER-IP:5232

Setting Up Reverse Proxy with SSL

For production use, you’ll want HTTPS encryption. Let’s configure both Traefik and Nginx options.

Option 1: Traefik Reverse Proxy

If you’re using Traefik, update your docker-compose.yml:

version: '3.8'

services:
  radicale:
    image: tomsquest/docker-radicale:latest
    container_name: radicale
    restart: unless-stopped
    volumes:
      - ./data:/data
      - ./config:/config:ro
    environment:
      - TZ=America/New_York
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.radicale.rule=Host(`cal.yourdomain.com`)"
      - "traefik.http.routers.radicale.entrypoints=websecure"
      - "traefik.http.routers.radicale.tls.certresolver=letsencrypt"
      - "traefik.http.services.radicale.loadbalancer.server.port=5232"
      # Middleware for security headers
      - "traefik.http.routers.radicale.middlewares=radicale-headers"
      - "traefik.http.middlewares.radicale-headers.headers.sslredirect=true"
      - "traefik.http.middlewares.radicale-headers.headers.stsSeconds=31536000"
      - "traefik.http.middlewares.radicale-headers.headers.stsIncludeSubdomains=true"
      - "traefik.http.middlewares.radicale-headers.headers.stsPreload=true"
    networks:
      - traefik-net

networks:
  traefik-net:
    external: true

Option 2: Nginx Reverse Proxy

Create an Nginx configuration file nginx/radicale.conf:

upstream radicale {
    server radicale:5232;
}

server {
    listen 80;
    server_name cal.yourdomain.com;

    # Redirect all HTTP to HTTPS
    return 301 https://cal.yourdomain.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name cal.yourdomain.com;

    # SSL Configuration
    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Security Headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Logging
    access_log /var/log/nginx/radicale_access.log;
    error_log /var/log/nginx/radicale_error.log;

    location / {
        proxy_pass http://radicale;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Script-Name /;

        # Disable buffering for CalDAV/CardDAV
        proxy_buffering off;
        proxy_request_buffering off;

        # Timeouts
        proxy_connect_timeout 300;
        proxy_send_timeout 300;
        proxy_read_timeout 300;
        send_timeout 300;
    }
}

Add Nginx service to docker-compose.yml:

  nginx:
    image: nginx:alpine
    container_name: radicale-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/radicale.conf:/etc/nginx/conf.d/default.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - radicale
    networks:
      - radicale-net

Obtaining SSL Certificates

# Using Certbot for Let's Encrypt
sudo apt install certbot -y

# Generate certificate
sudo certbot certonly --standalone   -d cal.yourdomain.com   --email [email protected]   --agree-tos

# Create SSL directory and copy certificates
mkdir -p ssl
sudo cp /etc/letsencrypt/live/cal.yourdomain.com/fullchain.pem ssl/
sudo cp /etc/letsencrypt/live/cal.yourdomain.com/privkey.pem ssl/
sudo chown $USER:$USER ssl/*.pem

Client Configuration

Now let’s connect various devices and applications to your Radicale server.

iOS and macOS (Native Support)

Calendar Setup:

  1. Open Settings (iOS) or System Preferences (macOS)
  2. Navigate to Calendar > Accounts > Add Account
  3. Select “Other” > “Add CalDAV Account”
  4. Enter details:
    • Server: cal.yourdomain.com
    • Username: your-username
    • Password: your-password
    • Description: Radicale Calendar
  5. Advanced Options:
    • Use SSL: Yes
    • Port: 443
  6. Tap “Next” and wait for verification

Contacts Setup:

  1. Settings > Contacts > Accounts > Add Account
  2. Select “Other” > “Add CardDAV Account”
  3. Enter the same server details
  4. Description: Radicale Contacts

Alternative Method (More Reliable):

Instead of using just the domain, use the full path:

  • CalDAV URL: https://cal.yourdomain.com/username/calendar-name/
  • CardDAV URL: https://cal.yourdomain.com/username/contacts/

Android with DAVx5

Installation:

  1. Install DAVx5 from F-Droid or Google Play Store
  2. Open DAVx5 and tap the ”+” button
  3. Select “Login with URL and user name”

Configuration:

  • Base URL: https://cal.yourdomain.com
  • User name: your-username
  • Password: your-password
  1. Tap “Login”
  2. DAVx5 will discover your calendars and address books
  3. Select which calendars and contacts to sync
  4. Grant necessary Android permissions

Syncing:

DAVx5 creates a new account on your Android device. You can now:

  • Use any calendar app (Google Calendar, Simple Calendar, etc.)
  • Use any contacts app (Google Contacts, Simple Contacts, etc.)
  • Data syncs automatically in the background

Thunderbird (Linux/Windows/macOS)

Calendar Setup:

  1. Install Thunderbird and the Lightning add-on (built-in on newer versions)
  2. Right-click in the calendar list > “New Calendar”
  3. Select “On the Network”
  4. Choose “CalDAV”
  5. Location: https://cal.yourdomain.com/username/calendar-name/
  6. Enter username and password when prompted

Contacts Setup:

  1. Tools > Address Book
  2. File > New > Remote Address Book
  3. Enter CardDAV details:
    • URL: https://cal.yourdomain.com/username/contacts/
    • Name: Radicale Contacts
  4. Enter credentials

GNOME Evolution (Linux)

  1. Edit > Accounts > Add Account
  2. Select “CalDAV” for calendars or “CardDAV” for contacts
  3. Enter server details:
    • Server: cal.yourdomain.com
    • Username: your-username
    • Use secure connection: Yes

Windows Outlook

Outlook doesn’t natively support CalDAV/CardDAV, but you can use plugins:

CalDAV Synchronizer (Free):

  1. Download from GitHub: CalDavSynchronizer
  2. Install and restart Outlook
  3. CalDAV Synchronizer > Synchronization Profiles > Add
  4. Choose “Generic CalDAV/CardDAV”
  5. Configuration:
    • CalDAV URL: https://cal.yourdomain.com/username/calendar-name/
    • Username and password
    • Synchronization interval: 15 minutes

Advanced Configuration

LDAP Authentication

For centralized user management, integrate with LDAP:

Update config/config:

[auth]
type = ldap

# LDAP server configuration
ldap_uri = ldap://ldap.yourdomain.com:389
ldap_base = ou=users,dc=yourdomain,dc=com
ldap_reader_dn = cn=admin,dc=yourdomain,dc=com
ldap_secret = your-ldap-password
ldap_filter = (uid=%(login)s)

Rights Management

Create fine-grained access controls with a rights file.

Update config/config:

[rights]
type = from_file
file = /config/rights

Create config/rights:

# Everyone can read the root collection
[root]
user: .+
collection:
permissions: R

# Authenticated users can read and write their own collections
[owner]
user: .+
collection: {user}/.*
permissions: RW

# Admin has full access
[admin]
user: admin
collection: .*
permissions: RW

# Shared calendar - multiple users can write
[shared]
user: (alice|bob|charlie)
collection: shared/team-calendar
permissions: RW

Storage Backends

Radicale supports multiple storage backends:

Default (multifs): Best for most use cases

[storage]
type = multifs
filesystem_folder = /data/collections

Single Filesystem: All data in one folder

[storage]
type = multifs
filesystem_folder = /data/collections

Custom Hooks: Run scripts on events

[storage]
hook = /scripts/backup-on-change.sh

Logging and Debugging

For troubleshooting, enable debug logging:

[logging]
level = debug

View logs in real-time:

docker-compose logs -f radicale

Backup and Restore

Manual Backup

The beauty of Radicale’s filesystem storage is backup simplicity:

# Create backup script
cat > ~/radicale-backup.sh <<'EOF'
#!/bin/bash
BACKUP_DIR="/mnt/backups/radicale"
DATE=$(date +%Y%m%d_%H%M%S)

# Stop container for consistent backup
docker-compose -f ~/radicale/docker-compose.yml stop radicale

# Backup data directory
tar -czf ${BACKUP_DIR}/radicale-data-${DATE}.tar.gz   -C ~/radicale data/

# Backup configuration
tar -czf ${BACKUP_DIR}/radicale-config-${DATE}.tar.gz   -C ~/radicale config/

# Restart container
docker-compose -f ~/radicale/docker-compose.yml start radicale

# Remove backups older than 30 days
find ${BACKUP_DIR} -name "radicale-*.tar.gz" -mtime +30 -delete

echo "Backup completed: ${DATE}"
EOF

chmod +x ~/radicale-backup.sh

# Schedule daily backups
crontab -e
# Add: 0 2 * * * /home/user/radicale-backup.sh

Automated Backup with Restic

For efficient, encrypted backups to cloud storage:

# Initialize Restic repository
restic init --repo /mnt/backups/radicale-restic

# Backup script with Restic
cat > ~/radicale-backup-restic.sh <<'EOF'
#!/bin/bash
export RESTIC_REPOSITORY="/mnt/backups/radicale-restic"
export RESTIC_PASSWORD="your-secure-password"

# Backup Radicale data
restic backup ~/radicale/data ~/radicale/config   --tag radicale   --exclude-file ~/radicale/.backup-exclude

# Keep last 7 daily, 4 weekly, 12 monthly backups
restic forget   --keep-daily 7   --keep-weekly 4   --keep-monthly 12   --prune

echo "Restic backup completed"
EOF

chmod +x ~/radicale-backup-restic.sh

Restore from Backup

# Stop Radicale
docker-compose stop radicale

# Restore data
tar -xzf /mnt/backups/radicale/radicale-data-YYYYMMDD_HHMMSS.tar.gz   -C ~/radicale/

# Restore config
tar -xzf /mnt/backups/radicale/radicale-config-YYYYMMDD_HHMMSS.tar.gz   -C ~/radicale/

# Start Radicale
docker-compose start radicale

Performance Optimization

Tuning for Large Datasets

If you have many calendars/contacts, optimize performance:

[server]
# Increase max connections
max_connections = 50

# Increase timeout for slow clients
timeout = 60

[storage]
# Enable filesystem caching
filesystem_fsync = false

Monitoring Resource Usage

# Check container resource usage
docker stats radicale

# View detailed logs
docker logs radicale --tail 100 -f

# Check data directory size
du -sh ~/radicale/data

Troubleshooting

Issue: Cannot Connect from Clients

Check server is accessible:

# Test locally
curl http://localhost:5232

# Test from another machine
curl http://YOUR-SERVER-IP:5232

# Check Docker logs
docker-compose logs radicale

Verify authentication:

# Test with curl
curl -u username:password https://cal.yourdomain.com/

# Should return Radicale web interface HTML

Issue: Sync Conflicts

CalDAV handles conflicts automatically, but if you experience issues:

# Check for corrupt files
find ~/radicale/data -name "*.ics" -o -name "*.vcf" |   while read file; do
    if ! iconv -f UTF-8 -t UTF-8 "$file" > /dev/null 2>&1; then
      echo "Corrupt file: $file"
    fi
  done

Issue: Permission Errors

# Fix ownership
sudo chown -R 2999:2999 ~/radicale/data

# Fix permissions
chmod 750 ~/radicale/data
chmod 640 ~/radicale/config/users

Issue: SSL Certificate Errors

# Verify certificate
openssl s_client -connect cal.yourdomain.com:443 -servername cal.yourdomain.com

# Check certificate expiry
openssl s_client -connect cal.yourdomain.com:443 2>/dev/null |   openssl x509 -noout -dates

Issue: High Memory Usage

# Set memory limit in docker-compose.yml
services:
  radicale:
    # ... other config
    deploy:
      resources:
        limits:
          memory: 256M

Security Best Practices

1. Strong Authentication

# Use strong passwords with htpasswd
htpasswd -B config/users username

# Regularly rotate passwords
# Remove old users
htpasswd -D config/users olduser

2. Network Isolation

# In docker-compose.yml
services:
  radicale:
    networks:
      - radicale-internal
    # Don't expose ports directly if using reverse proxy

networks:
  radicale-internal:
    internal: true

3. Regular Updates

# Update Radicale container
docker-compose pull radicale
docker-compose up -d radicale

# Check for security advisories
curl -s https://api.github.com/repos/Kozea/Radicale/releases/latest

4. Firewall Configuration

# Only allow connections from reverse proxy
sudo ufw allow from 172.18.0.0/16 to any port 5232

# If exposing directly, limit connections
sudo ufw limit 5232/tcp

5. Fail2Ban Integration

Create /etc/fail2ban/filter.d/radicale.conf:

[Definition]
failregex = ^.* login failed for user <HOST>.*$
ignoreregex =

Create /etc/fail2ban/jail.d/radicale.conf:

[radicale]
enabled = true
port = http,https
filter = radicale
logpath = /home/user/radicale/data/radicale.log
maxretry = 5
bantime = 3600

Migrating from Other Services

From Google Calendar/Contacts

  1. Export data from Google:

    • Calendar: Google Calendar > Settings > Import & Export > Export
    • Contacts: Google Contacts > Export > vCard format
  2. Import to Radicale:

    • Access web interface: https://cal.yourdomain.com
    • Create new calendar/address book
    • Upload .ics or .vcf files

From iCloud

  1. Export calendars from iCloud web interface
  2. Import files through Radicale web interface
  3. Configure devices to sync with Radicale instead of iCloud

Maintenance

Regular Maintenance Tasks

# Weekly: Check logs for errors
docker-compose logs radicale | grep -i error

# Monthly: Verify backups
ls -lh /mnt/backups/radicale/

# Monthly: Clean old logs if enabled
find ~/radicale/data -name "*.log" -mtime +30 -delete

# Quarterly: Update container
docker-compose pull radicale && docker-compose up -d radicale

Health Monitoring

Create a monitoring script:

#!/bin/bash
# Check Radicale health

# Test HTTP response
if curl -f -s -u username:password https://cal.yourdomain.com/ > /dev/null; then
    echo "Radicale is healthy"
else
    echo "Radicale is down!"
    # Send alert (email, ntfy, etc.)
fi

# Check disk space
DISK_USAGE=$(df -h ~/radicale/data | awk 'NR==2 {print $5}' | sed 's/%//')
if [ $DISK_USAGE -gt 80 ]; then
    echo "Warning: Disk usage above 80%"
fi

Conclusion

Congratulations! You now have a fully functional Radicale server providing:

  • Self-hosted calendar and contacts
  • Cross-platform device synchronization
  • Complete privacy and data ownership
  • Minimal resource consumption
  • Simple backup and maintenance

Next Steps

  1. Configure all your devices to use Radicale
  2. Set up automated backups
  3. Create shared calendars for family or team
  4. Monitor logs and performance
  5. Explore advanced authentication options

Resources

Your personal calendar and contacts server is now ready. Enjoy the freedom of self-hosted scheduling and contacts management!