Sign inSign up

jventech/jodoc-server

By jventech

•Updated 21 days ago

Distributed document sync server with multi-node sync, OCR, and a web admin panel.

Image
0

2.6K

jventech/jodoc-server repository overview

⁠Jodoc Server

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.

⁠Quick Start

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).

⁠Supported Tags

Tag patternExampleWhat it points at
latestlatestMost recent stable release. Safe default for new deployments.
<major>.<minor>1.9Most recent patch release within a minor version. Pin to receive patches but not minor bumps.
<major>.<minor>.<patch>1.9.1Specific patch version. Pin for fully deterministic deployments.
<version>-rc<N>1.9.0-rc1Release 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.

⁠Environment Variables

VariableDefaultDescription
JODOC_DATA_PATH/dataData directory inside the container. Map a host volume here for persistent storage.
JODOC_HTTP_PORT8714HTTP listen port inside the container. Adjust the -p / ports: mapping to match if changed.
JODOC_HTTPSself-signedHTTPS 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_HTTPonHTTP 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_CIDR172.16.0.0/12CIDR 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.
TZEurope/StockholmTimezone used for log timestamps and scheduled backups. Standard Linux IANA timezone name.

⁠Admin password

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.

  1. Browse to 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.
  2. Choose a password (minimum 8 characters). You're logged in immediately, and can change it later under /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

  • delete the "AdminPasswordHash" line from /data/appsettings.local.json and restart the container (you'll be sent back to the setup page), or
  • set the JODOC_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.

⁠Volumes

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.

⁠Verify the mount actually took effect

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.

⁠/data

Persistent runtime data. Contents:

  • SQLite databases: server.sqlite (main) and per-vault vaults/{id}/data.sqlite
  • DataProtection keys (antiforgery / cookie keys — must persist across restarts)
  • Uploaded document blobs
  • OCR cache
⁠/backups

Output 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.

⁠/certs

TLS 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.

⁠Healthcheck

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

⁠Reverse Proxy

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.

⁠Quick start
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.
⁠SignalR WebSocket contract

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⁠.

⁠Security

  • Non-root runtime user. The container runs as 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.
  • Pre-publish secret scan. Every release runs gitleaks v8.18.4 against the ServerCore publish output before the image is pushed. A leaked credential aborts the release.
  • Provenance + SBOM attestation. Images are built with 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.
  • Trivy CVE scan on every release. 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.
  • HTTPS by default (self-signed). The container listens on both HTTP:8714 (v1.7-compat) and HTTPS:8715 (self-signed cert, auto-generated in /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.
  • Standalone TLS toggle. Set 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.

⁠Documentation

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.

Tag summary

Content type

Image

Digest

sha256:2e5165aec…

Size

95 MB

Last updated

21 days ago

docker pull jventech/jodoc-server