Deploy a FastAPI OCR Service with Docker and Tesseract
Package a FastAPI OCR endpoint with Tesseract, safe upload handling, health checks, limits, and production verification.
On this page
An OCR API needs more than Python packages. Tesseract is a system executable with separate language data, uploaded images can consume memory and disk, and OCR work can block a web worker for much longer than an ordinary JSON request. A container makes those dependencies explicit, but the service still needs limits and operational controls.
Use only synthetic or sanitized sample documents while developing. Passport, citizenship, licence, medical, and financial documents contain sensitive data. Do not log their extracted text, raw bytes, filenames, or storage URLs.
Project structure
ocr-service/
├── app/
│ ├── __init__.py
│ └── main.py
├── Dockerfile
├── requirements.txt
└── .dockerignorePin and update dependencies through the project's chosen lock workflow. A minimal requirements file needs FastAPI, Uvicorn, multipart parsing, the Python Tesseract wrapper, and image decoding:
fastapi
uvicorn[standard]
python-multipart
pytesseract
PillowThe unpinned list illustrates direct dependencies; production should use reviewed, reproducible versions or hashes generated by the selected dependency manager.
FastAPI upload and OCR endpoint
from io import BytesIO
import pytesseract
from fastapi import FastAPI, HTTPException, UploadFile
from PIL import Image, UnidentifiedImageError
app = FastAPI()
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp"}
MAX_BYTES = 8 * 1024 * 1024
@app.get("/health/live")
def liveness() -> dict[str, str]:
return {"status": "ok"}
@app.get("/health/ready")
def readiness() -> dict[str, str]:
try:
version = str(pytesseract.get_tesseract_version())
except Exception as exc:
raise HTTPException(status_code=503, detail="OCR engine unavailable") from exc
return {"status": "ready", "tesseract": version}
@app.post("/ocr")
async def ocr(file: UploadFile) -> dict[str, str]:
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(status_code=415, detail="Unsupported image type")
data = await file.read(MAX_BYTES + 1)
await file.close()
if len(data) > MAX_BYTES:
raise HTTPException(status_code=413, detail="Image is too large")
try:
with Image.open(BytesIO(data)) as image:
image.verify()
with Image.open(BytesIO(data)) as image:
text = pytesseract.image_to_string(
image.convert("RGB"),
lang="eng",
timeout=15,
)
except (UnidentifiedImageError, Image.DecompressionBombError) as exc:
raise HTTPException(status_code=400, detail="Invalid image") from exc
except RuntimeError as exc:
raise HTTPException(status_code=504, detail="OCR timed out") from exc
return {"text": text}FastAPI's UploadFile uses a spooled file and is preferable to loading arbitrary uploads directly as a bytes parameter. This example deliberately reads only MAX_BYTES + 1 so the API can reject an oversized body. Enforce another body limit at the reverse proxy or platform because application code runs only after some request handling has already occurred.
MIME type is client-supplied and not proof of the file format; Pillow still decodes and verifies it. Pixel dimensions also matter: a compressed image can expand dramatically, so retain Pillow's decompression-bomb protection and set application-specific dimension limits.
pytesseract's timeout terminates a slow Tesseract subprocess. It is not a complete request deadline across queueing, upload, decoding, and response transmission. Add platform-level timeouts and cancellation behavior, then test them.
For larger uploads, stream into a uniquely created temporary file rather than holding all bytes in memory. Use the OS temporary directory, never trust the submitted filename as a path, close handles in finally, and delete temporary data promptly. A container filesystem is normally ephemeral and is not archival storage.
Install Tesseract in the image
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
tesseract-ocr \
tesseract-ocr-eng \
curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt ./
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY app ./app
RUN useradd --create-home --uid 10001 appuser
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl --fail http://127.0.0.1:8000/health/live || exit 1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]tesseract-ocr installs the engine; tesseract-ocr-eng installs English trained data on Debian-based images. Add only required packs such as tesseract-ocr-nep after confirming the exact package in the chosen base-image release. More languages increase image size and can change OCR behavior.
The exec-form CMD lets signals reach Uvicorn correctly. The service runs as a non-root user. curl exists only for this health-check implementation; a platform-native TCP or Python check can avoid that extra package.
Keep development reload out of production. One process per container is a common starting point when a platform handles replicas. Multiple workers increase concurrency but also multiply baseline memory and simultaneous Tesseract subprocesses.
Build and run locally
docker build --pull -t example-ocr:local .
docker run --rm \
--name example-ocr \
--memory 768m \
--cpus 1.5 \
-p 127.0.0.1:8000:8000 \
example-ocr:localThe memory and CPU values are examples. Measure representative sanitized images and set platform requests, limits, concurrency, and queue depth accordingly. When a container reaches its memory limit, the kernel or platform may terminate it; adding more web workers can make that more likely.
Inspect the installed languages and health endpoint:
docker exec example-ocr tesseract --list-langs
curl --fail http://127.0.0.1:8000/health/ready
curl --fail --form file=@sanitized-sample.png http://127.0.0.1:8000/ocrDo not put a real identity document in a shell history or shared CI artifact. Use a purpose-built sample containing non-sensitive text.
Production controls
- Authenticate and authorize access before accepting sensitive documents.
- Apply request-body, pixel-dimension, concurrency, rate, and timeout limits.
- Queue long or bursty OCR jobs instead of holding unlimited HTTP workers.
- Encrypt transport and any necessary temporary/object storage.
- Define deletion, retention, and incident policies for uploaded and extracted data.
- Scan supported upload types where the threat model requires it.
- Log request IDs, duration, size bucket, language, and outcome—not OCR contents.
- Separate liveness from readiness so a temporary dependency problem does not create restart loops.
Ordinary shared hosting commonly prevents installing system packages, running Docker, controlling process limits, or keeping workers alive. Some providers offer application containers, but support depends on the plan. If Tesseract packages and long-running processes are unavailable, use a container-capable service or VPS rather than attempting to bundle an incompatible binary blindly.
Verification checklist
- The image build installs the Tesseract engine and required language packs.
- The container runs as a non-root user with a pinned dependency set.
- Invalid MIME types, malformed images, excessive bytes, and excessive pixels are rejected.
- OCR and platform deadlines are bounded and return controlled errors.
- Health checks prove both the API and OCR executable are available.
- Representative sanitized workloads fit the configured CPU and memory envelope.
- Logs and telemetry contain no uploaded document text or raw content.
References
Documentation checked on 2026-08-12:
Related writing
- Docker networking for engineers who resent memorizing buzzwordsBridge overlays, NAT and published ports, embedded DNS versus external discovery, and debugging paths beyond restart theater.
- Deploy a Node.js Application on DirectAdmin or cPanel Shared HostingEvaluate shared-hosting Node.js support and deploy through cPanel Passenger or DirectAdmin Nginx Unit with realistic platform limits.
- Deploy a Node.js Application with PM2 and NginxBuild and run a Node.js application with PM2, proxy it through Nginx, preserve client headers, and verify the deployment safely.