A Docker-based OCR pipeline worker that:
- Polls an inbox directory for new PDF files.
- Runs OCR using OCRmyPDF with the PaddleOCR plugin via Python API.
- Generates sidecar documents via the Docling API (Markdown + JSON).
- Pushes the processed PDFs into a Paperless-ngx consume directory.
- Serves a FastAPI endpoint for on-demand document understanding (Open-WebUI compatible).
INBOX/*.pdf
│
├─ Stability check (waits for upload to finish)
│
├─ PROCESSING/
│ ├─ Docling API → sidecar JSON
│ ├─ ocrmypdf.ocr() + PaddleOCR → searchable PDF
│ └─ Push → Paperless consume/
│
├─ DONE/ (successfully processed)
└─ ERROR/ (failed processing, for inspection)
API Server (port 8000)
├─ GET /health — Health check
├─ POST /layout-parsing — Open-WebUI PaddleOCR-VL endpoint
└─ POST /extract — Direct text extraction (non-Open-WebUI)
Published image tags:
| Tag | Backend |
|---|---|
:latest |
CPU default |
:cpu |
CPU explicit |
:cuda |
NVIDIA CUDA |
Build locally:
# CPU (default)
docker build -t doc-worker:cpu -t doc-worker:latest .
# CUDA GPU (NVIDIA)
docker build --build-arg PADDLE_GPU=cuda -t doc-worker:cuda .See docker-compose.yml for a complete example:
services:
doc-worker:
image: doc-worker
ports:
- "8000:8000" # API server
volumes:
- ./inbox:/work/inbox
- ./processing:/work/processing
- ./done:/work/done
- ./error:/work/error
- ./docling:/work/docling
- ./paperless-consume:/paperless-consume
environment:
- DOCLING_BASE_URL=http://docling:12000
- DOCLING_MODE=best_effort
- OCR_LANG=deu
- OCR_USE_GPU=false
- POLL_INTERVAL=5For GPU acceleration (NVIDIA):
environment:
- OCR_USE_GPU=true
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]cp new-document.pdf ./inbox/The worker will pick it up within POLL_INTERVAL seconds.
In Open-WebUI, go to Admin Settings > Documents:
- Set Content Extraction Engine to
PaddleOCR-VL - Set API Base URL to
http://doc-worker:8000 - (Optional) Set API Token if you configured
PADDLEOCR_VL_TOKEN
Open-WebUI will call POST /layout-parsing with base64-encoded files.
# Health check
curl http://localhost:8000/health
# Extract text (direct API, non-Open-WebUI)
curl -X POST http://localhost:8000/extract \
-F "file=@document.pdf" | python -m json.tool| Variable | Default | Description |
|---|---|---|
INBOX |
/work/inbox |
Directory to poll for new PDFs |
PROCESSING |
/work/processing |
Staging area during processing |
DONE |
/work/done |
Successfully processed files |
ERROR |
/work/error |
Failed files (for inspection) |
DOCLING_DIR |
/work/docling |
Docling sidecar output |
PAPERLESS_CONSUME |
/paperless-consume |
Paperless-ngx consume directory |
OCR_LANG |
deu |
OCR language (see Language Codes) |
OCR_USE_GPU |
false |
Enable GPU acceleration (true/false) |
POLL_INTERVAL |
5 |
Seconds between inbox polls |
DOCLING_BASE_URL |
http://docling:12000 |
Docling API endpoint |
DOCLING_MODE |
best_effort |
Sidecar mode: off, best_effort, required, or native |
DOCLING_TIMEOUT |
900 |
Docling API timeout (seconds) |
MAX_RETRIES |
3 |
Max OCR retry attempts |
RETRY_DELAY |
10 |
Seconds between OCR retries |
PADDLEOCR_VL_TOKEN |
(none) | Bearer token for /layout-parsing and /extract auth (Open-WebUI) |
PADDLEOCR_MODELS |
/app/models |
Path to pre-downloaded PaddleOCR model dirs |
MAX_REQUEST_SIZE |
104857600 |
Max request body size in bytes (100 MB default) |
| Argument | Default | Description |
|---|---|---|
PADDLE_GPU |
cpu |
PaddlePaddle variant: cpu or cuda |
Health check. Returns:
{"status": "ok", "ocr_lang": "deu", "paddleocr_lang": "german", "gpu": false}The endpoint Open-WebUI calls when PaddleOCR-VL is selected as Content Extraction Engine.
Request:
POST /layout-parsing
Authorization: Bearer <PADDLEOCR_VL_TOKEN>
Content-Type: application/json
{
"file": "<base64-encoded file bytes>",
"fileType": 0, // 0=PDF, 1=image
"useDocOrientationClassify": false,
"useDocUnwarping": false,
"useChartRecognition": false
}Response:
{
"result": {
"layoutParsingResults": [
{ "markdown": { "text": "Page 1 extracted text..." } },
{ "markdown": { "text": "Page 2 extracted text..." } }
]
}
}Upload a PDF or image, extract text via PaddleOCR.
Request: multipart/form-data with field file
Response:
{
"filename": "document.pdf",
"pages": [
{
"page": 1,
"text": "Full page text...",
"blocks": [
{"text": "Line text", "bbox": [x0, y0, x1, y1], "confidence": 0.98}
]
}
],
"full_text": "All pages concatenated"
}The bundled local ocrmypdf_paddleocr plugin accepts common Tesseract-style OCR language codes:
| Language | Code |
|---|---|
| German | deu |
| English | eng |
| French | fra |
| Spanish | spa |
| Italian | ita |
| Portuguese | por |
| Dutch | nld |
| Polish | pol |
| Turkish | tur |
| Chinese (Simplified) | ch_sim |
| Chinese (Traditional) | ch_tra |
| Japanese | jpn |
| Korean | kor |
The Docker image ships with the PP-OCRv6 medium detection/recognition models and text-line orientation model pre-downloaded. PP-OCRv6 medium is a unified multilingual model, so normal OCR does not need to download per-language models at runtime.
| Mode | Behavior |
|---|---|
best_effort (default) |
Attempt Docling API. If it fails, continue with OCR. |
off |
Skip sidecar generation entirely. |
required |
If sidecar generation fails, move to ERROR. |
native |
Use local PaddleOCR instead of the Docling API. Same output format, no external dependency. |
| Path | Purpose | Required |
|---|---|---|
/work/inbox |
Input directory for new PDFs | Yes |
/work/processing |
Temporary staging area | Yes |
/work/done |
Archive of processed files | Yes |
/work/error |
Failed files for inspection | Yes |
/work/docling |
Docling sidecar output | Optional |
/paperless-consume |
Paperless-ngx consume directory | Yes |
- Unstable files: Skipped if size changes during stability check.
- Docling failures: Non-fatal by default (mode=best_effort).
- OCR failures: Retried up to
MAX_RETRIEStimes. On final failure → ERROR. - Paperless push failures: File moved to ERROR.
[Add your license here]