Xandikos 0.4.10 部署指南

Xandikos 0.4.10 部署实测:pipx 主形态(PEP 668 绕行)与官方 Docker 镜像辅形态(env 驱动 entrypoint、uid=1000 权限坑、官网示例纠偏)、systemd socket activation 单元、curl PROPFIND/MKCALENDAR/PUT/REPORT 建日历建事件全闭环、Lightning/Khal/Vdirsyncer/DAVx5 客户端对接要点与验收清单。

XandikosXandikosCalDAVCardDAVDocker自托管2026/11/02

Xandikos 0.4.10 部署指南

目标:在真实 Linux 宿主部署 Xandikos 0.4.10,走通"建日历→写事件→查询→改删"的 CalDAV/CardDAV 端到端闭环。全部命令与回显取自 2026-11-02 实测(完整取证:/opt/data/quzuo/tmp/xandikos/evidence.md)。Xandikos 是纯 Python 轻量服务,本指南以 pipx 形态为主(零外部服务依赖、最可复现),官方 Docker 镜像为辅形态并列实测。

一、形态选择

形态命令量依赖适用
pipx(主)2 条命令仅 Python≥3.10单机常驻、systemd 托管、要 xandikos-milter 入口
官方镜像 ghcr.io/jelmer/xandikosdocker runDocker容器化编排、与 nginx 同机

两者数据目录布局完全一致(每集合一个 Git 仓库),可互相迁移(直接拷目录)。

二、pipx 部署实测(主形态)

2.1 安装

Debian 12/Ubuntu 24.04 类系统受 PEP 668 限制,pip3 install --user xandikos 会报 externally-managed-environment 直接拒绝——必须走 pipx 或 venv:

$ pipx install xandikos
done! ✨ 🌟 ✨
  installed package xandikos 0.4.10, installed using Python 3.12.3
  These apps are now globally available
    - xandikos
    - xandikos-milter
$ xandikos --version
xandikos 0.4.10

启动横幅逐字(含完整依赖栈版本,排障时先核对这行):

xandikos 0.4.10, dulwich 1.2.20, vobject 0.9.9, icalendar 7.3.0, aiohttp 3.14.5, defusedxml 0.7.1

2.2 首次启动

$ mkdir -p /srv/xandikos/data
$ xandikos serve -d /srv/xandikos/data --defaults -l 0.0.0.0 -p 8165
Creating /srv/xandikos/data/user
Create calendar in /srv/xandikos/data/user/calendars/calendar.
Create addressbook in /srv/xandikos/data/user/contacts/addressbook.
Create inbox in /srv/xandikos/data/user/inbox.
Create outbox in /srv/xandikos/data/user/outbox.
Listening on 0.0.0.0:8165
  • --defaults 一次性创建 principal /user/ 的默认日历、地址簿与 schedule inbox/outbox(隐含 --autocreate);
  • qrcode package not installed; QR code generation disabled 为无害警告(根页二维码可选依赖);
  • 数据布局:user/calendars/calendar/.git、user/contacts/addressbook/.git、user/inbox/.git、user/outbox/.git——每个集合独立 Git 仓库,.xandikos 文件存集合元数据。

2.3 systemd 托管(官方 deployment 页 + 0.4.x CLI 校正)

官网示例仍是 0.0.x 全局参数写法,0.4.x 已子命令化,ExecStart 需改为 xandikos serve。socket activation 双单元(serve 自动检测 LISTEN_FDS,无需传 -l/-p):

# /etc/systemd/system/xandikos.socket
[Unit]
Description=Xandikos socket
[Socket]
ListenStream=/run/xandikos.sock
[Install]
WantedBy=sockets.target

# /etc/systemd/system/xandikos.service
[Unit]
Description=Xandikos CalDAV/CardDAV server
After=network.target
[Service]
ExecStart=/usr/local/bin/xandikos serve \
  -d /var/lib/xandikos \
  --route-prefix=/dav \
  --current-user-principal=/user/ \
  -l /run/xandikos.sock
User=xandikos
Group=www-data
Restart=on-failure
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all

systemctl enable --now xandikos.socket xandikos.service 生效;不想用 socket activation 就删 .socket 单元、service 里直接 -p 8080。

三、curl 协议闭环实测(CalDAV 天然 curl 友好,浏览器非必需)

以下全部为 8165 实例逐字回显。

3.1 能力探测

$ curl -s -X OPTIONS -D- -o /dev/null http://127.0.0.1:8165/
HTTP/1.1 200 OK
DAV: 1, 2, 3, calendar-access, calendar-auto-schedule, addressbook, extended-mkcol, add-member, sync-collection, quota
Allow: COPY, DELETE, GET, HEAD, MKCALENDAR, MKCOL, MOVE, OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT
Server: Python/3.12 aiohttp/3.14.5

/.well-known/caldav 返回 302 → /,支持 well-known 发现的客户端填根 URL 即可。

3.2 principal 与日历家目录发现

$ curl -s -X PROPFIND -H "Depth: 0" -H "Content-Type: application/xml" \
  --data '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:current-user-principal/></d:prop></d:propfind>' \
  http://127.0.0.1:8165/
<ns0:current-user-principal><ns0:href>/user/</ns0:href></ns0:current-user-principal>

$ curl -s -X PROPFIND -H "Depth: 1" ... http://127.0.0.1:8165/user/
<ns0:resourcetype><ns0:principal/></ns0:resourcetype>
<ns1:calendar-home-set><ns0:href>/user/calendars/</ns0:href></ns1:calendar-home-set>

3.3 MKCALENDAR 建日历

$ curl -s -o /dev/null -w "%{http_code}\n" -X MKCALENDAR \
  -H "Content-Type: application/xml" \
  --data '<?xml version="1.0"?><c:mkcalendar xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"><d:set><d:prop><d:displayname>Work</d:displayname><c:calendar-description>Git-backed test calendar</c:calendar-description></d:prop></d:set></c:mkcalendar>' \
  http://127.0.0.1:8165/user/calendars/work/
201

注意 URL 尾斜杠必须保留。displayname/description 会各自生成一条 Git commit(见 3.6)。

3.4 PUT 事件

$ cat > event.ics <<'ICS'
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//xandikos-test//EN
BEGIN:VEVENT
UID:evt-20261101-001@xandikos-test
DTSTAMP:20261101T215000Z
DTSTART:20261105T090000Z
DTEND:20261105T100000Z
SUMMARY:Xandikos 取证事件
END:VEVENT
END:VCALENDAR
ICS
$ curl -s -D- -o /dev/null -X PUT -H "Content-Type: text/calendar; charset=utf-8" \
  --data-binary @event.ics \
  http://127.0.0.1:8165/user/calendars/work/evt-20261101-001.ics | grep -iE "^(HTTP|ETag)"
HTTP/1.1 201 Created
ETag: "85f5cc45404b76bbc358a383fdfa46c93adb9d1f"
  • ETag 即 Git blob SHA-1;另返回 Schedule-Tag(auto-schedule 启用);
  • 同内容重复 PUT → 204 No Content(幂等,不产生新 commit);
  • 错误 If-Match → 412(乐观锁实测)。

3.5 REPORT 查询(关键坑:必须带 Depth: 1)

$ curl -s -X REPORT -H "Depth: 1" -H "Content-Type: application/xml" \
  --data '<?xml version="1.0"?><c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"><d:prop><d:getetag/><c:calendar-data/></d:prop><c:filter><c:comp-filter name="VCALENDAR"/></c:filter></c:calendar-query>' \
  http://127.0.0.1:8165/user/calendars/work/
<ns0:response><ns0:href>/user/calendars/work/evt-20261101-001.ics</ns0:href>
  ...<ns0:getetag>"85f5cc45..."</ns0:getetag><ns1:calendar-data>BEGIN:VCALENDAR...

不带 Depth: 1 时返回空 multistatus(实测 207 但零条目),手写 curl 最常踩。增量同步用 sync-collection REPORT(空 sync-token 起步),实测返回全部条目与新 token。

3.6 验证事件溯源落盘

$ cd /srv/xandikos/data/user/calendars/work && git log --oneline
c2e20c1 Delete Second event
e73b51c Added Second event
b84ec57 Added Xandikos 取证事件
9c62b48 Set description.
3927847 Set display name.
c54b657 Set collection type.

每次 DAV 写操作一个 commit,消息含事件 SUMMARY 与 Requester: <User-Agent>。

3.7 CardDAV 写联系人

$ curl -s -o /dev/null -w "%{http_code}\n" -X PUT -H "Content-Type: text/vcard" \
  --data-binary @contact.vcf http://127.0.0.1:8165/user/contacts/addressbook/contact-001.vcf
201

vCard 4.0(FN:张三 Xandikos)写入成功,addressbook-query REPORT 可命中。

四、Docker 辅形态实测(含官网示例纠偏)

4.1 官网示例已失效

官方 docker 页示例 docker run -p 8000:8000 -v $HOME/dav:/data ghcr.io/jelmer/xandikos:latest --defaults 在 latest 镜像直接报错退出:entrypoint 把裸参数原样传给 python3 -m xandikos,0.4.x 的 serve 强制要求 -d。

4.2 正确姿势(env 驱动 entrypoint)

$ docker run -d --name xandikos -p 8172:8000 \
    -e DEFAULTS=true -e DATA_DIR=/data \
    -v /srv/xandikos-dav:/data ghcr.io/jelmer/xandikos:latest
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8172/
200

实测要点:

  • 镜像内同版本 0.4.10,EXPOSE 8000 8001(8001 为 metrics);
  • 权限坑:容器以 uid=1000(xandikos) 运行,宿主 bind 目录若为 root:755 会 PermissionError: [Errno 13] Permission denied: '/data/user' 崩溃退出;chown 1000:1000 或 chmod 777 后正常(实测);
  • entrypoint 支持的环境变量全集:PORT / METRICS_PORT / LISTEN_ADDRESS / DATA_DIR / CURRENT_USER_PRINCIPAL / ROUTE_PREFIX / AUTOCREATE / DEFAULTS / DUMP_DAV_XML / AVAHI / NO_STRICT / DEBUG / PARANOID / INDEX_THRESHOLD / EAGER / NO_DETECT_SYSTEMD / AUTOCERT / WEBDAV_PUSH / STATE_DIR / HTPASSWD / IMIP_LISTEN / MILTER_LISTEN(布尔取 true/1);
  • WEBDAV_PUSH 默认开启(仅显式 false/0 关闭),首次启动在 /data/state/vapid/ 生成 VAPID 密钥对——pipx 形态则因缺 py_vapid 需显式安装;
  • 数据布局与 pipx 形态逐目录一致,两种形态可互迁。

五、客户端对接要点

客户端配置口径(实测/官网 clients 页)
Thunderbird Lightning无 well-known 自动发现,直填日历 URL:http://<host>:8165/user/calendars/work/(实测该 URL 即标准 CalDAV 端点);联系人用 CardBook 填 /user/contacts/addressbook/
Khal / ikhal(Khard 同)经 Vdirsyncer 中转:type=caldav,url 填日历集合 URL,username/password 按反代认证
Vdirsyncer官方测试清单第一;pair 配置 calendars + contacts 两段即可全同步
DAVx⁵(Android)服务器 URL 填根 http://<host>:8165/,走 well-known 发现(实测 302 正常)
Apple iOS/macOS账户设置里"其他 CalDAV 账户"填主机+/user/(实测 principal 发现正常)
Evolutionclients 页在列,直接根 URL

生产建议:前置 nginx/caddy 做 TLS 与 Basic auth(此时不要用 --htpasswd,官方 help 明确"If you run Xandikos behind a reverse proxy, configure authentication there instead"),Xandikos 监听 127.0.0.1 或 unix socket(-l /run/xandikos.sock --socket-mode 660 --socket-group www-data 实测支持)。

六、验收清单

  • xandikos --version = 0.4.10,启动横幅依赖栈版本齐(dulwich/vobject/icalendar/aiohttp/defusedxml)
  • OPTIONS / 的 DAV 能力串含 calendar-access, addressbook, sync-collection
  • /.well-known/caldav → 302
  • PROPFIND 根 → current-user-principal /user/
  • MKCALENDAR → 201;PUT 事件 → 201 且 ETag 为 40 位十六进制(git blob SHA)
  • REPORT(带 Depth:1)→ 207 且命中事件
  • git log 每写操作一条 commit,含 Requester
  • Docker 形态:容器 Status=Up(非 Exited),宿主目录属主 uid 匹配
  • 生产:TLS 终结在反代;Xandikos 仅监听回环/unix socket
Xandikos 0.4.10 部署指南 - Quzuo