Distributed document sync server with multi-node sync, OCR, and a web admin panel.
2.6K
Distributed document sync server with real-time multi-node synchronization and OCR.
Jodoc Server runs the server-side of the Jodoc document management system: real-time document synchronization across nodes, a REST API for clients, OCR for searchable PDFs and images, and a web admin panel. Pair it with the desktop client or the Android client for a complete distributed document workflow.
Minimal one-liner (anonymous volumes, default ports — HTTP 8714 + HTTPS 8715):
docker run -d --name jodoc-server -p 8714:8714 -p 8715:8715 -v jodoc-data:/data -v jodoc-backups:/backups -v jodoc-certs:/certs --restart unless-stopped jventech/jodoc-server:latest
Same command, line-broken for readability:
docker run -d \
--name jodoc-server \
-p 8714:8714 \
-p 8715:8715 \
-v jodoc-data:/data \
-v jodoc-backups:/backups \
-v jodoc-certs:/certs \
--restart unless-stopped \
jventech/jodoc-server:latest
Recommended docker compose (bind-mounted host directories, healthcheck, log rotation):
services:
jodoc-server:
image: jventech/jodoc-server:latest
container_name: jodoc-server
ports:
- "8714:8714"
- "8715:8715"
environment:
- JODOC_DATA_PATH=/data
- TZ=Europe/Stockholm
# - JODOC_HTTPS=self-signed # self-signed (default) | custom | off
# - JODOC_HTTP=on # on (default) | off
volumes:
- ./jodoc-data:/data
- ./jodoc-backups:/backups
- ./jodoc-certs:/certs
restart: unless-stopped
stop_grace_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
compress: "true"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8714/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
Save as docker-compose.prod.yml, then:
docker compose -f docker-compose.prod.yml up -d
HTTPS is enabled by default (self-signed cert auto-generated in /certs volume). For LAN-bound hostnames or public Let's Encrypt certs, put a reverse proxy in front (see the Reverse Proxy section below).
| Tag pattern | Example | What it points at |
|---|---|---|
latest | latest | Most recent stable release. Safe default for new deployments. |
<major>.<minor> | 1.9 | Most recent patch release within a minor version. Pin to receive patches but not minor bumps. |
<major>.<minor>.<patch> | 1.9.1 | Specific patch version. Pin for fully deterministic deployments. |
<version>-rc<N> | 1.9.0-rc1 | Release candidate. NOT auto-synced to this README's contents; do NOT use in production. |
Examples above are illustrative — actual tag values track current releases. For per-version notes, see the project release notes.
| Variable | Default | Description |
|---|---|---|
| JODOC_DATA_PATH | /data | Data directory inside the container. Map a host volume here for persistent storage. |
| JODOC_HTTP_PORT | 8714 | HTTP listen port inside the container. Adjust the -p / ports: mapping to match if changed. |
| JODOC_HTTPS | self-signed | HTTPS listener mode (case-insensitive). self-signed (default) auto-generates a cert in /certs/server.pfx on first boot and reuses it on subsequent boots. custom loads an operator-supplied /certs/server.pfx plus /certs/server.pfx.password and fails fast at boot if either file is missing. off skips the HTTPS listener entirely. Invalid combination JODOC_HTTPS=off + JODOC_HTTP=off fails fast at boot. |
| JODOC_HTTP | on | HTTP listener toggle (case-insensitive). on (default) binds port 8714 for backward compatibility with v1.7 clients. off skips the HTTP listener — operators must override the built-in HEALTHCHECK to point at HTTPS (see Healthcheck section). |
| JODOC_TRUSTED_PROXY_CIDR | 172.16.0.0/12 | CIDR range for trusted reverse-proxy networks. Override for macvlan or custom bridge topologies. |
| JODOC_ADMIN_PASSWORD | (unset) | Optional headless override for the admin panel password. When unset, you choose the password at first login on the setup page (see Admin password). When set, it always takes precedence over a password set in the UI. |
| JODOC_PUBLIC_URL | (unset) | Optional. When set, must be an absolute http/https URL; the value is pushed to the JodocConnect server registry via the existing heartbeat loop and used by the server when emitting new QR-pairing tokens, so clients scan a token that points at the operator-reachable external URL instead of localhost. A malformed value causes the container to fail-fast at boot with the config.public_url.invalid log event. See the Reverse Proxy section for the typical Caddy-fronted deployment pattern. |
| TZ | Europe/Stockholm | Timezone used for log timestamps and scheduled backups. Standard Linux IANA timezone name. |
The web admin panel (/admin) is always password-protected. A fresh server behaves like a new NAS or router: on first visit you choose the admin password yourself.
http://<host>:8714/admin from a device on the same local network as the server. While no password is set, the admin panel is reachable only from the local network (localhost or a private LAN address) and redirects you to the setup page./admin/settings (requires the current password).The password is stored as a salted PBKDF2 hash in /data/appsettings.local.json — never in plaintext — and survives container restarts and upgrades via the /data volume.
Set the password before exposing the server to the internet. Like any NAS, complete first-run setup on the LAN first. If you front the container with a reverse proxy that does not forward the real client IP, external requests can appear to come from the local network — so do not port-forward or publish the admin panel until a password exists. (Sync, API and health endpoints are unaffected by the gate.)
Headless / no-browser setup: set the JODOC_ADMIN_PASSWORD environment variable instead — it takes effect at boot with no setup page needed.
Recovery (lost password): either
"AdminPasswordHash" line from /data/appsettings.local.json and restart the container (you'll be sent back to the setup page), orJODOC_ADMIN_PASSWORD environment variable.Note that JODOC_ADMIN_PASSWORD, when set, always takes precedence: a password changed in the UI is persisted but the env var wins again on the next boot. Unset the variable to make UI-managed passwords authoritative.
The image uses a three-volume topology so operators can apply different host-level quotas, snapshot policies, and permissions to runtime data, backups, and TLS material.
Do this once after the first start, and again after changing how the container is deployed:
docker inspect jodoc-server --format '{{json .Mounts}}'
/data must appear in the output. If it does not, your data is in the container's writable layer and will be destroyed the next time the container is recreated — which happens on any image update or redeploy, though not on a plain restart.
This check is worth the ten seconds because some orchestration layers accept a volume setting, store it, display it back to you, and never apply it. Coolify silently ignores -v in its "custom docker run options" field, for example; the correct place there is its own Persistent Storage. A missing mount is invisible until the day the container is replaced, and by then the data is gone.
Since v1.16.0 the image declares /data as a volume, so a container started with no mount at all still gets an anonymous volume rather than the writable layer — data is then at least recoverable from docker volume ls after a bad recreate. The startup log also prints a warning when /data is not a mount point. Neither is a substitute for mounting a named volume you can find again.
/dataPersistent runtime data. Contents:
server.sqlite (main) and per-vault vaults/{id}/data.sqlite/backupsOutput destination for the BackupScheduler service. Kept on a separate volume so the host can apply different retention quotas and permissions than the live /data volume.
/certsTLS certificate + password storage for the HTTPS listener. Behaviour depends on JODOC_HTTPS:
self-signed (default) — Container auto-generates /certs/server.pfx on first boot (RSA-2048 self-signed cert, CN=JodocServer, SAN localhost + IPv4/IPv6 loopback, 100-year validity). The same PFX is reused on subsequent boots so the SHA-256 fingerprint stays stable. The fingerprint is logged at startup (tls.cert.fingerprint sha256={hex}) and exposed at GET /api/server/fingerprint for out-of-band verification.custom — Operator mounts their own /certs/server.pfx plus /certs/server.pfx.password (single-line UTF-8 file containing the PFX password). The container does not auto-generate in this mode; it fails fast at boot if either file is missing.off — The HTTPS listener is disabled and the /certs volume is unused. You can omit the mount entirely.On a fresh bind-mount, ensure chown -R 10001:10001 ./jodoc-certs before the first start (matches the /data + /backups ownership convention).
All three volumes (/data, /backups, /certs when active) need ownership uid=10001 gid=10001 on the host to match the non-root jodoc user inside the image. On a fresh bind-mount, ensure chown -R 10001:10001 ./jodoc-data ./jodoc-backups ./jodoc-certs before the first start, or let docker volume create handle ownership for named volumes.
The image ships a built-in HEALTHCHECK (interval=30s timeout=5s start-period=30s retries=3) that hits GET http://localhost:8714/healthz from inside the container. Operators can verify externally after docker run:
curl -fsS http://localhost:8714/healthz
A 200 OK response indicates the ASP.NET Core host is up and the container boot self-check (data-path writability + PDFium readability) has passed. Container orchestrators (Docker Swarm, Kubernetes, Synology Container Manager) read the HEALTHCHECK status directly.
If you set JODOC_HTTP=off (HTTPS-only deployment), the built-in HEALTHCHECK above will fail because it hits HTTP:8714. Override it in your compose file — the -k flag tells curl to accept the self-signed cert without trust-store verification (the HEALTHCHECK runs inside the container against localhost):
healthcheck:
test: ["CMD", "curl", "-fkS", "https://localhost:8715/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
A Caddy reverse-proxy opt-in ships alongside the default standalone-TLS deployment as a compose overlay. Caddy terminates TLS at the edge, handles Let's Encrypt certificate acquisition and renewal automatically, and forwards plain HTTP to the container's internal listener. The overlay is appropriate when the container is reachable on a public hostname with ports 80 and 443 forwarded — Let's Encrypt's HTTP-01 challenge then completes without additional configuration.
docker compose -f docker-compose.prod.yml -f docker-compose.caddy.yml up -d
The overlay needs two files from the source tree:
docker/docker-compose.caddy.yml — the overlay itself; merged on top of docker-compose.prod.yml via the -f/-f flag pair.docker/Caddyfile.example — sample Caddyfile; copy to ./Caddyfile and edit the domain to match the public hostname.Operators must set the following environment variables in their host environment when using the overlay:
JODOC_HTTPS=off — Caddy terminates TLS, so the container's HTTPS listener is unnecessary and should be disabled to avoid double-TLS overhead.JODOC_PUBLIC_URL=https://<your-domain> — must match the domain configured in the Caddyfile; the value is pushed to JodocConnect via the heartbeat loop (see the JODOC_PUBLIC_URL row in the Environment Variables table above) so QR-pairing tokens emit the external URL instead of localhost.The Caddyfile MUST contain flush_interval -1 in the reverse_proxy directive that fronts the SignalR hub. Omitting this directive causes silent degradation of the WebSocket transport to long-polling, which adds roughly a 5× increase in sync round-trip latency without surfacing any error to the operator. Background and root-cause analysis live in Caddy issue #6733. The sample Caddyfile.example ships with this directive pre-set plus a # DO NOT REMOVE inline comment — operators MUST keep that comment as a regression-fence when customizing the file.
For the full Synology deployment procedure (DSM bind-mount permissions, troubleshooting, upgrade flow), see the project's Swedish and English deep-dive runbooks linked from the project website at https://jodox.xyz.
jodoc (uid 10001, gid 10001). The /app tree is owned by root and read-only to the runtime user; only /data, /backups, and /certs are writable.gitleaks v8.18.4 against the ServerCore publish output before the image is pushed. A leaked credential aborts the release.provenance: true and sbom: true (SLSA in-toto provenance + SPDX 2.3 software bill of materials). Both surface as tabs on the Docker Hub tag page.aquasecurity/trivy-action produces a HIGH+CRITICAL advisory report (artifact + GitHub Actions step summary) for every published tag. Currently advisory; a blocking gate is tracked as a future hardening milestone./certs on first boot). The SHA-256 fingerprint is logged at startup and exposed at GET /api/server/fingerprint for out-of-band verification. A reverse proxy (Caddy, nginx, traefik) is still recommended for hostname-bound or publicly-trusted certs.JODOC_HTTPS=off to formalize the pre-1.9 reverse-proxy-fronted pattern (HTTP-only listener, TLS terminated at the proxy). Set JODOC_HTTPS=custom to load an operator-supplied PFX from /certs/server.pfx + /certs/server.pfx.password. See the Environment Variables section for the full contract.Docker-specific operator documentation (Synology deploy guide, generic Docker deploy guide, changelog) is in development and will be linked from the project website as it lands.
Content type
Image
Digest
sha256:2e5165aec…
Size
95 MB
Last updated
21 days ago
docker pull jventech/jodoc-server