Skip to main content

Private Cloud OCR quickstart

This guide takes a platform engineer from an image tag to a processed document. Mathpix hands over the image in a registry you can pull from, and an install bundle: a Docker Compose stack for one host and a Helm chart for Kubernetes.

Prerequisites

componentminimumnotes
GPU1× NVIDIA with at least OCR_WORKER_MEMORY_GB (default 5 GB) of VRAM per OCR workerworkers per GPU = VRAM / OCR_WORKER_MEMORY_GB: a 24 GB card runs 4, a 48 GB card 9, a 96 GB card 19. Throughput scales with that count. We do not publish per-model throughput figures because they depend on your documents
NVIDIA driver535 or newer (CUDA 12)
disk40 GB for the image, plus retentionthe object store holds every document's input, page images and outputs for the retention period, so it grows with pages processed × retention days
CPU, RAMno published minimumpage rasterization, format conversion, Redis and the object store run on the CPU beside the GPU workers
LinuxUbuntu 22.04, or any distribution with glibc 2.35+
Docker24 or newer, with the NVIDIA Container Toolkitdocker run --gpus all nvidia/cuda:12.8.0-base-ubuntu22.04 nvidia-smi works
Docker Composev2.20 or newer
Kubernetes, instead of DockerHelm 3 or newer, the NVIDIA device plugin, a StorageClass for persistent volumesone pco pod per GPU

Network. Outbound HTTPS to global.gateway.mathpix.com for the license check, licensed builds only; an airgapped build makes no outbound connection. Inbound to the API port (default 8080) from your clients. Outbound to your bucket if you use your own, and to your receiver if you use webhooks.

From Mathpix. Pull access to the registry repository holding your image, and the tag. The image is built for your organization: it carries your license credential and your contract's modes (licensed or airgapped, metered or unmetered), so it is not interchangeable with another customer's. GET /pco/v1/status shows which modes you have.

1. Install on one host with Docker

The bundle has three files: docker-compose.yml, .env.example and seaweedfs-s3.json. You edit .env and seaweedfs-s3.json; the compose file is never edited.

docker login REGISTRY                              # with the credentials Mathpix gave you
cp .env.example .env # set PCO_IMAGE, PCO_DEPLOYMENT_ID, the two storage keys
sed -i 's/change-me-access-key/<same key as .env>/; s/change-me-secret-key/<same secret as .env>/' seaweedfs-s3.json
docker compose up -d --wait # first start: a few minutes while the models load
curl -s localhost:8080/health

.env needs four values:

variablemeaning
PCO_IMAGEthe image tag from Mathpix
PCO_DEPLOYMENT_IDthe stable name of this installation, e.g. example-prod-1. Mathpix records license check-ins and usage under it. Keep it constant across upgrades and Redis rebuilds; a new id looks like a second installation
PCO_STORAGE_ACCESS_KEY, PCO_STORAGE_SECRET_KEYcredentials for the included object store; set identically in seaweedfs-s3.json

Your own Redis or bucket. The included redis and storage containers are Compose profiles switched by COMPOSE_PROFILES in .env. Set the connection variables and drop the matching profile; the container is not created, and the compose file stays untouched:

# both included (the default in .env.example)
COMPOSE_PROFILES=included-redis,included-storage
# own Redis, included storage
COMPOSE_PROFILES=included-storage
PCO_REDIS_URL=redis://redis.internal:6379/0
# own bucket, included Redis
COMPOSE_PROFILES=included-redis
PCO_STORAGE_BUCKET=s3://example-pco/
PCO_STORAGE_ENDPOINT=https://s3.amazonaws.com # keys unset: the instance role
# both your own: no profile, and both sets of connection variables
COMPOSE_PROFILES=
PCO_REDIS_URL=redis://redis.internal:6379/0
PCO_STORAGE_BUCKET=s3://example-pco/
PCO_STORAGE_ENDPOINT=https://s3.amazonaws.com
PCO_STORAGE_ACCESS_KEY=... # or unset both keys for the instance role
PCO_STORAGE_SECRET_KEY=...

The bucket URI's form picks the provider. Google Cloud Storage and Azure Blob take no endpoint, and the variable must still be present and empty: when it is absent from .env, Compose falls back to the included store's address, which the GCS and Azure clients would read as an emulator.

# Google Cloud Storage: Application Default Credentials, here the host VM's service account
COMPOSE_PROFILES=included-redis
PCO_STORAGE_BUCKET=gs://example-pco/
PCO_STORAGE_ENDPOINT=
PCO_STORAGE_ACCESS_KEY=
PCO_STORAGE_SECRET_KEY=
# Azure Blob: the account key, or both keys empty for the host VM's managed identity
COMPOSE_PROFILES=included-redis
PCO_STORAGE_BUCKET=https://example.blob.core.windows.net/pco/
PCO_STORAGE_ENDPOINT=
PCO_STORAGE_ACCESS_KEY=<account key>
PCO_STORAGE_SECRET_KEY=

Credentials come from where the container runs: on Compose, the host VM's identity (an EC2 instance role, a GCE service account, an Azure managed identity) or the S3 keys and Azure account key above; on Kubernetes, the node's identity in the same way. The chart does not yet attach a per-pod identity (GKE Workload Identity, Azure Workload Identity), so grant the bucket to the node pool's identity or use keys. Run Redis with persistence (appendonly yes, or ElastiCache and Azure Cache with persistence on): the deployment recovers usage counters from the bucket after a Redis loss, but documents queued in Redis at that moment are lost.

2. Install on Kubernetes with Helm

The chart installs everything in one command: a pco Deployment with one GPU per pod, Redis and SeaweedFS as StatefulSets on persistent volumes, the Service, and the pull secret for your registry credentials.

cat > pco-values.yaml <<'VALUES'
image: REGISTRY/mathpix-pco:TAG
registry: {server: REGISTRY, username: USER, password: TOKEN}
deploymentId: example-prod-1
replicas: 2 # one pod per GPU
storage: {accessKey: <any random string>, secretKey: <any random string>}
VALUES
helm install pco ./helm/mathpix-pco -n pco --create-namespace -f pco-values.yaml
kubectl -n pco rollout status deployment/pco-pco # a few minutes while the models load

Your own Redis or bucket: redis.enabled: false with redis.url, or storage.enabled: false with storage.bucket and, for S3, storage.endpoint (leave the keys empty to use the node's IAM role). GPU nodes with taints: nodeSelector and tolerations. Clusters that expose the NVIDIA runtime as a RuntimeClass, such as k3s and RKE2: runtimeClassName: nvidia; EKS, GKE and AKS with the NVIDIA device plugin need nothing. Reaching the API from outside the cluster: service.type: LoadBalancer, or your Ingress in front of the pco-pco Service. The chart refuses to render when a required value is missing and says which. helm upgrade replaces pods one at a time, old pod first, so it never needs a spare GPU; helm uninstall keeps the Redis and storage volumes, delete their PersistentVolumeClaims yourself when you mean to. Every value is documented in the chart's values.yaml.

3. Process a document

Health is {"status": "ok", ...} when Redis and storage answer and at least one GPU worker is alive. Then:

curl -sF file=@sample.pdf localhost:8080/v3/pdf                          # {"pdf_id": "...", "status": "processing"}
curl -s localhost:8080/v3/pdf/<pdf_id> # status, num_pages, num_pages_completed
curl -s localhost:8080/v3/pdf/<pdf_id>.mmd # the Mathpix Markdown
curl -sF file=@sample.pdf -F 'options_json={"conversion_formats":{"docx":true}}' localhost:8080/v3/pdf
curl -s -o out.docx localhost:8080/v3/pdf/<pdf_id>.docx # 202 with Retry-After while converting

The request and response shapes are the hosted API's: Process Documents and Process Images apply, minus app_id and app_key.

4. Or use the pco tool

pco config set endpoint http://HOST:8080
pco status # health, versions, workers, license
pco convert paper.pdf # paper.mmd and paper.lines.json beside it
pco convert ./scans/ --formats md,docx # a folder: several files at a time, one progress line per file
pco convert s3://example-docs/scans/ --formats md # a folder in your bucket: a job on the deployment, watched to the end
pco convert paper.pdf --options-json '{"page_ranges": "1-5", "include_hyperlinks": true}' # any /v3/pdf option
pco text equation.png # one image through POST /v3/text, the JSON body printed

--detach returns once the deployment has the work; pco convert --resume RUN_ID fetches local results later. Ctrl-C pauses and asks before stopping. --dry-run sends nothing. --options-json passes any /v3/pdf option through unchanged, and on pco text any /v3/text option. The tool will be released as open source with its own documentation.

Configuration reference

Everything the deployment reads from the environment. Nothing else is configurable at run time; the customer name, license mode, metering mode and credential are baked into your image.

variablerequiredmeaning
PCO_REDIS_URLyesredis://host:6379/0
PCO_STORAGE_BUCKETyesbucket root URI; its form picks the provider (above)
PCO_STORAGE_ENDPOINTS3 only; present and empty for GCS and Azurehttp://storage:8333, https://s3.amazonaws.com, your gateway. On Compose an absent variable means the included store
PCO_STORAGE_ACCESS_KEY, PCO_STORAGE_SECRET_KEYS3: optional together; Azure: account key in the firstunset on S3 means the default credential chain (instance role)
PCO_DEPLOYMENT_IDyesthe stable installation name; the compose file and the chart refuse to start without it
OCR_WORKER_MEMORY_GBno, default 5GPU memory per OCR worker; workers per GPU = VRAM / this
PCO_LOG_FORMATno, default textjson for a logging tool: one object per line with the fields listed under logs
PCO_LOG_LEVELno, default INFODEBUG adds the OCR stack's per-page bookkeeping
PCO_LOG_SINKnoa copy of every line from the container, for a host without a log agent: syslog+udp://, syslog+tcp://, syslog+tls://host:port, or cloudwatch://GROUP?region=...; see shipping logs

A missing or contradictory variable stops the container at startup with the variable named in the log, rather than at the first request.

If you lose the deployment id (a rebuilt host with no copy of .env): while the service runs, GET /pco/v1/status shows it; the bucket holds a copy at pco-meta/deployment_id; and for a licensed or metered build Mathpix support can read it back to you from your organization name. An airgapped, unmetered build sends nothing to Mathpix, so for it the bucket copy and your usage exports are the record.

Where to go next

  • Operations: what to monitor, usage and license, upgrades, retention, troubleshooting.
  • Batch jobs: a folder in your bucket in, outputs beside the inputs out.