This folder contains everything needed to run ocrapi on a Compute Engine VM
with Docker, a data disk for JOBS_DIR, and a GCS bucket (legacy batch
scratch, unused by the current job pipeline). Async jobs split PDFs into
small chunks, run concurrent online processDocument calls, and the VM
assembles searchable PDFs with PyMuPDF — no Ghostscript rasterization.
The app is served over HTTPS on port 8000, with TLS terminated in-process by
uvicorn (--ssl-keyfile / --ssl-certfile) — no reverse proxy. A long-lived
self-signed certificate is generated once by 02-vm-setup.sh
and stored on the data disk at /data/tls, so uploads and results are encrypted
in transit. See TLS / certificate for client trust.
| File | Where it runs | Purpose |
|---|---|---|
01-provision.sh |
dev machine | Create SA, GCS bucket, Artifact Registry, firewall, data disk, VM (SA + Drive OAuth scopes). |
02-vm-setup.sh |
VM (root) | Format/mount data disk, generate self-signed TLS cert, install Docker, register systemd units. |
03-build-and-push.sh |
dev machine | Build linux/amd64 image, push to Artifact Registry. |
04-run.sh |
VM (root) | Pin image tag, (re)start ocrapi.service, health-check. |
ocrapi.env.example |
VM (/etc/ocrapi.env) |
Production env: GCS bucket, concurrency, paths, no key file. |
ocrapi.service |
VM systemd unit | Runs the container on 0.0.0.0:8000 over TLS, with /data/jobs and /data/tls bind mounts. |
ocrapi-cleanup.sh |
VM (/usr/local/bin) |
Prunes old job folders. |
ocrapi-cleanup.service |
VM systemd unit | Oneshot wrapper around the cleanup script. |
ocrapi-cleanup.timer |
VM systemd timer | Runs the cleanup daily + 10 min after boot. |
QUOTA.md |
runbook | How to verify and raise Document AI online OCR quota. |
Replace YOUR_PROJECT with your GCP project ID (e.g. anda-dev-457721).
# --- on your dev machine ---
gcloud config set project YOUR_PROJECT
# Optional but recommended: lock the app port to your own IP instead of the world.
# APP_SOURCE_CIDR="203.0.113.4/32" bash deploy/gcp/01-provision.sh
bash deploy/gcp/01-provision.sh # ~2 min
# Share the Drive folder with the service account printed above
# (Content Manager on the Shared Drive folder).
# See "Google Drive on GCE" below if uploads fail with 403 scope or 404 folder errors.
# --- on the VM (SSH in) ---
gcloud compute ssh ocrapi-vm --zone=us-central1-a
# Copy the deploy/ folder onto the VM (e.g. git clone the repo) and:
sudo bash deploy/gcp/02-vm-setup.sh # mounts /data, installs docker
# Edit /etc/ocrapi.env if you need to change GCP_PROJECT_ID / processor / GCS bucket / Drive ID
sudoedit /etc/ocrapi.env
# --- back on dev machine ---
bash deploy/gcp/03-build-and-push.sh # builds + pushes to Artifact Registry
# Copy the IMAGE=... line it prints.
# --- on the VM ---
sudo IMAGE=us-central1-docker.pkg.dev/YOUR_PROJECT/ocrapi/ocrapi:TAG \
bash deploy/gcp/04-run.sh # starts ocrapi.service, hits /healthzThe API is now served over HTTPS on the VM's external IP. From your machine
(substitute the external IP printed by 01-provision.sh). Trust the cert with
--cacert after copying it (see TLS / certificate), or use
-k to skip identity verification — traffic is encrypted either way:
curl -sfk https://EXTERNAL_IP:8000/healthz
# Swagger UI: https://EXTERNAL_IP:8000/docsTLS is terminated inside uvicorn (--ssl-keyfile / --ssl-certfile in
ocrapi.service) — there is no reverse proxy. 02-vm-setup.sh
generates a self-signed certificate on the data disk and reuses it on every rerun:
| Item | Value |
|---|---|
| Location | /data/tls/cert.pem, /data/tls/key.pem (survives redeploys) |
| Validity | 10 years (-days 3650) — no renewal workflow needed |
| SANs | localhost, 127.0.0.1, and the VM's internal + external IPs (from the metadata server at setup time) |
Trust it on clients — copy the cert once and pass it with --cacert:
gcloud compute scp ocrapi-vm:/data/tls/cert.pem . --zone=us-central1-a --project=YOUR_PROJECT
curl --cacert cert.pem https://EXTERNAL_IP:8000/healthzOr skip identity verification with curl -k — traffic is still encrypted; you only
lose protection against an active man-in-the-middle (passive sniffing is already
defeated by the encryption).
IP-SAN caveat. The cert pins the VM's IPs. The default external IP is ephemeral,
so a VM stop/start can change it and break --cacert validation. Either reserve a
static external IP (recommended), reach the VM by its stable internal IP, or
regenerate the cert after the IP changes.
Regenerate (e.g. after an IP change or to add a name):
sudo rm /data/tls/cert.pem /data/tls/key.pem
# optional: add a DNS alias or reserved IP to the cert
sudo TLS_EXTRA_SAN="DNS:ocrapi.intranet.local" bash deploy/gcp/02-vm-setup.sh
sudo bash deploy/gcp/04-run.sh # restart to pick up the new cert# Logs
sudo journalctl -u ocrapi.service -f
# Disk usage on the data volume
df -h /data
du -sh /data/jobs
# Manual cleanup (defaults to 7 days)
sudo RETENTION_DAYS=3 /usr/local/bin/ocrapi-cleanup.sh
# Roll a new build
bash deploy/gcp/03-build-and-push.sh
# then on the VM:
sudo IMAGE=us-central1-docker.pkg.dev/YOUR_PROJECT/ocrapi/ocrapi:NEW_TAG \
bash deploy/gcp/04-run.sh- Async jobs (
POST /v1/jobs): split PDF into chunks → concurrent onlineprocessDocumentper chunk → PyMuPDF invisible text layer → localoutput.pdf→ optional upload to Shared Drive. - Sync OCR (
POST /v1/ocr): same chunked onlineprocessDocumentpath → returns the extracted text layer astext/plain(no PyMuPDF injection). - GCS bucket is provisioned but unused by the current async pipeline.
- Default VM sizing is
c4-standard-4with a 50 GB data disk (input/output PDFs only; no page rasterization scratch).
Drive uploads from the VM use Application Default Credentials from the attached
service account (ocrapi-sa@PROJECT.iam.gserviceaccount.com). There is no JSON key
in the container (GOOGLE_APPLICATION_CREDENTIALS is intentionally unset in
ocrapi.env.example).
| Requirement | How |
|---|---|
| Document AI | IAM role roles/documentai.apiUser on the project (granted by 01-provision.sh) |
| GCS batch bucket | IAM roles/storage.objectAdmin on gs://PROJECT-ocrapi-batch |
| Artifact Registry / logging | roles/artifactregistry.reader, roles/logging.logWriter, etc. |
| Drive file upload | Not an IAM role — share the Shared Drive folder with ocrapi-sa@... as Content Manager |
| Drive API | Enable drive.googleapis.com on the project (01-provision.sh enables it) |
Set DRIVE_SHARED_FOLDER_ID in /etc/ocrapi.env to the folder ID from the Shared
Drive URL. Use the full ID (typically 33 characters). A truncated ID (e.g. missing
a leading character) causes 404 File not found on upload.
The VM must be allowed to obtain OAuth tokens that include the Drive scope.
01-provision.sh attaches the service account with:
cloud-platform,https://www.googleapis.com/auth/drive
cloud-platform— Document AI, GCS, Logging, etc.https://www.googleapis.com/auth/drive— Google Drive API (upload to Shared Drive)
If the VM was created with only cloud-platform, uploads fail with:
403 Request had insufficient authentication scopes
Fix an existing VM (brief downtime — stop the VM first):
gcloud compute instances stop ocrapi-vm --zone=us-central1-a --project=YOUR_PROJECT
gcloud compute instances set-service-account ocrapi-vm \
--zone=us-central1-a --project=YOUR_PROJECT \
--service-account=ocrapi-sa@YOUR_PROJECT.iam.gserviceaccount.com \
--scopes=cloud-platform,https://www.googleapis.com/auth/drive
gcloud compute instances start ocrapi-vm --zone=us-central1-a --project=YOUR_PROJECTVerify scopes:
gcloud compute instances describe ocrapi-vm \
--zone=us-central1-a --project=YOUR_PROJECT \
--format="json(serviceAccounts)"The app uploads searchable PDFs with the Drive v3 resumable REST API via
requests and AuthorizedSession (see app/services/drive_client.py):
POSTresumable session init (uploadType=resumable,supportsAllDrives=true)PUTPDF bytes to the sessionLocationURL- Fresh HTTP session per upload so multiple jobs can upload in parallel when OCR
finishes at the same time (
OCR_WORKER_CONCURRENCYworkers)
Async jobs: pass folder_id on POST /v1/jobs, or retry later with
POST /v1/jobs/{jobId}/drive. If upload fails after OCR succeeds, check
driveUploadError on the job status; the PDF is still available at
/v1/jobs/{jobId}/result.
Example batch (client-side parallelism; each job uploads on its own when ready):
API=https://EXTERNAL_IP:8000
ls *.pdf | xargs -n1 -P4 -I{} curl --cacert cert.pem -X POST "$API/v1/jobs" \
-F "file=@{}" \
-F "folder_id=YOUR_SHARED_DRIVE_FOLDER_ID"See QUOTA.md. The limiting factor for large async jobs is
Document AI online pages/min quota, not VM CPU. Confirm and raise it
before turning up OCR_WORKER_CONCURRENCY or ONLINE_MAX_CONCURRENCY past
the defaults shipped in ocrapi.env.example.