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: true và no-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:
- Thêm tài khoản → URL và tên người dùng
- Nhập
https://cal.yourdomain.comcùng thông tin đăng nhập - DAVx⁵ tự động tìm lịch và sổ địa chỉ của bạn
- 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 và .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.

