Background#
Due to large number of schedule, it is necessary for me to have a calendar which can help me to schedule my day. In the last year, I used Outlook Calendar to schedule my time. It is both useful and convenient, but I still choose to self-host a scheduler for privacy reasons.
My Solution#
My solution contains two parts, the server and the client. Both of them should be open-source and non-commercialized applications, which can protect my privacy as much as possible.
For the server side, I choose Radicale to be the CalDAV backend, and use Traefik for reverse proxy. To hide the IP of my server, I have Radicale running behind Gcore’s CDN.
For the client side, I have two platforms, the Linux Desktop and the Android mobile phone. For the previous one, I use Thunderbird Calendar and for the last one, I use DAVx5 + Etar Calendar.
So my solution can be displayed as a graph below:
graph LR
subgraph Server
Radicale-->Traefik
end
subgraph Clients
subgraph Linux
Thunderbird
end
subgraph Android
DAVx5-->client[Etar Calendar]
end
end
Gcore[Gcore CDN]
Traefik-->Gcore
Gcore-->Thunderbird
Gcore-->DAVx5
Now, I will introduce how to deploy Radicale and Traefik and how to configure the clients.
Deploy Traefik#
I use docker compose to deploy Traefik. Here is my compose file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
version: '3'
services:
reverse-proxy:
image: traefik:v3.2
container_name: traefik
command:
- "--api.insecure=false"
- "--log.level=DEBUG"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.websecure.address=:443"
- "--entryPoints.websecure.http.tls.certResolver=myresolver"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=your-email@example.com"
- "--certificatesresolvers.myresolver.acme.storage=/acme.json"
# - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
networks:
- traefik
ports:
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./acme.json:/acme.json
- ./auth:/auth
networks:
traefik:
external: true
|
Replace the your-email@example.com
with your email.
The ./auth
folder in line 30 stores the user authentication information of Radicale, and I will explain it further in the next section.
Before starting, it needs a network, running the following command to create a network.
1
|
docker network create traefik
|
To store the SSL certificate, it also requires a file named acme.json
. Just running these commands.
1
2
|
touch acme.json
chmod 600 acme.json
|
Attention: The chmod 600 acme.json
is very important!
Now, the directory looks like:
1
2
3
4
5
|
traefik/
├── acme.json
├── auth
│ └── radicale
└── docker-compose.yml
|
The radicale stores the credential of Radicale authentication.
Now, we can start with the following command.
Deploy Radicale#
I prefer to deploy application with docker. So I use docker-radicale to deploy Radicale.
Here is my compose file and I will explain it later.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
# Can be enhanced with an additional compose file
# See also https://docs.docker.com/compose/production/#modify-your-compose-file-for-production
services:
radicale:
image: tomsquest/docker-radicale
container_name: radicale
ports:
- 127.0.0.1:5232:5232
init: true
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- SETUID
- SETGID
- CHOWN
- KILL
deploy:
resources:
limits:
memory: 256M
pids: 50
healthcheck:
test: curl -f http://127.0.0.1:5232 || exit 1
interval: 30s
retries: 3
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.radicale.rule=Host(`radicale.example.com`)"
- "traefik.http.routers.radicale.entrypoints=websecure"
- "traefik.http.routers.radicale.tls.certresolver=myresolver"
- "traefik.docker.network=traefik"
- "traefik.http.routers.radicale.middlewares=radicale-auth"
- "traefik.http.middlewares.radicale-auth.basicauth.usersfile=/auth/radicale"
- "traefik.http.middlewares.radicale-auth.basicauth.headerField=X-Remote-User"
networks:
- traefik
volumes:
- ./data:/data
- ./config:/config:ro
networks:
traefik:
external: true
|
Replace the radicale.example.com
with your own domain, and set an A DNS record which points to the IP of your server. Once the ACME challenge is done, there will be some content in the acme.json
.
For security reasons, it is better to turn on the authentication option. So I add a middle ware between Radicale and Traefik for authentication. Since Radicale uses a configuration file to setting, we need to create a file named config
and put it under the config/
folder. So the structure of the directory is:
1
2
3
4
|
radicale/
├── config
│ └── config
└── docker-compose.yml
|
There are three types of authentication, htpasswd
, remote_user
and http_x_remote_user
. Because I use Traefik, so I have to use the last one, http_x_remote_user
. The configuration file is below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
# -*- mode: conf -*-
# vim:ft=cfg
# Config file for Radicale - A simple calendar server
#
# Place it into /etc/radicale/config (global)
# or ~/.config/radicale/config (user)
#
# The current values are the default ones
[server]
# CalDAV server hostnames separated by a comma
# IPv4 syntax: address:port
# IPv6 syntax: [address]:port
# Hostname syntax (using "getaddrinfo" to resolve to IPv4/IPv6 adress(es)): hostname:port
# For example: 0.0.0.0:9999, [::]:9999, localhost:9999
#hosts = localhost:5232
hosts = 0.0.0.0:5232
# Max parallel connections
#max_connections = 8
# Max size of request body (bytes)
#max_content_length = 100000000
# Socket timeout (seconds)
#timeout = 30
# SSL flag, enable HTTPS protocol
#ssl = False
# SSL certificate path
#certificate = /etc/ssl/radicale.cert.pem
# SSL private key
#key = /etc/ssl/radicale.key.pem
# CA certificate for validating clients. This can be used to secure
# TCP traffic between Radicale and a reverse proxy
#certificate_authority =
[encoding]
# Encoding for responding requests
#request = utf-8
# Encoding for storing local collections
#stock = utf-8
[auth]
# Authentication method
# Value: none | htpasswd | remote_user | http_x_remote_user
type = http_x_remote_user
# Htpasswd filename
# htpasswd_filename =
# Htpasswd encryption method
# Value: plain | bcrypt | md5 | sha256 | sha512 | autodetect
# bcrypt requires the installation of 'bcrypt' module.
# htpasswd_encryption =
# Incorrect authentication delay (seconds)
#delay = 1
# Message displayed in the client when a password is needed
#realm = Radicale - Password Required
# Сonvert username to lowercase, must be true for case-insensitive auth providers
#lc_username = False
[rights]
# Rights backend
# Value: none | authenticated | owner_only | owner_write | from_file
# type = owner_only
# File for rights management from_file
#file = /etc/radicale/rights
# Permit delete of a collection (global)
#permit_delete_collection = True
[storage]
# Storage backend
# Value: multifilesystem | multifilesystem_nolock
#type = multifilesystem
# Folder for storing local collections, created if not present
#filesystem_folder = /var/lib/radicale/collections
filesystem_folder = /data/collections
# Delete sync token that are older (seconds)
#max_sync_token_age = 2592000
# Command that is run after changes to storage
# Example: ([ -d .git ] || git init) && git add -A && (git diff --cached --quiet || git commit -m "Changes by \"%(user)s\"")
#hook =
[web]
# Web interface backend
# Value: none | internal
type = internal
[logging]
level = debug
# Threshold for the logger
# Value: debug | info | warning | error | critical
#level = info
# Don't include passwords in logs
#mask_passwords = True
[headers]
# Additional HTTP headers
#Access-Control-Allow-Origin = *
[hook]
# Hook types
# Value: none | rabbitmq
#type = none
#rabbitmq_endpoint =
#rabbitmq_topic =
#rabbitmq_queue_type = classic
|
Then it comes to the last step, generating the credential. I use SHA-512
encryption method. The first step is to install htpasswd
, running this command:
1
|
apt install apache2-utils
|
Then, go to the traefik/auth
directory, and run this command:
1
|
htpasswd -s -c ./radicale username password
|
Replace the username
and the password
with your own username and password. After that, the credential can be generated. If you want to add more users, please remove the -c
flag, which means creating a new file that will overwrite the original file.
Now, just run docker compose up -d
to start the docker container. Try to access the website. If everything goes well, after inputting the username and the password, it will redirect to the https://radicale.example.com/.web/
and display a user interface. After logging in, you can create calendars.
Desktop#
It is easy to configure Thunderbird Calendar. Firstly, open Thunderbird and go to the calendar tab. Then, click on New Calendar
and select On the Network
. Finally, input the username and the domain and click Find Calendars
.
Android#
The process is similar to configuring Thunderbird Calendar. Just click the +
sign, and input the URL and username.
However, DAVx5 is only a tool for synchronization, which means it does not provide the user interface, and we can treat it as a backend.
For the frontend, I choose Etar Calendar. It is also easy to configure Etar Calendar, just launch the application and go to the setting page. Then, click Add CalDAV Calendar
button, and it will jump to DAVx5, where you can select calendars in Radicale. Finally, it can show the events in the calendars.
References
ChatGPT
Radicale Document
Fireyeah
Traefik Document
docker-radicale
Radicale Discussions