# koreader-sync-server: keeping one reading position across several e-readers

> koreader-sync-server is a small self hostable Lua service that synchronizes KOReader reading progress across devices, storing only document hashes and positions in Redis. It suits readers with several devices and a willingness to run their own container.

**koreader/koreader-sync-server** — self hostable synchronization service for koreader devices

- Repository: https://github.com/koreader/koreader-sync-server
- Stars: 790 · Forks: 58
- Language: Lua
- License: AGPL-3.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/koreader-koreader-sync-server

## One reading position, several e-readers

KOReader devices accumulate a small but irritating piece of state: how far you got in the book you were reading. Lose that state and a second device starts the chapter over. koreader-sync-server exists to close exactly that gap. It is a self hostable service that KOReader devices register with, and it keeps reading progress synchronized between them. The scope is narrow on purpose. It does not move books, it does not index a library, and it offers no dashboard. The single job is to remember where each reader stopped so that a reader picking up a different device resumes on the same page. That narrowness is what keeps the project small enough to read in an evening and to run on a machine you control.

## Gin on OpenResty with Redis as the only store

The implementation is Lua throughout, running on the Gin JSON-API framework hosted by OpenResty, with Redis as the store behind it. The container image is built from debian:trixie-slim and compiles OpenResty from source at version 1.29.2.3, then applies a patch so the Gin framework can serve HTTPS. A second daemon, redis-server, is started under runit. Lua rocks for luasocket, luasec and redis-lua are installed after the Gin build, because the framework pins older versions that would otherwise overwrite them. The result is a JSON over HTTP service on port 7200 inside the container, using a self signed certificate generated at image build time. There is no separate database to administer, and no migration step beyond the Redis volume you mount.

## A 32 digit document hash instead of your file name

Privacy is handled at the storage layer, not merely at the transport layer. The service states that it does not store file names or file content. Each document is identified by a unique string of 32 digits, described as an MD5 hash, so the same document arriving from different devices collapses into a single record. What is kept is the furthest reading progress for that document: a percentage value, a location such as /body/DocFragment[20]/body/p[22]/img.0, and the name of the device that reported it. The password never reaches the server in plain text, since KOReader hashes it on the client before authorizing, and the stored account entry is that hash. In practice, an operator of the server learns that you read something and how far you got, and learns nothing about what the document contained.

## Spinning up your own instance with Docker

Deployment is the shortest path into this project. The first command starts a throwaway container for a quick test:

```bash
docker run -d -p 7200:7200 --name=kosync koreader/kosync:latest
```

For anything longer lived, mount the log directories and the Redis data directory so that progress survives a container being replaced:

```bash
mkdir -p ./logs/{redis,app} ./data/redis
docker run -d -p 7200:7200 \
    -v `pwd`/logs/app:/app/koreader-sync-server/logs \
    -v `pwd`/logs/redis:/var/log/redis \
    -v `pwd`/data/redis:/var/lib/redis \
    --name=kosync koreader/kosync:latest
```

A compose file ships with the project, and it exposes user registration explicitly through the environment variable ENABLE_USER_REGISTRATION=true, so you can leave registration closed until you want it open:

```bash
docker compose up -d --build
```

You can also build the image yourself from the repository directory:

```bash
docker build --rm=true --tag=koreader/kosync .
```

To confirm that the server is ready to serve traffic, request the healthcheck endpoint:

```bash
curl -k -v -H "Accept: application/vnd.koreader.v1+json" https://localhost:7200/healthcheck
# should return {"state":"OK"}
```

The -k flag is needed because the certificate is self signed.

## Running on port 17200 when a reverse proxy owns TLS

Inside the container the server listens on 7200 and presents its own certificate. If you would rather have a reverse proxy terminate TLS, the project tells you to run the server on port 17200 instead and let the proxy forward traffic to it. A Traefik V3 configuration looks like this:

```bash
  kosync:
    # ...
    labels:
      - traefik.enable=true
      - 'traefik.http.routers.kosync.rule=Host(`kosync.example.com`)'
      - 'traefik.http.services.kosync.loadbalancer.server.port=17200'
```

Only the router rule and the backend port are fixed here. The proxy still has to be able to reach the container on 17200, which means your port mapping has to expose that port as well, so the compose file published with the project maps 7200 rather than 17200 and needs editing for this arrangement.

## Deleting an account leaves no tombstone

Account removal has an unusually careful design, because the endpoint has to be safe to retry over a flaky connection. DELETE /users/me uses the x-auth-user and x-auth-key headers and deletes the account together with all of its reading progress. Success returns HTTP 200 with a deleted marker. Invalid credentials return HTTP 401 with code 2001. An absent account returns HTTP 404 with code 2006 and the message Account not found., after any orphaned user data has been removed. That specific response is the one that confirms deletion after a lost response; a generic 404 or 401 does not, and the advice is to use a different password when re-registering, because a stale deletion request cannot be distinguished from a new one if both credentials are reused. No deletion records are retained, so usernames can be registered again immediately with empty progress.

## Password changes require the current key

Changing a password follows the same credential scheme. PUT /users/password uses the current x-auth-user and x-auth-key headers together with a JSON body carrying the replacement key, and as with registration the replacement must be a nonempty client derived key, where KOReader uses the MD5 hash of the password. A successful update returns HTTP 200 with an updated marker and preserves existing reading progress, so changing the key does not cost you your place in the book. You still have to update the saved password on every connected reader. The endpoint requires the current key, which means it offers no forgotten password recovery at all. Incorrect credentials and stale retries return HTTP 401, and if a response is lost you can confirm the replacement key with GET /users/auth. Invalid replacement values are rejected with HTTP 403 and code 2003.

## Two gaps worth naming before you deploy

Two documentation gaps deserve attention before you make this your sync backend. The README does not document rollback, backup or restore procedures for the Redis volume, so a lost data directory leaves you with no written recovery path for anyone reading progress. It also says nothing about rate limiting or brute force protection on the authentication endpoints, and the only failure behaviour described for bad credentials is a 401. On the protocol side, the API is described in the third party kosync-conformance project, together with a verifier that checks an implementation against it in one command. That description is observational rather than normative: it documents what this server does and notes where implementations in the wild disagree, which is a reasonable warning that client behaviour may vary. The licence is Affero GPL v3. For a network service this has one practical consequence: code you modify and then run as a service for others falls under the copyleft terms, so read the COPYING file before changing it. The last push was on 2026-09-23, and the v2.1.1 release is dated 2026-05-16, with v2.0 dating from 2024-12-06.

## Conclusion

koreader-sync-server fits readers who use more than one KOReader device and already have somewhere to host a container. It does not fit anyone looking for a library manager, a browser interface or a turnkey hosted service, and it offers no recovery path for a lost Redis volume. Before adopting it, check the healthcheck endpoint and the user registration switch on your first deployment, and read the COPYING file before modifying the Lua code, because the Affero GPL terms attach to the service you run for other people.

## FAQ

### How do I run koreader-sync-server on my own machine?

With Docker, using the published image koreader/kosync:latest on port 7200, and mounting a Redis data volume so progress persists across restarts. The healthcheck endpoint at https://localhost:7200/healthcheck should return a state of OK, and the compose file in the repository starts the same service with ENABLE_USER_REGISTRATION=true.

### Does koreader-sync-server store my book files, titles or contents?

No. It stores no file name and no file content. Each document is represented by a unique 32 digit MD5 hash string, alongside the furthest reading progress: a percentage, a location inside the document and the device name that reported it.

### Can I recover a forgotten password on koreader-sync-server?

No, there is no forgotten password recovery. The password change endpoint requires the current key, and the password itself is hashed by the client before it reaches the server, so the server never holds a value you could send back to prove who you are.

### How do I put koreader-sync-server behind a reverse proxy that terminates TLS?

Run the server on port 17200 instead of 7200 and let the proxy forward to it. The example Traefik V3 labels set the router rule on the host name and point the service at port 17200, and your own port mapping has to expose that port as well.

## Sources

- [Issues](https://github.com/koreader/koreader-sync-server/issues)
- [koreader/koreader-sync-server on GitHub](https://github.com/koreader/koreader-sync-server)
- [License: AGPL-3.0](https://github.com/koreader/koreader-sync-server/blob/master/LICENSE)
- [README](https://github.com/koreader/koreader-sync-server/blob/master/README.md)
- [Releases](https://github.com/koreader/koreader-sync-server/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/koreader-koreader-sync-server
