From zero to a compiled document.
nautris is self-hosted: it runs entirely in your infrastructure as a Docker image. This page takes you from an empty machine to parsing your first document.
On this page
Current versions
These are the versions we publish and run in production today. Pin them rather than tracking :latest: a pinned tag is the only way a redeploy cannot quietly change the software under a corpus you already compiled.
| Component | Current version | Pull |
|---|---|---|
| Engine | 0.9.20 | registry.nautris.com/nautris:0.9.20 |
| RAG generator | 0.5.2 | registry.nautris.com/nautris-rag-generator:0.5.2 |
The registry is public and read-only, so you can always check what exists without an account, rather than trusting this page to be current:
curl -s https://registry.nautris.com/v2/nautris/tags/list
curl -s https://registry.nautris.com/v2/nautris-rag-generator/tags/list
:latest is a convenience, not a contract. It moves whenever we publish, so two machines that pulled it a week apart are not running the same engine, and neither is the same machine before and after a docker compose pull. Use it to try nautris out. Once you have compiled anything you intend to keep, pin the version, so that re-running a corpus produces the same output it did last time.Deploy with Docker
Pull the engine image straight from our registry, no account or login required, then deploy with a compose file. The image is public; the engine still needs a valid license to run (it refuses to start without one), so the license, not the registry, is the gate.
docker pull registry.nautris.com/nautris:0.9.20 # CPU, ~2.7 GB, amd64 + arm64
# docker pull registry.nautris.com/nautris:0.9.20-gpu # only if you have an NVIDIA GPU
# docker-compose.yml - one image, two roles: API + N workers on a shared volume
services:
api:
image: registry.nautris.com/nautris:0.9.20
restart: unless-stopped
ports: ["8000:8000"]
environment:
NAUTRIS_LICENSE: ${NAUTRIS_LICENSE:?required}
NAUTRIS_WORKERS: "0" # the API enqueues...
NAUTRIS_BACKGROUND: "1" # ...the worker containers process
NAUTRIS_RAG_DB: /data/lancedb
volumes:
- engine-data:/data
read_only: true # the API writes only to the volume and /tmp
tmpfs: [/tmp]
worker:
image: registry.nautris.com/nautris:0.9.20
command: ["python", "-m", "nautris.worker"]
restart: unless-stopped
environment:
NAUTRIS_RAG_DB: /data/lancedb
volumes:
- engine-data:/data
# Workers parse untrusted bytes: no privilege escalation, hard caps.
security_opt:
- no-new-privileges:true
mem_limit: 8g # isolates a heavy doc to one worker; large OCR PDFs peak past 4g
pids_limit: 256
volumes:
engine-data:
Which image to pull
The default tag is the CPU image, and it is what most self-hosters want: ~2.7 GB on disk, multi-architecture, and it runs the complete extraction pipeline. A GPU is an optimization, not a requirement. Pull the CUDA image only if you actually have an NVIDIA GPU, because it is more than twice the size.
| Tag | Contents | Architectures | Size on disk |
|---|---|---|---|
:latest, :X.Y.Z, :X.Y | CPU. Full extraction pipeline, no CUDA | amd64 + arm64, resolved automatically | ~2.7 GB |
:latest-gpu, :X.Y.Z-gpu | CUDA build. Adds GPU acceleration and the opt-in NAUTRIS_VLM_OCR tier | amd64 only | ~6-7 GB |
That is the whole list: two tags to choose between, each pinnable by version. There is no architecture-suffixed tag to pull, because there is nothing to choose - :X.Y.Z is a multi-architecture manifest and docker pull resolves your architecture on its own. Confirm any tag yourself with docker manifest inspect registry.nautris.com/nautris:X.Y.Z, which lists every platform the tag really carries.
:latest-gpu or :X.Y.Z-gpu. A GPU worker left on the unsuffixed tag keeps working, it just runs on the CPU.Apple Silicon and ARM servers are covered by the default tag: docker pull resolves the arm64 build automatically, and no platform: line is needed. arm64 is CPU-only (the CUDA wheel index publishes nothing for aarch64), so NAUTRIS_VLM_OCR is unavailable there, but everything else works, including the full ML pipeline, because arm64 needs no AVX2. Do not force platform: linux/amd64 on a Mac: the emulated x86 image has no AVX2 under Rosetta, which limits it to born-digital documents.
Extraction models are fetched once on first run into the shared volume. Scale extraction with docker compose up --scale worker=4.
System requirements
No GPU is required: the core path is fully CPU-deterministic, and a GPU only accelerates the same OCR when one is present. What actually matters:
| Resource | Minimum | Comfortable | What consumes it |
|---|---|---|---|
| Disk | ~8 GB free | 20 GB+ | CPU image ~2.7 GB (the default) or CUDA image ~6-7 GB + model cache ~1.3 GB, fetched on first job + job artifacts. Budget ~12 GB minimum if you pull the CUDA image |
| RAM | ~8 GB (1 worker) | 16 GB+ | extraction peaks at 3–6 GB per worker (more on large / OCR-heavy PDFs); the API process is light. Budget per-worker RAM × worker count |
| CPU | 2 cores (any); AVX2 + FMA for OCR | 4+ cores, AVX2 + FMA | Born-digital PDFs use the model-free fast path and run on any CPU - older x86, ARM, Apple Silicon. The ML / OCR path (scanned pages, complex layouts) needs AVX2 + FMA on x86: its runtime (onnxruntime/torch) is built for them, so Intel Haswell (2013)+ / AMD Zen (2017)+ is the floor there; ARM/Apple Silicon run it fine without AVX2. On an x86 chip without AVX2 the ML path is unavailable and those documents fail cleanly with a clear error (as of engine 0.9.2 - no longer a worker crash). Above the floor, cores are throughput only: native-text pages are fast, OCR pages run ~20–90 s/page |
| GPU | optional | "GPU" here means NVIDIA CUDA, and it needs the -gpu image plus a GPU device reservation on the worker (10-30x faster on scanned / OCR-heavy PDFs, identical output). The CUDA image also falls back to the CPU on hosts without a GPU, so it is safe to run either way, just larger. Apple GPUs are not used: Docker on macOS runs containers in a Linux VM with no Metal passthrough, so a containerized engine on a Mac is CPU-only whatever the host hardware | |
Measured throughput: born-digital documents extract at roughly 280-300 pages/min on a single GPU worker (a 150-page report finishes in about 32 s); full GPU OCR on image-heavy scans runs around 20 pages/min. On the published benchmark corpus the engine scores 100% on tables, reading order, and headings.
Is the CPU image good enough for OCR?
Yes, and the output is the same. Both images run the same OCR pipeline with the same models and the same weights, so a scanned page yields the same text either way. A GPU does not read better, it reads faster. This is the one thing worth being precise about, because "CPU-only" is often assumed to mean "degraded", and here it does not.
What a GPU changes is throughput, and only really on scanned pages. Measured on the same documents, CPU on a 4-core laptop-class chip against a single NVIDIA worker:
| Document | CPU image | GPU image | Difference |
|---|---|---|---|
| Scanned page (OCR, full ML path) | ~80-85 s/page | ~6-14 s/page | roughly 6-14x |
| Born-digital page (model-free fast path) | ~3.7 s/page | ~1.4 s/page | roughly 2.6x |
| Text output | identical | ||
So the choice follows your corpus, not your quality bar:
- Mostly born-digital (exports, generated reports, Office files): the CPU image, comfortably. A GPU is idle money here, and the fast path never touches a model anyway.
- Occasional scans: the CPU image. A minute a page is fine for a background queue; add workers rather than a GPU, since extraction scales across them.
- Scan-heavy and time-sensitive (an archive backlog, or anything a person is waiting on): the GPU image. At ~80 s/page a thousand scanned pages is around a day of CPU, against roughly two hours on one GPU worker.
- You want
NAUTRIS_VLM_OCR: the GPU image is the only option. The CPU image does not ship the VLM tier at all, so this is a capability difference rather than a speed one.
NAUTRIS_OCR_LANGS is usually the more productive knob: a recogniser set to the wrong script is the most common cause of mangled Latin text.When resources run short, failures stay contained and visible, never silent corruption:
- Disk full: image pulls fail loudly; a mid-job failure retries, then lands in the dead-letters view in
/uiwith its error. Free space, hit Requeue. - Out of memory: under Docker Compose each worker is fenced by
mem_limit(8 GB in the example above; raise it for large OCR-heavy PDFs, which can peak past 4 GB), so a heavy document kills only that worker (exit 137) and the job dead-letters while the API and UI stay up. Bare pip installs have no fence, so prefer Compose on small machines. - Slow CPU: never fatal.
POST /v1/parsedegrades to202 + job IDafter ~55 s and the queue keeps working; pollGET /v1/jobs/{id}. - x86 CPU without AVX2/FMA: no longer fatal (as of engine 0.9.2). Born-digital documents still extract via the model-free fast path. Documents that need the ML pipeline (scanned / OCR / complex layouts) fail cleanly with a clear
ml-unavailableerror instead of crash-looping the worker (older versions aborted with SIGILL / exit132). To process those, run the engine on a newer x86 host (AVX2), an NVIDIA GPU, or ARM/Apple Silicon (which run the ML path without AVX2). Check x86 support withgrep -qw avx2 /proc/cpuinfo && echo ok || echo "no AVX2: born-digital only". - Oversized inputs: uploads stream against a hard cap (413 past 50 MB by default) instead of buffering into memory; workers run with pids/memory caps and a read-only rootfs.
Quickstart
A valid license is required: the server refuses to start without one (personal licenses are free and instant at get.nautris.com). There are no API keys to invent: the engine mints a default token on first boot, prints it in the startup logs, and shows it in /ui.
# Put the token in a .env file next to docker-compose.yml. Compose reads it
# automatically, so it survives a new shell and a reboot. (An exported shell
# variable works too, but only in the shell you exported it in.)
echo 'NAUTRIS_LICENSE=nl1....' > .env # free personal license
docker compose up -d
docker compose logs api | grep "Default API token"
# Default API token: dek_...
Parse a document and search it (send the token as X-API-Key):
# parse one file, get the canonical tree back in a single call
curl -X POST localhost:8000/v1/parse -H "X-API-Key: dek_..." -F "file=@mydoc.pdf" | jq .tree
# semantic search across everything indexed
curl -G localhost:8000/v1/search -H "X-API-Key: dek_..." --data-urlencode "q=your question" | jq '.[0]'
Then open the review dashboard at http://localhost:8000/ui. It signs itself in with the default token automatically; view, copy, or rotate the token on its API token page.