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
| component | minimum | notes |
|---|---|---|
| GPU | 1× NVIDIA with at least OCR_WORKER_MEMORY_GB (default 5 GB) of VRAM per OCR worker | workers 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 driver | 535 or newer (CUDA 12) | |
| disk | 40 GB for the image, plus retention | the object store holds every document's input, page images and outputs for the retention period, so it grows with pages processed × retention days |
| CPU, RAM | each worker keeps its own copy of the models in memory (~2 GB), so budget roughly (GPU workers + document workers + 2) × 2 GB plus a few GB of overhead. A single-GPU node needs about 24-32 GB of host RAM | this is host RAM, not VRAM. Page rasterization, format conversion, Redis and the object store also run on the CPU beside the GPU workers |
| Linux | Ubuntu 22.04, or any distribution with glibc 2.35+ | |
| Docker | 24 or newer, with the NVIDIA Container Toolkit | docker run --gpus all nvidia/cuda:12.8.0-base-ubuntu22.04 nvidia-smi works |
| Docker Compose | v2.20 or newer | |
| Kubernetes, instead of Docker | Helm 3 or newer, the NVIDIA device plugin, a StorageClass for persistent volumes | one 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
Get the deployment files from the public repository (or download the ZIP from its page):
git clone https://github.com/Mathpix/pco-deploy && cd pco-deploy
The three core files are docker-compose.yml, .env.example and seaweedfs-s3.json (plus the
optional Compose overlays and the Helm chart). 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:
| variable | meaning |
|---|---|
PCO_IMAGE | the image tag from Mathpix |
PCO_DEPLOYMENT_ID | the 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_KEY | credentials 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.
For Google Cloud Storage on a Compose host that is not a GCE VM (on-prem, or any host outside
GCP) there is no metadata service to supply Application Default Credentials, so mount a
service-account key with the docker-compose.gcp.yml overlay: set PCO_GCP_CREDENTIALS_FILE in
.env to the key's path and start with both files -
docker compose -f docker-compose.yml -f docker-compose.gcp.yml --env-file .env up -d. The key
needs read/write on the bucket and permission to read the bucket's metadata (PCO checks the
bucket exists on startup), e.g. roles/storage.admin on that bucket. On GKE/GCE the node's own
service account is used and the overlay is not needed.
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 is in the same pco-deploy repository - git clone https://github.com/Mathpix/pco-deploy && cd pco-deploy if you haven't already. It 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 Mathpix CLI
The Mathpix CLI's pco service drives the deployment. Install it
with curl -fsSL https://mathpix.com/mpx-cli/install.sh | sh, then point it at your host:
export MPX_ENDPOINT=http://HOST:8080 # or pass --endpoint per call, or set it once with mpx configure
mpx pco status # versions, workers, license and metering
mpx pco convert paper.pdf # paper.mmd and paper.lines.json beside it
mpx pco convert ./scans/ --formats md,docx # a local folder: several files at a time, a live progress bar, one line per file
mpx pco convert ./scans/ ./out/ --dry-run # list the files and estimate pages; send nothing
mpx pco convert s3://example-docs/scans/ --formats md # a folder in your bucket: a server-side job, watched to the end
mpx pco convert paper.pdf --options-json '{"page_ranges": "1-5", "include_hyperlinks": true}' # any /v3/pdf option
mpx pco convert equation.png # a single image is OCR'd through /v3/text automatically
A file or local folder is sent to the deployment and the outputs are written beside the inputs (or
under a second folder you name); a cloud folder becomes a server-side job over your own bucket. A
folder run shows a live progress bar and records itself under _mathpix/<run_id>/, so you can stop
and resume it (--resume), reprocess after an image upgrade (--overwrite), or submit and collect
the results later (--detach). --include/--exclude narrow which files are picked up, and
--options-json passes any /v3/pdf option through unchanged. See the
batch jobs guide and the
CLI reference for jobs, usage and the connection flags.
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.
| variable | required | meaning |
|---|---|---|
PCO_REDIS_URL | yes | redis://host:6379/0 |
PCO_STORAGE_BUCKET | yes | bucket root URI; its form picks the provider (above) |
PCO_STORAGE_ENDPOINT | S3 only; present and empty for GCS and Azure | http://storage:8333, https://s3.amazonaws.com, your gateway. On Compose an absent variable means the included store |
PCO_STORAGE_ACCESS_KEY, PCO_STORAGE_SECRET_KEY | S3: optional together; Azure: account key in the first | unset on S3 means the default credential chain (instance role) |
PCO_DEPLOYMENT_ID | yes | the stable installation name; the compose file and the chart refuse to start without it |
OCR_WORKER_MEMORY_GB | no, default 5 | GPU memory per OCR worker; workers per GPU = VRAM / this |
PCO_LOG_FORMAT | no, default text | json for a logging tool: one object per line with the fields listed under logs |
PCO_LOG_LEVEL | no, default INFO | DEBUG adds the OCR stack's per-page bookkeeping |
PCO_LOG_SINK | no | a 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.