Tự Host Server CalDAV/CardDAV với Radicale trên Docker: Đồng Bộ Lịch và Danh Bạ Không Cần Google hay iCloud

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

Cú Sốc Thay Đổi Thói Quen Quản Lý Dữ Liệu Của Tôi

Câu chuyện bắt đầu từ một thông báo tài khoản Google bị tạm khóa — không phải của tôi, mà là của một đồng nghiệp. Anh ấy thức dậy và phát hiện nhiều năm danh bạ và sự kiện lịch bị khóa sau cờ cảnh báo “hoạt động đáng ngờ”. Google khôi phục sau ba ngày, nhưng ba ngày đó đủ để tôi phải suy nghĩ lại về nơi lưu trữ dữ liệu của mình.

Lịch của tôi chứa lịch bảo trì server, deadline của nhóm, các cuộc hẹn cá nhân. Danh bạ có những số điện thoại tôi chưa sao lưu ở bất kỳ đâu khác. Tất cả đang nằm trên server của Google, chỉ cần một quyết định từ thuật toán là có thể trở nên không thể truy cập lúc 2 giờ sáng khi tôi thực sự cần.

Cuối tuần đó, tôi cài Radicale — một server CalDAV/CardDAV nhẹ — trên homelab của mình. Và nó đã chạy ổn định trong môi trường thực tế từ đó đến nay. Đồng bộ giữa iPhone, điện thoại Android công việc và máy tính Linux chưa bỏ lỡ một nhịp nào trong hơn một năm. Toàn bộ stack chỉ dùng dưới 50MB RAM. Đây là cách để bạn tái tạo lại setup này.

Hiểu Về Giao Thức Trước Khi Đụng Vào Cấu Hình

CalDAV là giao thức điện thoại của bạn dùng để đồng bộ lịch. Khi bạn thêm sự kiện trên iPhone, nó gửi yêu cầu CalDAV lên iCloud — hoặc bất kỳ server nào bạn cấu hình. CardDAV làm điều tương tự cho danh bạ. Cả hai đều là chuẩn mở chạy qua HTTPS, được hỗ trợ sẵn bởi iOS, Android, macOS, Windows và hầu hết mọi ứng dụng lịch hoặc danh bạ đáng dùng.

Radicale là server CalDAV/CardDAV viết bằng Python, được thiết kế tối giản có chủ ý. Không có web UI, không dashboard, không cồng kềnh — chỉ là server giao thức đang làm đúng nhiệm vụ của nó. Ít thành phần hơn đồng nghĩa với bề mặt tấn công nhỏ hơn và ít thứ bị hỏng vào một ngày thứ Ba ngẫu nhiên nào đó.

Stack chúng ta sẽ xây dựng:

  • Radicale trong Docker với persistent data volume
  • Xác thực mật khẩu được hash bằng bcrypt
  • Nginx reverse proxy cho HTTPS (client từ chối kết nối HTTP thuần)

Xây Dựng Stack Từng Bước

Cấu Trúc Thư Mục

Tạo workspace trước khi chạy bất kỳ lệnh Docker nào:

mkdir -p ~/radicale/{data,config}
cd ~/radicale

File Cấu Hình Radicale

Tạo file config/config (không có phần mở rộng — đó là quy ước của Radicale):

[server]
hosts = 0.0.0.0:5232

[auth]
type = htpasswd
htpasswd_filename = /data/users
htpasswd_encryption = bcrypt

[storage]
filesystem_folder = /data/collections

[logging]
level = info

Dùng xác thực htpasswd có nghĩa là Radicale đọc thông tin đăng nhập từ một file do bạn kiểm soát. Không có OAuth token, không phụ thuộc dịch vụ ngoài, không có điểm lỗi duy nhất nào nằm ngoài server của bạn.

Tạo Người Dùng

Tạo mật khẩu được hash bằng bcrypt mà không cần cài thêm package hệ thống:

docker run --rm -it python:3.11-slim bash -c \
  "pip install bcrypt -q && python3 -c \"import bcrypt; print(bcrypt.hashpw(b'yourpassword', bcrypt.gensalt()).decode())\""

Sao chép hash đầu ra, sau đó tạo file users:

echo "yourusername:\$2b\$12\$yourhashedpasswordhere" > ~/radicale/data/users

Để thêm nhiều người dùng, thêm mỗi dòng username:hash cho mỗi người. Mật khẩu dạng văn bản thường là thứ bạn nhập trên điện thoại hoặc ứng dụng máy tính — không phải hash.

File Docker Compose

Tạo file docker-compose.yml trong thư mục ~/radicale/:

services:
  radicale:
    image: tomsquest/docker-radicale:latest
    container_name: radicale
    restart: unless-stopped
    ports:
      - "5232:5232"
    volumes:
      - ./data:/data
      - ./config/config:/config/config:ro
    environment:
      - RADICALE_CONFIG=/config/config
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:5232/.well-known/caldav"]
      interval: 30s
      timeout: 10s
      retries: 3

read_only: trueno-new-privileges không phải là bảo mật hình thức. Với một dịch vụ được expose ra mạng, bạn muốn container không thể tự sửa đổi filesystem của nó nếu có gì đó xảy ra lúc 2 giờ sáng khi bạn không theo dõi.

docker compose up -d
docker compose logs -f radicale

Bạn sẽ thấy Radicale khởi động và xác nhận đang lắng nghe trên port 5232.

Nginx Reverse Proxy với HTTPS

Client — đặc biệt là iOS — từ chối đồng bộ qua HTTP thuần. TLS là bắt buộc. Nếu bạn đã chạy Nginx Proxy Manager hoặc Caddy trong homelab, chỉ cần trỏ nó vào port 5232. Nếu không, đây là cấu hình Nginx tối giản:

limit_req_zone $binary_remote_addr zone=radicale:10m rate=5r/m;

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

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

    location / {
        limit_req zone=radicale burst=20 nodelay;
        proxy_pass http://127.0.0.1:5232;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_set_header X-Script-Name "";
    }
}

Giới hạn tốc độ (rate limiting) quan trọng ở đây. Radicale không có bảo vệ brute-force tích hợp, nên config Nginx này xử lý ở lớp proxy trước khi các yêu cầu xấu chạm đến server của bạn.

Lấy chứng chỉ TLS bằng Certbot:

certbot --nginx -d cal.yourdomain.com

Nếu homelab của bạn chỉ dùng nội bộ và không có domain công khai, chứng chỉ tự ký (self-signed) vẫn dùng được. Bạn chỉ cần thêm nó làm chứng chỉ tin cậy trên mỗi thiết bị.

Kết Nối Các Thiết Bị

iOS (iPhone và iPad)

Vào Cài đặt → Lịch → Tài khoản → Thêm tài khoản → Khác → Thêm tài khoản CalDAV:

  • Server: https://cal.yourdomain.com
  • Tên đăng nhập: tên người dùng của bạn
  • Mật khẩu: mật khẩu dạng văn bản thường (không phải hash bcrypt)

Để thêm danh bạ: Cài đặt → Danh bạ → Tài khoản → Thêm tài khoản → Khác → Thêm tài khoản CardDAV với cùng URL server và thông tin đăng nhập. iOS tự động phát hiện cả endpoint CalDAV và CardDAV từ URL gốc — Radicale xử lý điều này chính xác mà không cần cấu hình thêm gì.

Android

Android không có hỗ trợ CalDAV/CardDAV sẵn (thật vậy đó). Cài DAVx⁵ từ F-Droid hoặc Play Store:

  1. Thêm tài khoản → URL và tên người dùng
  2. Nhập https://cal.yourdomain.com cùng thông tin đăng nhập
  3. DAVx⁵ tự động tìm lịch và sổ địa chỉ của bạn
  4. Chọn collection cần đồng bộ và đặt khoảng thời gian

DAVx⁵ tích hợp với ứng dụng lịch và danh bạ hệ thống, nên mọi thứ trông và hoạt động như một tài khoản gốc sau khi cấu hình xong.

macOS và Máy Tính Linux

Trên macOS, vào Cài đặt Hệ thống → Tài khoản Internet → Thêm tài khoản → CalDAV. Để thêm danh bạ, mở ứng dụng Danh bạ và thêm tài khoản CardDAV qua phần tùy chọn của ứng dụng. Cả hai đều dùng cùng URL server.

Trên Linux, Thunderbird với extension CardBook xử lý CardDAV đáng tin cậy. GNOME Calendar và GNOME Contacts đều hỗ trợ tài khoản trực tuyến qua GNOME Settings nếu bạn thích dùng ứng dụng gốc.

Xác Minh Đồng Bộ

Tạo một sự kiện thử nghiệm trên điện thoại, sau đó kiểm tra thư mục data của Radicale trên server:

ls ~/radicale/data/collections/collection-root/yourusername/

Bạn sẽ thấy các thư mục cho mỗi lịch và sổ địa chỉ, với các file .ics cho sự kiện và .vcf cho danh bạ. Nếu sự kiện xuất hiện ở đây, đồng bộ đang hoạt động hoàn toàn từ đầu đến cuối.

Duy Trì Hoạt Động Lâu Dài

Sử dụng thực tế đã dạy tôi một vài điều mà tài liệu không đề cập:

Backup là các file, nên rất dễ dàng. Radicale lưu mọi thứ dưới dạng file .ics.vcf thuần trên đĩa. Một cron job đơn giản là đủ:

# Thêm vào crontab bằng lệnh: crontab -e
0 3 * * * tar -czf /backup/radicale-$(date +%Y%m%d).tar.gz ~/radicale/data/ && find /backup/ -name 'radicale-*.tar.gz' -mtime +30 -delete

Lệnh này sao lưu hàng đêm và tự động xóa các bản backup cũ hơn 30 ngày.

Thời gian uptime không phải vấn đề. Chính sách restart: unless-stopped trong file Compose xử lý crash process tự động. Docker khởi động container khi hệ thống boot. Trong hơn một năm sử dụng thực tế, container chỉ khởi động lại đúng một lần — sau khi update kernel host — và trở lại online mà không cần can thiệp trong vài giây.

Thêm người dùng mới sau này chỉ là thêm một dòng vào file users. Không cần khởi động lại container — Radicale đọc file này mỗi lần xác thực.

# Tạo hash cho người dùng mới
docker run --rm python:3.11-slim bash -c \
  "pip install bcrypt -q && python3 -c \"import bcrypt; print(bcrypt.hashpw(b'newpassword', bcrypt.gensalt()).decode())\""

# Thêm vào file users
echo "newuser:\$2b\$12\$newhash" >> ~/radicale/data/users

Ba Mươi Phút Cài Đặt, Nhiều Năm Làm Chủ Dữ Liệu

Từ không có gì đến đồng bộ hoàn chỉnh mất khoảng 30 phút, với điều kiện Docker đã chạy và bạn có domain trỏ về server. Sau đó, dữ liệu là của bạn — không cần theo dõi thay đổi điều khoản dịch vụ, không lo tài khoản bị tạm khóa, không phí thuê bao.

Điều tôi không ngờ tới: đồng bộ bình thường cảm giác y hệt Google Calendar khi đã chạy. Sự kiện thêm trên điện thoại xuất hiện trên máy tính trong vài giây. Danh bạ đồng bộ hai chiều mà không cần làm gì thêm. Trên iOS và Android, nó trông như một tài khoản tích hợp sẵn — vì DAVx⁵ và iOS xử lý nó đúng như vậy.

Lợi ích thực sự hiện ra khi có chuyện xảy ra với người khác — tài khoản bị tạm khóa, chính sách dữ liệu thay đổi. Bạn chỉ nhún vai. Lịch của bạn đang trên server do chính bạn kiểm soát.

Share: