Running the reference kosync server locally
September 21, 2026 · View on GitHub
koreader/koreader-sync-server is Lua on OpenResty with Redis. There is no
written guide to running it outside Docker, and the Docker path has a couple of
sharp edges. This is the recipe that worked on 2026-09-21, written down because
the verifier's results are only meaningful if you can reproduce the thing it is
measured against.
The short version
./run.sh # starts it on http://127.0.0.1:8080
./run.sh stop
Then:
node ../verify.mjs --base-url http://127.0.0.1:8080 \
--user refuser1 --password refpass1 --register --strict-accept
What the image gives you
docker.io/koreader/kosync:latest runs runsvdir with two services, Redis and
the app, and exposes:
| Port | Protocol | Notes |
|---|---|---|
7200 | HTTPS | self-signed certificate generated at image build time |
17200 | HTTP | for use behind a TLS-terminating proxy |
Map 17200 unless you are specifically testing TLS. A self-signed
certificate makes Node's fetch fail with UNABLE_TO_VERIFY_LEAF_SIGNATURE,
and the workaround (NODE_TLS_REJECT_UNAUTHORIZED=0) disables verification
process-wide.
The image sets ENABLE_USER_REGISTRATION=true and GIN_ENV=production in its
Dockerfile, so registration is open and Redis database 1 is in use.
Verified on 2026-09-21: image db2a16684d0b, linux/arm64, OpenResty
1.29.2.3, X-Framework: gin/0.2.0.
Podman instead of Docker
The image is multi-arch and runs unmodified under Podman, which needs no background daemon on macOS beyond its own VM:
podman machine init # once; harmless "already exists" if you have one
podman machine start
podman run -d --name kosync-ref -p 8080:17200 -p 8443:7200 \
docker.io/koreader/kosync:latest
To reset state between runs, flush the database rather than recreating the container:
podman exec kosync-ref redis-cli -n 1 flushdb
Without containers, on macOS
Possible in principle, and it is where most of a day can go. Recording what was learned so nobody repeats it.
OpenResty's Homebrew formula does not build on current macOS. It passes
--with-http_geoip_module, and the geoip formula it depends on was removed
from homebrew-core, so ./configure fails:
./configure: error: the GeoIP module requires the GeoIP library.
Building OpenResty from source works — the default configure does not enable GeoIP:
brew install openresty-openssl3 pcre2 redis
curl -fsSLO https://openresty.org/download/openresty-1.31.1.1.tar.gz
tar xzf openresty-1.31.1.1.tar.gz && cd openresty-1.31.1.1
./configure --prefix="$PWD/../openresty" \
--with-cc-opt="-I/opt/homebrew/include -I/opt/homebrew/opt/openresty-openssl3/include" \
--with-ld-opt="-L/opt/homebrew/lib -L/opt/homebrew/opt/openresty-openssl3/lib" -j8
make -j8 && make install
…but the resulting binary segfaulted on this machine (macOS 26, arm64):
every worker died with signal 11 at startup, including with a configuration
containing no Lua at all —
server { listen 8080; location / { return 200 "plain-ok\n"; } }
— so the crash is in the built nginx, not in the Lua application.
nginx -t reported the configuration valid; running it exited 139. The link
step had warned:
ld: warning: building for macOS-26.0, but linking with dylib
'.../libluajit-5.1.2.dylib' which was built for newer version 26.7
This was not chased further, because the container path works. If you get that binary running, the configuration below is what the app needs, and it has not been executed end to end — treat it as a starting point, not a verified recipe.
gin is from 2015 and pins old dependencies; do not luarocks make it. It
is pure Lua, so putting it on lua_package_path is enough:
git clone --depth 1 https://github.com/koreader/koreader-sync-server.git
git clone --depth 1 https://github.com/ostinelli/gin.git
The app's config/nginx.conf is a template with {{GIN_INIT}} and
{{GIN_RUNTIME}} placeholders that gin's launcher substitutes
(gin/cli/launcher.lua:49-52 and :87-91). Expanded by hand:
worker_processes 1;
daemon off;
env ENABLE_USER_REGISTRATION;
env GIN_ENV;
events { worker_connections 1024; }
http {
lua_code_cache on;
lua_package_path "<sync-server>/?.lua;<gin>/?.lua;<openresty>/lualib/?.lua;<openresty>/lualib/?/init.lua;;";
lua_package_cpath "<openresty>/lualib/?.so;;";
server {
listen 8080;
location / {
content_by_lua 'require("gin.core.router").handler(ngx)';
}
}
}
Two things that are easy to get wrong:
- nginx's working directory must be the sync-server checkout.
gin/core/router.lua:1prepends./app/controllers/?.luatopackage.path, andconfig.routesis resolved relative to the prefix. Usenginx -p <sync-server>. GIN_ENVselects the Redis database, not the port.developmentuses database 3,test2,production1 (koreader-sync-server db/redis.lua:5-25). The port comes from yourlistendirective;config/settings.lua's port is only read by gin's own launcher.db/redis.luahardcodes127.0.0.1:6379, so running Redis elsewhere means editing that file.
A note on --strict-accept
The reference server requires Accept: application/vnd.koreader.v1+json
and uses it to select the API version. Run the verifier with --strict-accept
against it, or the relaxed-Accept check will be reported as a deviation when it
is actually correct behaviour. See SPEC.md §3.3.