Skip to content

korrosync

Self-hosted KOReader Progress Sync server, running as a Docker stack on the arrstack VM. Syncs reading position between KOReader on the Kobo Clara BW and Crosspoint Reader on the XTEINK X4 — same wire protocol as the official koreader/kosync server, written in Rust on top of an embedded redb K/V store, no Redis sidecar.

LAN URL: http://192.168.1.252:3030. Public URL: https://kosync.rampancy.cloud via Caddy on the edge LXC, TLS terminated at the edge with the Let's Encrypt wildcard cert.

Service details

Property Value
Host arrstack VM (192.168.1.252) — see arrstack service page
Image szaffarano/korrosync:latest (upstream)
Storage embedded redb K/V file under named volume korrosync_data (/data/db.redb in-container)
Compose stacks/korrosync/docker-compose.yml
Management Dockhand (Git-backed Docker Compose) — controller is local on arrstack
Host port 3030 → container 3000 (host-side remapped to dodge Dockhand's own UI on :3000)
Reverse proxy Caddy on edge LXC — kosync.rampancy.cloud192.168.1.252:3030
Backup Captured by pbs-daily (named volume sits under /var/lib/docker/volumes/korrosync_data/_data, part of the VM's vzdump snapshot)

Devices

Both devices register self-service via KOReader's Progress Sync plugin — no server-side account creation.

Device Reader Plugin Server URL
Kobo Clara BW KOReader (sideloaded via MobileRead OCP) Progress Sync (built-in) http://192.168.1.252:3030
XTEINK X4 Crosspoint Reader firmware KOReader Sync (built-in to Crosspoint) http://192.168.1.252:3030

Open Progress Sync → Custom sync server → enter the URL above, then Register / Login on the first device and Login with the same credentials on the second.

Note

The plugin matches books by binary hash by default — both devices need the byte-identical file. If a book gets re-derived in Calibre on the way to one device, switch the plugin to filename matching.

Warning

Only reading position syncs through this plugin. Bookmarks and highlights live in .sdr sidecar files alongside each book — separate problem, separate tool (Syncthing is the usual answer if it ever matters).

Deployment flow

  1. Compose lives in homelab-ansible/stacks/korrosync/ — not edited on the host.
  2. Dockhand (on arrstack) reads the compose from git on its sync schedule.
  3. Dockhand dispatches docker compose up against the local Docker daemon (no Hawser hop — same host).
  4. korrosync container starts, binds 0.0.0.0:3000 inside the container, host exposes :3030, persists state to the korrosync_data named volume.

The named volume sits under /var/lib/docker/volumes/korrosync_data/_data on arrstack, part of the VM's vzdump rootfs snapshot. PBS backup is automatic.

Reverse-proxy & TLS

Reverse proxy lives on the edge LXC (Caddy, Phase 5D). Caddy terminates TLS with the LE wildcard *.rampancy.cloud issued via Cloudflare DNS-01 and proxies to the upstream over plain HTTP.

client → CF DNS → 180.150.117.133 → UDM forward → Caddy on edge → http://192.168.1.252:3030

KORROSYNC's compose ships KORROSYNC_USE_TLS=false — no server-side TLS, edge handles it. KOReader's plugin honours the custom hostname; on each device, set Server URL to https://kosync.rampancy.cloud and re-login.

Lessons from the 2026-04-29 deploy

Port-collision miss on :3000

First push of the compose (190629d) used the upstream-default 3000:3000. Dockhand's own UI runs on arrstack:3000 (and the Hawser agents on nginx + n8n VMs phone home there via ws://192.168.1.252:3000/api/hawser/connect — see hawser role page). Container failed to bind on first reconcile.

Fix (236b53b) remapped host-side to 3030; container still listens on 3000. Lesson: before publishing a stack onto an existing host, grep homelab-ansible/ for the upstream's default ports — most app READMEs default to 3000/8080/8000 and arrstack already runs a controller on each.

serve subcommand required — README example is stale

After the port fix, the container went into a restart loop. docker logs showed the binary printing its CLI help (Commands: serve / user / db / help) and exiting, then restarting under unless-stopped. Cause: the image's ENTRYPOINT is just /app/korrosync with no default subcommand, so without explicit serve the binary treats the invocation as --help and exits 0.

Upstream README's docker run examples don't pass a subcommand and imply the image runs as a server out of the box. Either the README is stale or the CLI was restructured into subcommands recently. Fix (dbee9b8) adds command: serve to the compose service.

Lesson: FROM scratch images leave you with only the binary's stdout to debug from — read the actual ENTRYPOINT in the image's Dockerfile (or docker inspect its Config.Cmd/Config.Entrypoint) rather than trusting the README's example, especially if the binary has subcommands. This is a close cousin of the existing memory rule about verifying a tool's own issue tracker before iterating on theory; here it's "verify the actual image, not the marketing README."