Administrator Guide
Operating the kimidoc document-rendering service. For the REST API, see the web app's API docs view.
Install & first run
sudo snap install kimidoc snap services kimidoc # daemon should be active curl http://127.0.0.1:8080/health
The service starts automatically and listens on port 8080, bound to localhost only until an API key is configured. Open http://<server>:8080/ in a browser for the web app: template management, an editor with live PDF preview, six built-in starter templates, stats, and API documentation. Dependencies (the Qt runtime content snap) install automatically.
Configuration
All configuration is via snap set; any change validates and restarts the service automatically:
sudo snap set kimidoc port=8080 api-key=CHANGE_ME pool=4 sudo snap set kimidoc allow-remote=true allowed-hosts=cdn.example.com,*.gstatic.com snap get kimidoc # view current configuration
| Key | Default | Meaning |
|---|---|---|
| port | 8080 | Listen port |
| bind | auto | Explicit bind address. Auto = 127.0.0.1 without an API key, 0.0.0.0 with one |
| api-key | unset | Enables auth on /v1/* (X-Api-Key / Bearer) and non-loopback binding |
| pool | 3 | Concurrent render pages (see Capacity) |
| webhook-allowed-hosts | unset | CSV of allowed webhook destinations (*.suffix ok). Unset = any public host; internal/LAN receivers must be listed here explicitly (entries override the internal-range block) |
| chromium-flags | unset | Extra Chromium flags appended to the built-in set (--disable-gpu + a V8 heap cap that bounds render memory) |
| recycle-renders | 100 | Replace each render page after N documents (bounds Chromium's per-render memory growth during large batches; lower = flatter memory, slightly slower) |
| allow-remote | false | Let rendered content fetch http(s) resources; also gates /v1/render-url |
| allowed-hosts | all | CSV allowlist when remote is on; *.suffix supported |
| max-body | 8388608 | Request size cap in bytes (also caps asset uploads) |
| storage-quota | 4294967296 | Byte cap for stored blobs: uploaded assets and batch-job outputs (documents + ZIP; a 40k job needs ~2.5 GB). Asset uploads beyond it return 507; a job that hits it fails with retry instructions |
Security model
- Safe by default: without an API key the service refuses to bind beyond localhost. Set
api-keybefore exposing it to a network. - Rendering sandbox: templates execute inside Chromium's sandbox (kept fully enabled; the service itself runs as the unprivileged
snap_daemonuser under strict snap confinement). - SSRF policy: remote fetches by rendered content are off by default. When enabled, requests to loopback, link-local (cloud metadata), and private address ranges are always blocked, and
allowed-hostsrestricts the rest. Blocked requests are logged ([ssrf] blocked …) and reported to API clients viaX-Kimidoc-Blocked-Remoteresponse headers. - Webhooks: outbound job-completion callbacks are SSRF-guarded — internal/loopback/link-local destinations are blocked (checked against resolved addresses at send time) unless explicitly allowlisted via
webhook-allowed-hosts. Payloads are HMAC-signed when the submitter provides a secret. - Document encryption & stamping:
output.encrypt(on synchronous renders and batch jobs alike) produces AES-256 PDFs with per-recipient passwords and optional permission restrictions (allowPrint/allowCopy/allowModify);output.stampwatermarks every page (DRAFT/COPY/VOID). Passwords are passed to the bundledqpdfthrough private argument files, never on process command lines; the server does not store resolved passwords — they exist only in the submitted document data and the job's stored configuration. - TLS: the service speaks plain HTTP — put a reverse proxy (nginx, Caddy) in front for TLS, rate limiting, and request logging at the edge if exposed beyond a trusted network.
- Web app: static assets are served without auth (they contain no secrets); every API call the app makes is authenticated. API docs and this guide are intentionally readable without a key.
Fonts
kimidoc bundles the Noto families (including CJK and color emoji) so complex-script rendering works identically on every install. Host system fonts are deliberately not used — this keeps output deterministic across machines. To add fonts server-wide:
sudo cp CorpFont-*.ttf /var/snap/kimidoc/common/fonts/ sudo snap restart kimidoc
The directory survives upgrades. Per-template fonts can instead be embedded as @font-face data URIs (no admin access needed). At startup the service logs a warning if coverage for major writing systems is missing.
Templates
- Built-ins: six starter templates (invoice, resume, report, certificate, letter, receipt) are synced from the snap at every start and marked built-in: read-only, cannot be deleted, refreshed on upgrades. Users customize them via "Use as base", which saves a copy.
- User templates live in the service database with their sample data and render options.
- Storage: everything is in a single SQLite file at
/var/snap/kimidoc/<rev>/state/templates.db(see Backup).
Monitoring & logs
journalctl -u snap.kimidoc.kimidoc -f # follow logs curl -H "X-Api-Key: KEY" http://127.0.0.1:8080/v1/stats
Log line formats:
POST /v1/render -> 200 from 10.0.0.5 # access log, every request
render ok: 843 ms, 51234 bytes # render detail
GET /wp-admin/x -> 404 (no route) from 1.2.3.4 # unmatched hits (scanners!)
[ssrf] blocked https://... - reason # subresource policy
[page console] ... # JS errors from templates -
# first stop for "renders blank"
/v1/stats returns uptime, render counters (ok / failed / timedOut / rejectedBusy), average render time, pool utilization, and the active remote-fetch config. Expected benign startup warnings (harmless headless artifacts, do not chase): the org.freedesktop.portal D-Bus line, QRhiGles2 / Failed to create RHI GL-probe lines, and the udev monitor error.
Capacity & performance
- Pool:
poolrender pages work concurrently; further requests queue up to 8× pool, beyond which the API returns503 server busy— clients should retry with backoff. Size the pool roughly to CPU cores minus one for sustained load. - Memory: expect on the order of ~250 MB resident for the service with a pool of 3, growing modestly under load. Rendering is software-based by design (
--disable-gpu) — no GPU required or used. - Timeouts: renders are bounded per request (
timeoutMs, default 20 s) and surface as504. - Simple renders complete in well under a second; remote assets,
waitMs, and Paged.js add their own time.
Backup & restore
Three things hold all state:
/var/snap/kimidoc/current/state/templates.db # templates + asset metadata /var/snap/kimidoc/common/storage/ # uploaded asset files /var/snap/kimidoc/common/fonts/ # operator-installed fonts
sudo snap stop kimidoc sudo cp /var/snap/kimidoc/current/state/templates.db /backup/ sudo cp -r /var/snap/kimidoc/common/storage /backup/ sudo snap start kimidoc # restore = copy the files back (same paths) and restart
Built-in templates need no backup — they re-sync from the snap. Configuration is snapshotted by snap save or re-applied with snap set.
Upgrades
Snaps auto-refresh; sudo snap refresh kimidoc forces it. On stop the service drains gracefully: it stops accepting connections, finishes in-flight renders (up to 30 s), then exits — API clients see completed responses, not dropped connections. After upgrade, the database schema migrates automatically and built-in templates refresh. Roll back with sudo snap revert kimidoc.
Troubleshooting
| Symptom | Check |
|---|---|
| Service inactive after install | Port conflict? journalctl -u snap.kimidoc.kimidoc; change with snap set kimidoc port=… |
| Template renders blank | [page console] lines in the journal — usually a JS error in the template |
| Images/fonts missing from PDFs | X-Kimidoc-Blocked-Remote header / [ssrf] blocked logs → enable allow-remote, extend allowed-hosts, or inline as data URIs |
| A script renders as boxes (tofu) | Startup "font coverage missing" warning → add fonts to common/fonts |
| 504 with waitForReady | The template never calls window.pdfReady() |
| Frequent 503s | Raise pool, add client backoff, or scale out behind a load balancer |
| Can't reach from another machine | No API key set → service binds localhost only by design |