A single-binary CalDAV + CardDAV server in Go, built as a drop-in replacement for Radicale: it speaks the same URLs, reads and writes the same on-disk data layout, and gets its users from the same header a reverse proxy injects. Designed to sit behind an OpenCloud deployment (its own full-stack reference ships Radicale exactly like this), but it works behind any proxy that authenticates and injects the identity.
One static binary (~7 MB), no runtime dependencies, no database: the binary and the data volume are the whole deployment (a container image is published as well, for those who prefer one).
- CalDAV: calendar discovery,
calendar-queryandcalendar-multigetREPORTs (with server-side RRULE/RDATE/EXDATE expansion in time-range queries),sync-collection(RFC 6578) for incremental sync, MKCALENDAR, free-busy query. - CardDAV: address book discovery,
addressbook-queryandaddressbook-multigetREPORTs, MKCOL, the same sync endpoint. - Shared DAV plumbing: PROPFIND (Depth 0/1/infinity,
prop/allprop/propname, 404 propstat for unknown properties), PROPPATCH with RFC 4918 atomicity (rename,calendar-color, descriptions), conditional PUT/GET (If-Match,If-None-Match, 304s), DELETE, MKCOL/MKCALENDAR, OPTIONS and/.well-known/caldav|carddavdiscovery. - Multi-user, proxy-agnostic auth: every request carries
X-Remote-User; the server never asks for passwords. Pair it with any authenticating reverse proxy (OpenCloud's proxy, Authelia, basic auth...). - Automatic per-user bootstrap: first access creates the user's "Personal Calendar" and "Personal Address Book", like Radicale's predefined collections.
- Plus an optional built-in web UI (React + FullCalendar) for standalone use, served by the same binary.
What it deliberately does not do (yet): invitations (iMIP), calendar sharing between users, and server-side sorting of recurrences in the web UI.
clients (DAVx5, Thunderbird, iOS, web apps)
| /caldav/* /carddav/*
v
authenticating reverse proxy (auth -> X-Remote-User: <user id>)
| prefix stripped, X-Script-Name set
v
davcloud 127.0.0.1:5232 (one static binary)
|
v
data volume, Radicale "multifilesystem" layout
<volume>/collection-root/<user>/<collection>/.Radicale.props
<volume>/collection-root/<user>/<collection>/<item>.ics|.vcf
- Flat URLs, Radicale-compatible:
/caldav/<user>/<collection>/<item>. Most of the DAV plumbing lives ininternal/dav, written for exactly this layout.go-webdav(the library) is still used where it shines: thecalendar-query/multigetfilter engine, which even expands recurrences. - Byte-for-byte storage: items are saved exactly as the client sent them
and served back unchanged, so a volume written by davcloud is
indistinguishable from one written by Radicale - in both directions.
Collection metadata lives in Radicale's own
.Radicale.propsfiles (same keys, including the comma separated supported-calendar-component-set). - Server-managed names are reserved: a name with a leading dot (a
collection's own
.Radicale.props, the.tmp-*files of atomic writes) or a trailing tilde does not exist as far as clients are concerned.PUTon such a name answers 400 andDELETE/GETanswer 404, so no client can break a collection by writing over, or deleting, its metadata. Radicale enforces the same rule throughpathutils.is_safe_filesystem_path_component. - Identity is a header:
X-Remote-Useris trusted blindly, so the port must never be reachable without the authenticating proxy in front. In-dav-rootmode the proxy strips the/caldav//carddavprefixes and marks the endpoint withX-Script-Name; davcloud still emits prefixed hrefs, exactly like Radicale does. - One codebase, two roles: the same binary serves the DAV API and (in
standalone mode) a small React web UI. Behind OpenCloud the web apps are
the UI, so davcloud runs with
-dav-rootand does not serve it.
To build it you need Go 1.24+ (plus npm for the web UI). No database, no
runtime dependencies. A released binary or the container image is
self-contained.
Download a release (tags move in steps of 0.2: v0.2.0, v0.4.0, ...):
curl -LO https://github.com/gnacho/davcloud/releases/latest/download/davcloud-linux-amd64
curl -LO https://github.com/gnacho/davcloud/releases/latest/download/davcloud-linux-amd64.sha256
sha256sum -c davcloud-linux-amd64.sha256
chmod +x davcloud-linux-amd64
./davcloud-linux-amd64 -versionUse arm64 instead of amd64 on ARM machines.
Container:
mkdir -p davcloud-data
docker run -d --name davcloud \
--user "$(id -u):$(id -g)" \
-p 127.0.0.1:5232:5232 \
-v "$PWD/davcloud-data:/data" \
ghcr.io/gnacho/davcloud:latest \
-addr :5232 -data-dir /data -ui-dir /app/ui -dev-user alice
curl -s -o /dev/null -w '%{http_code}\n' -X PROPFIND -H 'Depth: 0' \
http://127.0.0.1:5232/caldav/
# 207The image runs as uid 1000, hence --user with your own ids so that it can
write into the bind-mounted directory. -dev-user is the development shortcut
that trusts every request as that user; in production the identity comes from
the X-Remote-User header injected by the proxy, so the port must stay
unreachable for anybody else.
Build
git clone https://github.com/gnacho/davcloud && cd davcloud
CGO_ENABLED=0 go build -trimpath \
-ldflags "-s -w -X main.version=$(git describe --tags --always) -X main.commit=$(git rev-parse --short HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o davcloud ./cmd/server
./davcloud -versionTry it standalone (identity simulated, no proxy needed):
./davcloud -addr 127.0.0.1:5232 -data-dir ./data -dev-user alice
# in another terminal: any CalDAV client against http://127.0.0.1:5232/caldav/Production behind OpenCloud (the tested path; full runbook with backup and rollback in deploy/README.md):
-
Back up Radicale's data directory, then swap services on the same port:
# /etc/systemd/system/davcloud.service (excerpt) [Service] ExecStart=/usr/local/bin/davcloud -addr 127.0.0.1:5232 -data-dir /var/lib/radicale/collections -dav-root User=radicale ReadWritePaths=/var/lib/radicale/
-
Make sure the proxy routes point at it and inject the identity (this is what an OpenCloud
proxy.yamllooks like with Radicale, and davcloud uses the exact same shape):additional_policies: - name: default routes: - endpoint: /caldav/ backend: http://127.0.0.1:5232 remote_user_header: X-Remote-User skip_x_access_token: true additional_headers: - X-Script-Name: /caldav - endpoint: /carddav/ backend: http://127.0.0.1:5232 remote_user_header: X-Remote-User skip_x_access_token: true additional_headers: - X-Script-Name: /carddav
-
Verify (as the proxy would call it, with your real user id):
curl -s -X PROPFIND -H 'Depth: 0' \ -H 'X-Script-Name: /caldav' -H 'X-Remote-User: <user-id>' \ http://127.0.0.1:5232/ | grep -o '<D:href>[^<]*</D:href>' # /caldav/<user-id>/
Rollback is a service switch: stop davcloud, start Radicale over the same volume (the layout and metadata are byte-compatible both ways).
Limits (defaults chosen to match Radicale's own configuration):
| Flag | Default | What it bounds |
|---|---|---|
-max-request-size |
100 MiB | any request body; over it the answer is 413 |
-max-item-size |
10 MiB | a single calendar object or vCard, announced to clients as max-resource-size |
-max-expand |
10000 | recurrence instances one object may expand to while a time-range query is matched; over it the object is matched on its plain dates |
Each one can also be set through the environment (DAVCLOUD_MAX_REQUEST_SIZE,
DAVCLOUD_MAX_ITEM_SIZE, DAVCLOUD_MAX_EXPAND). The HTTP server is built with
explicit timeouts (ReadHeaderTimeout, ReadTimeout, WriteTimeout,
IdleTimeout, MaxHeaderBytes) rather than Go's defaults, which are none.
In daily use as the DAV server behind the author's OpenCloud deployment. The
rollback drill passed on 2026-10-18: Radicale 3.5.3 served the same volume
again, read every item davcloud had written (same UIDs and fields, same
collection properties) and left the files untouched, so going back is a service
switch. The soak period is still running, so treat it as software in active
development. The phase list and what is left are in
PLAN.md. Tagged releases are published from v* tags: static
binaries for linux/amd64 and linux/arm64 with their checksums, and a
multi-arch image at ghcr.io/gnacho/davcloud. Covered by 40 Go tests and 10 web
tests, CI on every push (test, lint, security, web, container image), and a
migration dry run against a copy of a real production volume. Known limitations
are tracked as issues: iMIP, calendar sharing, recurrence editing in the web UI.
AGPL-3.0 (see LICENSE). Dependencies: go-webdav MIT, go-ical MIT, go-vcard MIT, FullCalendar MIT, ical.js MPL-2.0.