Rootoor/doctr-ocr-service
docTR OCR Service
Self-hosted REST microservice for text extraction from images. Used in the Webhook: Verify Student Card workflow.
Based on docTR (Apache 2.0) — no data sent to any external service.
Requirements
- Docker
- Docker Compose v2 (
docker compose, notdocker-compose)
Setup
cp .env.example .env
# Edit .env: set API_KEY to a strong secret
docker compose up -d --buildThe first start downloads docTR models (~130 MB) and caches them in a persistent Docker volume. Subsequent restarts are fast.
Endpoints
GET /health
Returns service status. No auth required.
curl http://localhost:8001/health{ "ok": true, "message": "OCR service is running", "data": {} }POST /extract/student-card
Main endpoint for the student card verification workflow. Extracts the student number and academic year from a card photo.
Handles automatically:
- Any orientation (portrait, landscape, 90°, 270°) — tries 4 rotations
- EXIF correction — compensates for phone orientation metadata
- Blur detection — rejects unreadable photos before OCR processing
Body: multipart/form-data with a file field (JPEG, PNG, or WebP). Max 10 MB.
curl -X POST http://localhost:8001/extract/student-card \
-F "file=@/path/to/card.jpg"Success response:
{
"ok": true,
"message": "Carte lue avec succès",
"data": {
"student_number": "645102",
"academic_year": "2024-2025"
}
}Blurry photo:
{
"ok": false,
"message": "Photo trop floue",
"data": {
"error": "La photo est trop floue pour être lue. Prends une photo plus nette, bien éclairée et sans bouger.",
"traceback": null
}
}Unreadable card (sharp photo but fields not found):
{
"ok": false,
"message": "Carte illisible",
"data": {
"error": "Impossible d'extraire le numéro étudiant ou l'année académique. Envoie une photo plus nette de ta carte.",
"traceback": null
}
}POST /extract
Extracts raw text from any image. Requires X-API-Key header if API_KEY is set.
Body: multipart/form-data with a file field (JPEG, PNG, or WebP). Max 10 MB.
curl -X POST http://localhost:8001/extract \
-H "X-API-Key: your_api_key" \
-F "file=@/path/to/image.jpg"Success response:
{
"ok": true,
"message": "Text extracted successfully",
"data": {
"full_text": "UNIVERSITE DE LOME\nNuméro: 645102\n...",
"blocks": ["block 1", "block 2"],
"page_count": 1
}
}Configuration
EC2 Deployment
Prerequisites
Ensure the EC2 instance has a swap file (critical on 2 GB RAM instances):
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstabStart the service
cp .env.example .env
# Set a strong API_KEY in .env
docker compose up -dThe service binds to 127.0.0.1:8001 only — it is not directly reachable from the internet.
Nginx configuration
Add the following to your existing Nginx config (inside the appropriate server block):
location /ocr/ {
proxy_pass http://127.0.0.1:8001/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 10m;
proxy_read_timeout 120s;
}Then reload Nginx:
sudo nginx -t && sudo systemctl reload nginxManagement
# Start
docker compose up -d
# Stop
docker compose down
# Logs
docker logs doctr-ocr -f
# Rebuild after code changes
docker compose up -d --buildStructure
doctr/
├── app/
│ ├── main.py # FastAPI app
│ └── requirements.txt # Python dependencies
├── Dockerfile
├── docker-compose.yml
├── .env.example
└── README.mdNotes
- Models are cached in the
doctr_modelsDocker volume — they survive restarts. - Uses PyTorch CPU backend (no GPU required).
- Memory capped at 1400 MB in docker-compose to protect the host.
- Uses
docker compose(v2) —docker-compose(v1) is not supported. - Student number accepted: 5–7 digits, anchored to the
Numéro:label on the card.
