An Automated E-Book Library with Calibre-Web, KOReader Sync and Docker Compose
| A warm fire, a good book, and one more chapter. |
Table of Contents 📑
- Changelog
- 1. What the stack does
- 2. Prerequisites
- 3. Prepare the directories and source
- 4. Environment and migration
- 5. One Compose file for all three services
- 6. Bootstrap CWA privately
- 7. Create the sync account and bridge configuration
- 8. Disable registration and publish the stack
- 9. Connect KOReader and verify a real book
- 10. Backups, updates and rollback
- 11. Troubleshooting
- Sources
A personal e-book server becomes more useful when it can do two jobs: maintain the library and remember where you stopped reading. Calibre-Web Automated handles browsing, imports and conversion. KOReader’s sync server stores reading positions for the reader’s built-in Progress Sync plugin. A small bridge brings those positions into the library’s web interface.
This guide combines the three in one Docker Compose file, behind an existing Traefik reverse proxy. Once the initial accounts and configuration are ready, the whole application stack starts with docker compose up -d. It also covers migrating an existing Calibre-Web installation while retaining its users and library.
Changelog
| Date | Change |
|---|---|
| 2026-10-25 | Clarified synchronization latency for larger libraries. |
| 2026-10-25 | Initial version: Automated library, private KOReader sync, progress bridge and migration workflow. |
1. What the stack does
Browser -----------------------> Calibre-Web Automated
^
| progress updates
KOReader <--> KOReader Sync <--- progress bridge
server |
+-- reads library checksums| Component | Responsibility | Persistent data |
|---|---|---|
| Calibre-Web Automated, or CWA | Library UI, user accounts, imports, conversion and CWA reading state | Application configuration and Calibre library |
| KOReader Sync | Device-to-device reading positions using the normal Progress Sync protocol | Its bundled Redis data |
| kosync-cwa-bridge | Polls the sync server and forwards matching positions to CWA | A small cache of already forwarded updates |
The bridge is one-way: KOReader Sync → CWA. Changes made only in CWA do not travel back through it. Its current implementation maps one sync account to one CWA account. The default polling interval is a five-minute pause after each complete pass. Querying every unique checksum takes additional time, so larger libraries can take longer to reflect an update. A book also needs a matching checksum in the CWA library. These are explicit upstream limitations.
Why use a separate sync server?
CWA already includes a /kosync service, intended for its own KOReader plugin. That is a smaller alternative if you are happy to install that plugin on your readers. The separate server in this guide lets you keep KOReader’s ordinary Progress Sync plugin instead. The bridge translates between the two authentication schemes. More components are not a guarantee of greater reliability; choose this arrangement for the compatibility you actually need. CWA syncing documentation, bridge architecture.
This guide uses CWA v4.0.8, KOReader Sync v2.1.1 and bridge commit 286ef6f4b20955fa4a0b3f0a5f96550f70b15880. The bridge is a young project; its source is pinned here so a restart cannot silently replace it. Check current releases and advisories before your own deployment. Version pins make changes deliberate; they do not establish that every bundled dependency is free of vulnerabilities.
2. Prerequisites
You need:
- Docker Engine and Docker Compose 2.30 or newer. The example uses the
rawformat for a private environment file, which Docker documents from that version onward. - A working Traefik v3 and CrowdSec stack, with the external Docker network
proxy, entrypointwebsecure, certificate resolvertls_resolver, and the file middlewarescrowdsec-bounceranddefault. Adapt those names if your proxy uses different ones. - A DNS record for a name such as
books.example.com, pointing to that proxy. - Administrative access to the Docker host, plus Python 3 and Git.
- Enough disk space for the images, library, conversions and backups. The example limits the three containers to about 1 GiB of RAM in total; conversion workloads may need a larger CWA limit and sufficient host reserve.
- A working KOReader installation. For an older Kindle, see the installation and plugin guide.
The commands below run in an administrative shell. They do not install a second proxy, change your firewall or configure public DNS. First check the existing network and your Compose version:
docker compose version
docker network inspect proxy >/dev/nullDocker’s environment-file documentation explains format: raw. The Compose services reference describes resource limits, bind mounts and health dependencies.
3. Prepare the directories and source
Choose a new directory for this application:
mkdir -p /opt/ebook-stack
cd /opt/ebook-stack
mkdir -p config library ingest bridge/config bridge/data sync/redis sync/logs/app sync/logs/redis
chmod 700 config bridge/configKeep imports and the managed library separate. CWA removes files from the ingest directory after processing. Never point that directory at your existing collection. Official Compose template.
Fetch the bridge source once:
git clone https://github.com/ericchansen/kosync-cwa-bridge.git bridge/upstream
git -C bridge/upstream checkout --detach 286ef6f4b20955fa4a0b3f0a5f96550f70b15880
git -C bridge/upstream rev-parse HEADCheck that the last command prints that exact commit. Keep this source directory and its license: the bridge container will run the code directly from it. It is a runtime dependency, not a disposable build directory.
The example reuses CWA’s Python image for the bridge. The selected image contains requests and PyYAML, which the bridge imports. This avoids a separate local build or reliance on a prebuilt bridge image being available. Only the bridge’s Python entrypoint runs in that container; the CWA web application runs separately. Check this dependency pairing again when changing either project. Bridge package requirements.
4. Environment and migration
Create .env:
LIBRARY_DOMAIN=books.example.com
LIBRARY_PATH=./library
PUID=1000
PGID=1000
TZ=Etc/UTC
NETWORK_SHARE_MODE=false
DISABLE_LIBRARY_AUTOMOUNT=false
PUBLISH_ROUTES=false
KOSYNC_ALLOW_REGISTRATION=truePUBLISH_ROUTES=false keeps both applications out of Traefik during initial setup. CWA will be reachable through a loopback-only port. Sync registration is enabled only during this private bootstrap; disable it before publishing the routes.
Use the UID/GID that should own your CWA configuration and ingest files. The public example uses 1000:1000; that is not a universal value. Prepare ownership for the writable CWA and bridge directories, substituting your chosen IDs:
chown -R 1000:1000 config ingest bridge/config bridge/data
chmod 700 config bridge/config
chmod 600 .envDo not recursively change ownership of an existing shared library just to match this example. Check its established access policy instead.
Migrating from stock Calibre-Web
Skip this subsection for a new installation.
- Back up the old Compose, complete configuration directory and library, including
metadata.db. Confirm you can read or restore the backup. - Stop the old Calibre-Web instance and any other application writing to that library.
- Copy the old complete configuration directory into this stack’s
config/. Preserve hidden files, particularly.key, as well asapp.dband any additional databases. - Set
LIBRARY_PATHto the existing library’s real host path. CWA sees it as/calibre-library; stock Calibre-Web commonly used/books. - Keep the old Compose and untouched original configuration for rollback. Start only one library writer during the migration.
For example, after stopping the old service:
cp -a /path/to/old/config/. /opt/ebook-stack/config/
chown -R 1000:1000 /opt/ebook-stack/config
chmod 700 /opt/ebook-stack/configThe paths above are placeholders. Do not overwrite a configuration directory that already contains another active CWA installation.
CWA documents migration by reusing the configuration and library mounts. This retains existing accounts and settings, while CWA adds its own schema and features. Some binary paths and UI settings can change; v4’s interface is not identical to stock Calibre-Web. Official migration instructions.
Libraries on NFS or SMB
Set NETWORK_SHARE_MODE=true for a network library. CWA uses this to avoid SQLite WAL on the share, skip problematic ownership changes and use polling watchers. Verify the share is genuinely mounted and the expected metadata.db is readable before starting. A local directory left behind by a missing network mount is not the library. CWA network-share guidance.
If you disable automatic library detection, explicitly configure CWA’s library directory as /calibre-library. Leave automatic detection enabled for an empty new library so CWA can initialize it.
5. One Compose file for all three services
Save this as docker-compose.yml:
name: ebook-stack
services:
cwa:
image: &cwa-image crocodilestick/calibre-web-automated:v4.0.8@sha256:5e00373854247750cc3e4479b492ae09293ff5e06ed10177f226634d97888679
restart: unless-stopped
stop_grace_period: 60s
mem_limit: 768m
memswap_limit: 896m
pids_limit: 256
cpus: 1.5
environment:
PUID: ${PUID}
PGID: ${PGID}
TZ: ${TZ}
NETWORK_SHARE_MODE: ${NETWORK_SHARE_MODE}
DISABLE_LIBRARY_AUTOMOUNT: ${DISABLE_LIBRARY_AUTOMOUNT}
volumes:
- ./config:/config
- ./ingest:/cwa-book-ingest
- type: bind
source: ${LIBRARY_PATH}
target: /calibre-library
bind:
create_host_path: false
ports:
- "127.0.0.1:8083:8083"
networks: [default, proxy]
labels:
traefik.enable: ${PUBLISH_ROUTES}
traefik.docker.network: proxy
traefik.http.routers.ebook-library.rule: Host(`${LIBRARY_DOMAIN}`)
traefik.http.routers.ebook-library.entrypoints: websecure
traefik.http.routers.ebook-library.tls: "true"
traefik.http.routers.ebook-library.tls.certresolver: tls_resolver
traefik.http.routers.ebook-library.middlewares: crowdsec-bouncer@file,default@file
traefik.http.routers.ebook-library.service: ebook-library
traefik.http.services.ebook-library.loadbalancer.server.port: "8083"
logging: &logging
driver: json-file
options: {max-size: "10m", max-file: "3"}
kosync:
image: koreader/kosync:v2.1.1@sha256:bb3f13615365703315a43b9059f65e71e876440f867e23a42bf27f2fa18264e1
restart: unless-stopped
stop_grace_period: 30s
mem_limit: 128m
memswap_limit: 128m
pids_limit: 64
cpus: 0.5
environment:
TZ: ${TZ}
ENABLE_USER_REGISTRATION: ${KOSYNC_ALLOW_REGISTRATION}
volumes:
- ./sync/redis:/var/lib/redis
- ./sync/logs/redis:/var/log/redis
- ./sync/logs/app:/app/koreader-sync-server/logs
networks: [default, proxy]
labels:
traefik.enable: ${PUBLISH_ROUTES}
traefik.docker.network: proxy
traefik.http.routers.ebook-sync.rule: Host(`${LIBRARY_DOMAIN}`) && (Path(`/koreader`) || PathPrefix(`/koreader/`))
traefik.http.routers.ebook-sync.priority: "100"
traefik.http.routers.ebook-sync.entrypoints: websecure
traefik.http.routers.ebook-sync.tls: "true"
traefik.http.routers.ebook-sync.tls.certresolver: tls_resolver
traefik.http.routers.ebook-sync.middlewares: crowdsec-bouncer@file,default@file,ebook-sync-prefix
traefik.http.middlewares.ebook-sync-prefix.stripprefix.prefixes: /koreader
traefik.http.routers.ebook-sync.service: ebook-sync
traefik.http.services.ebook-sync.loadbalancer.server.port: "17200"
logging: *logging
bridge:
image: *cwa-image
restart: unless-stopped
stop_grace_period: 20s
mem_limit: 128m
memswap_limit: 128m
pids_limit: 32
cpus: 0.25
user: "${PUID}:${PGID}"
entrypoint: [python3, -m, kosync_cwa_bridge]
working_dir: /bridge
environment:
TZ: ${TZ}
PYTHONDONTWRITEBYTECODE: "1"
KOSYNC_CWA_BRIDGE_CONFIG: /config/config.yml
env_file:
- path: ./bridge/config/bridge.env
format: raw
required: false
volumes:
- ./bridge/upstream/src:/bridge:ro
- ./bridge/config:/config:ro
- ./bridge/data:/data
- type: bind
source: ${LIBRARY_PATH}
target: /calibre-library
read_only: true
bind:
create_host_path: false
tmpfs:
- /cwa-book-ingest:size=1m,mode=0700
depends_on:
cwa: {condition: service_healthy}
kosync: {condition: service_healthy}
healthcheck: {disable: true}
labels: {traefik.enable: "false"}
logging: *logging
networks:
default:
internal: true
proxy:
external: trueThe sync server’s image includes Redis; you do not need another Redis container. Its 7200 endpoint uses a self-signed TLS certificate. Traefik instead talks to the server’s plain HTTP endpoint on 17200 over Docker networking, and terminates public HTTPS itself. Only CWA has a host port, bound to loopback. KOReader server deployment notes.
The /koreader route has a higher priority than the library route and strips that prefix before forwarding. CWA keeps its separate /kosync API for the bridge. Adapt router and middleware names if another application already uses them. Traefik StripPrefix.
The bridge does not run an HTTP server, so CWA’s inherited web healthcheck is disabled for it. A running bridge process alone does not prove synchronization: inspect its preflight and cycle results as described below.
6. Bootstrap CWA privately
Validate without printing expanded configuration or secrets, then start only the two servers:
docker compose config --quiet
docker compose up -d cwa kosync
docker compose psOn the host, open http://127.0.0.1:8083. From another computer, create an SSH tunnel:
ssh -N -L 18083:127.0.0.1:8083 user@server.example.comThen browse to http://127.0.0.1:18083 on that computer. Replace the SSH account and server with your own.
For a fresh CWA installation, the documented initial account is admin with password admin123. Change it during this private setup, before enabling the public routes. For a migration, use the existing account instead. Verify the library path and inspect several books. Do not assume a successful login proves the collection is complete. CWA post-installation instructions.
In CWA Settings, enable Enable KOReader Sync (CWA Plugin) and save. Although the label mentions a plugin, the bridge also needs this setting: it enables CWA’s /kosync API and the library checksum table. The bridge supplies the HTTP requests that the CWA plugin would otherwise send. CWA setting and explanation.
Restart CWA to run its checksum backfill after enabling the setting:
docker compose restart cwa
docker compose logs --tail 60 cwaLet the backfill finish before testing the bridge. It can briefly lock the metadata database. If logs report a missing book_format_checksums table, return to CWA Settings, save the sync setting through the UI and check again; repeatedly restarting without initializing the schema will not create useful progress mappings. CWA’s supplied checksum generator can rerun the backfill once the tables exist:
docker compose exec -T cwa python3 \
/app/calibre-web-automated/scripts/generate_book_checksums.py --batch-size 257. Create the sync account and bridge configuration
The sync server and CWA are separate account systems. You can choose the same username, but the passwords do not need to match. The bridge needs both account credentials. For the pinned CWA version, its /kosync authentication code checks the actual local CWA password; do not assume a newer branch’s app-password feature is present. Pinned CWA authentication implementation.
Save the following as setup-bridge.py in the stack directory. It prompts without echoing passwords, checks the CWA login, registers the private sync account and writes the two protected bridge files. It does not reset an existing account’s password.
#!/usr/bin/env python3
import base64
import getpass
import json
import os
from pathlib import Path
import subprocess
import urllib.request
os.umask(0o077)
root = Path(__file__).resolve().parent
os.chdir(root)
cwa_user = input("Existing CWA username: ").strip()
sync_user = input("New sync username [same]: ").strip() or cwa_user
cwa_password = getpass.getpass("Existing CWA password: ")
sync_password = getpass.getpass("Password for the new sync account: ")
if getpass.getpass("Repeat sync password: ") != sync_password:
raise SystemExit("Passwords differ; no sync account created.")
if not cwa_user or not sync_user or ":" in sync_user:
raise SystemExit("Use nonempty usernames; sync usernames cannot contain ':'.")
if not cwa_password or not sync_password or any(
c in cwa_password for c in "\r\n"
):
raise SystemExit("Use nonempty passwords without line breaks.")
headers = {
"Accept": "application/vnd.koreader.v1+json",
"Authorization": "Basic " + base64.b64encode(
f"{cwa_user}:{cwa_password}".encode()
).decode(),
}
request = urllib.request.Request(
"http://127.0.0.1:8083/kosync/users/auth", headers=headers
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
assert json.load(response)["authorized"] == "OK"
except Exception:
raise SystemExit("CWA login failed; check credentials and sync settings.")
# Registration runs inside CWA's network, with credentials supplied on stdin.
# The sync server has no published host port.
register = r'''
import hashlib, json, sys, urllib.request, urllib.error
q = json.load(sys.stdin)
key = hashlib.md5(q["password"].encode()).hexdigest()
accept = {"Accept": "application/vnd.koreader.v1+json"}
request = urllib.request.Request(
"http://kosync:17200/users/create",
data=json.dumps({"username": q["username"], "password": key}).encode(),
headers={**accept, "Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
assert response.status == 201
except urllib.error.HTTPError as error:
# An existing name is acceptable only if these credentials authenticate it.
if json.load(error).get("code") != 2002:
raise SystemExit("Sync registration refused; check private bootstrap.")
request = urllib.request.Request(
"http://kosync:17200/users/auth",
headers={**accept, "x-auth-user": q["username"], "x-auth-key": key},
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
assert json.load(response)["authorized"] == "OK"
except Exception:
raise SystemExit("Sync login failed; existing account left unchanged.")
print(json.dumps({"password_md5": key}))
'''
result = subprocess.run(
["docker", "compose", "exec", "-T", "cwa", "python3", "-c", register],
input=json.dumps({"username": sync_user, "password": sync_password}),
text=True, capture_output=True, timeout=30,
)
if result.returncode:
raise SystemExit("Sync setup failed; no bridge files written.")
key = json.loads(result.stdout)["password_md5"]
config = json.loads(subprocess.check_output(
["docker", "compose", "config", "--format", "json"], text=True
))
env = config["services"]["cwa"]["environment"]
uid, gid = int(env["PUID"]), int(env["PGID"])
bridge = {
"kosync_rocks": {
"url": "http://kosync:17200",
"username": sync_user,
"password_md5": key,
},
"cwa": {
"url": "http://cwa:8083",
"username": cwa_user,
"password": "__CWA_PASSWORD_JSON__",
},
"metadata_db_path": "/calibre-library/metadata.db",
"cache_db_path": "/data/bridge_cache.db",
"poll_interval_seconds": 300,
"request_delay_seconds": 0.2,
"log_level": "INFO",
}
config_text = json.dumps(bridge, indent=2).replace("$", r"\u0024")
config_text = config_text.replace(
'"password": "__CWA_PASSWORD_JSON__"',
'"password": ${CWA_PASSWORD_JSON}',
)
folder = root / "bridge/config"
folder.mkdir(parents=True, exist_ok=True)
os.chown(folder, uid, gid)
folder.chmod(0o700)
for name, data in {
"config.yml": config_text + "\n",
"bridge.env": "CWA_PASSWORD_JSON=" + json.dumps(cwa_password) + "\n",
}.items():
path = folder / name
path.write_text(data)
os.chown(path, uid, gid)
path.chmod(0o600)
print("Accounts checked; protected bridge configuration written.")Run it from the stack directory:
chmod 700 setup-bridge.py
python3 setup-bridge.pyThe configuration key is named kosync_rocks upstream, but its URL points to your own kosync service here. No reading progress is sent to the public KOReader sync service by this example.
The private environment value is JSON-encoded because the upstream bridge substitutes variables before parsing YAML. This preserves quotes, backslashes and dollar signs in a password; JSON encoding is not encryption. format: raw keeps Compose from interpreting the encoded value.
The sync protocol uses an MD5-derived password key. Treat that key as a credential, not as harmless diagnostic data. Keep bridge/config/ private and out of public repositories and screenshots. The private environment file also contains a recoverable CWA password; administrators with Docker access can inspect container environments. KOReader authentication format, bridge configuration reference.
8. Disable registration and publish the stack
Change these values in .env:
KOSYNC_ALLOW_REGISTRATION=false
PUBLISH_ROUTES=trueThen start the complete stack:
docker compose config --quiet
docker compose up -d
docker compose ps
docker compose logs --tail 30 bridgeLook for successful authentication preflight and completed polling cycles. A cycle with checked=0 is not evidence of successful progress transfer: first check the CWA checksum backfill. Network or authentication errors must be resolved before relying on the bridge.
Confirm both public routes:
curl --fail --silent --show-error --output /dev/null \
https://books.example.com/login
curl --fail --silent --show-error \
-H 'Accept: application/vnd.koreader.v1+json' \
https://books.example.com/koreader/healthcheckThe sync healthcheck should return {"state":"OK"}. Its API expects the vendor-specific Accept header; an ordinary request without it can return a protocol-format error even while the server is healthy. Healthchecks demonstrate availability, not a completed reading-state transfer. Server healthcheck example.
9. Connect KOReader and verify a real book
In KOReader’s built-in Progress Sync settings:
- Set Custom sync server to
https://books.example.com/koreader. - Log in using the sync account created above.
- Use Binary document matching. CWA’s stored checksums identify the file; filename-based matching will not match this bridge’s checksum lookup.
- Download a book from this CWA library, open that exact file on the reader, move to a recognizable position and push its progress.
- Wait for a bridge cycle and check that CWA reflects the position.
- Open the same file on a second KOReader device and explicitly retrieve the progress to verify device-to-device synchronization.
The menu labels and binary-matching option are defined in KOReader’s Progress Sync plugin. The bridge obtains document identifiers from CWA’s checksum table.
If you previously used another sync server, its stored positions do not automatically migrate. Start with the device that has your current reading position and deliberately upload that state to the new server before asking other devices to retrieve it. Keep the reader’s local sidecar data backed up during this transition.
CWA imports and conversions can change a document’s bytes. For a first test, use a freshly downloaded library copy on both readers. If an older sideloaded file has a different checksum, the sync server can store its position while the bridge has no matching CWA entry.
You can also add CWA’s https://books.example.com/opds catalog to KOReader or an OPDS client for browsing and downloads. Catalog access and reading-position synchronization are separate configurations. CWA quick-start endpoints.
10. Backups, updates and rollback
Back up the stack’s configuration, library, bridge source and sync data. Important items include:
| Item | Why it matters |
|---|---|
config/app.db and .key | CWA accounts, application state and the key needed for encrypted settings |
config/cwa.db and the rest of config/ | CWA settings and additional persistent configuration |
Library files and metadata.db | The collection and its catalog |
sync/redis/ | Sync accounts and reading positions |
bridge/config/ | Both account mappings and the private bridge credential |
Compose, .env and bridge/upstream/ | Reproducible service configuration and pinned bridge code |
The bridge cache is reconstructible; it is less important than account credentials and progress data. CWA’s backups of processed books are useful, but are not a backup of this entire stack. CWA backup feature.
For a simple consistent backup, stop the stack and other writers to the same library, take a filesystem snapshot or copy all required data to your backup destination, then restart. On a shared library, stopping these containers alone does not stop a desktop Calibre instance. An online SQLite backup can produce a consistent individual database copy, but does not make the database, book files and Redis state one atomic recovery point. SQLite Online Backup API, Redis persistence.
Existing backup jobs may name the old Calibre-Web configuration directory explicitly. Update those paths when migrating, include cwa.db, and verify a new backup actually contains readable copies. Do not infer coverage from a scheduler’s green status.
Restore without overwriting the working library
Restore into a separate directory or isolated test environment first. Keep its routes disabled, use distinct Compose project and router names, avoid the live loopback port, and mount the restored library. Verify account access, representative book downloads, settings and progress retrieval. Only then plan a controlled replacement of production data. Prevent a restored test instance from sending mail or triggering other external integrations.
Updates and rollback
Before changing a version, read its release notes, preserve the current image identifiers and take a usable data backup. Change the image reference deliberately, review the bridge dependency pairing and run:
docker compose pull
docker compose up -dAn old image is only part of a rollback. A database migrated by a newer application may require its matching earlier backup. Preserve newer imports and reading progress before restoring an older snapshot. Keeping the old Compose alongside the new file helps recovery, but does not by itself preserve changed data.
11. Troubleshooting
| Symptom | Check |
|---|---|
| Library appears empty | Correct host bind, mounted share, metadata.db, and CWA’s internal /calibre-library path |
| CWA login works but bridge auth fails | KOReader sync enabled in CWA, correct CWA user/password, and the pinned version’s actual authentication support |
| Sync registration fails during setup | Routes still private and KOSYNC_ALLOW_REGISTRATION=true; an existing account is not overwritten |
| Reader sync works but CWA does not change | Binary matching, matching library checksum, bridge account mapping and completed poll cycles |
no such table: book_format_checksums | Initialize sync through CWA Settings before checksum generation; inspect startup logs |
database is locked on a network library | NETWORK_SHARE_MODE=true, other writers and the actual NFS/SMB mount |
/koreader/healthcheck reports a header error | Include Accept: application/vnd.koreader.v1+json |
| Readers work but CWA-only changes do not reach them | Expected one-way bridge behavior |
| Bridge container repeatedly exits | Its two credentials, configuration-file access, source mount and Python dependencies |
Useful local diagnostics:
docker compose ps
docker compose logs --tail 60 cwa kosync bridgeRead logs locally before sharing them: they can contain account names, book titles and device identifiers. Avoid dumping expanded Compose configuration or container environments into a support request.
Sources
- Calibre-Web Automated — project and migration instructions, v4.0.8 release, Compose template, sync setting, sync authentication and persistence.
- KOReader Sync Server — deployment and protocol, v2.1.1 server controller, KOReader Progress Sync plugin.
- kosync-cwa-bridge — purpose, configuration and limitations, pinned source.
- Docker Compose services, environment files and network definitions.
- Traefik — StripPrefix middleware.
- SQLite — Online Backup API and Redis — persistence.
The migration and the three-service progress path were checked using isolated configuration/database copies and an artificial reading position. The example still requires the real-reader and restore checks described above; it is not a claim of universal reader compatibility or a completed audit of every bundled dependency.





