Merge pull request #39 from scub-france/feature/docker-split-local-remote

Feature/docker split local remote
This commit is contained in:
Pier-Jean Malandrino 2026-04-03 12:19:16 +02:00 committed by GitHub
commit b34b56aba2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 207 additions and 58 deletions

View file

@ -1,3 +1,13 @@
# Conversion engine: "local" (in-process Docling) or "remote" (Docling Serve)
# CONVERSION_ENGINE=local
# Docling Serve settings (remote mode only)
# DOCLING_SERVE_URL=http://localhost:5001
# DOCLING_SERVE_API_KEY=
# Max seconds per conversion (default: 600)
# CONVERSION_TIMEOUT=600
# CORS (comma-separated origins, only needed for custom deployments) # CORS (comma-separated origins, only needed for custom deployments)
# CORS_ORIGINS=http://localhost:3000,https://your-domain.com # CORS_ORIGINS=http://localhost:3000,https://your-domain.com

View file

@ -1,4 +1,4 @@
name: Release Docker Image name: Release Docker Images
on: on:
push: push:
@ -11,12 +11,16 @@ env:
jobs: jobs:
release: release:
name: Build & push Docker image name: Build & push — ${{ matrix.target }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write packages: write
strategy:
matrix:
target: [remote, local]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -28,15 +32,15 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version from tag - name: Docker metadata
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}},suffix=-${{ matrix.target }}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}},suffix=-${{ matrix.target }}
type=raw,value=latest type=raw,value=latest-${{ matrix.target }}
- name: Extract version from tag - name: Extract version from tag
id: version id: version
@ -45,6 +49,7 @@ jobs:
- uses: docker/build-push-action@v6 - uses: docker/build-push-action@v6
with: with:
context: . context: .
target: ${{ matrix.target }}
push: true push: true
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: APP_VERSION=${{ steps.version.outputs.value }} build-args: APP_VERSION=${{ steps.version.outputs.value }}

View file

@ -22,7 +22,13 @@ Thank you for your interest in contributing to Docling Studio! This guide will h
```bash ```bash
cd document-parser cd document-parser
python -m venv .venv && source .venv/bin/activate python -m venv .venv && source .venv/bin/activate
# Remote mode (lightweight — delegates to Docling Serve)
pip install -r requirements.txt pip install -r requirements.txt
# Local mode (full — runs Docling in-process)
pip install -r requirements-local.txt
pip install ruff pytest pytest-asyncio httpx # dev tools pip install ruff pytest pytest-asyncio httpx # dev tools
uvicorn main:app --reload --port 8000 uvicorn main:app --reload --port 8000
``` ```
@ -129,11 +135,16 @@ We use [Semantic Versioning](https://semver.org/): `MAJOR.MINOR.PATCH`.
### Docker Image Tags ### Docker Image Tags
Each release produces two image variants:
| Tag | Description | | Tag | Description |
|-----|-------------| |-----|-------------|
| `X.Y.Z` | Exact version | | `X.Y.Z-remote` | Exact version — lightweight (Docling Serve) |
| `X.Y` | Latest patch of this minor | | `X.Y.Z-local` | Exact version — full (in-process Docling) |
| `latest` | Latest stable release | | `X.Y-remote` | Latest patch of this minor — lightweight |
| `X.Y-local` | Latest patch of this minor — full |
| `latest-remote` | Latest stable — lightweight |
| `latest-local` | Latest stable — full |
### Hotfix ### Hotfix

View file

@ -1,10 +1,10 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# ============================================================================= # =============================================================================
# Docling Studio — single-image build (frontend + backend) # Docling Studio — single-image build (frontend + backend, multi-target)
# #
# Usage: # Usage:
# docker build -t docling-studio . # docker build --target remote -t docling-studio:remote .
# docker run -p 3000:3000 docling-studio # docker build --target local -t docling-studio:local .
# ============================================================================= # =============================================================================
# --- Stage 1: Build frontend assets --- # --- Stage 1: Build frontend assets ---
@ -18,21 +18,19 @@ RUN npm ci
COPY frontend/ . COPY frontend/ .
RUN VITE_APP_VERSION=${APP_VERSION} npm run build RUN VITE_APP_VERSION=${APP_VERSION} npm run build
# --- Stage 2: Runtime (Python + Nginx) --- # --- Stage 2: Base runtime (Python + Nginx) ---
FROM python:3.12-slim FROM python:3.12-slim AS base
ARG APP_VERSION=dev ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION} ENV APP_VERSION=${APP_VERSION}
# System deps: poppler (pdf2image), nginx, and OpenCV runtime libs # System deps: poppler (pdf2image), nginx
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
poppler-utils \ poppler-utils \
libgl1 \
libglib2.0-0 \
nginx \ nginx \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Python deps # Python deps (common)
WORKDIR /app WORKDIR /app
COPY document-parser/requirements.txt . COPY document-parser/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
@ -57,5 +55,24 @@ ENV DB_PATH=/app/data/docling_studio.db
EXPOSE 3000 EXPOSE 3000
# nginx needs to run as root for port binding, then drops to appuser for uvicorn
CMD ["sh", "-c", "nginx && exec su appuser -c 'uvicorn main:app --host 127.0.0.1 --port 8000'"] CMD ["sh", "-c", "nginx && exec su appuser -c 'uvicorn main:app --host 127.0.0.1 --port 8000'"]
# --- Remote: lightweight, delegates to Docling Serve ---
FROM base AS remote
ENV CONVERSION_ENGINE=remote
# --- Local: full Docling in-process ---
FROM base AS local
RUN apt-get update && apt-get install -y --no-install-recommends \
libgl1 \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
COPY document-parser/requirements-local.txt .
RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu \
&& pip install --no-cache-dir -r requirements-local.txt
RUN chown -R appuser:appuser /app \
&& chown -R appuser:appuser /usr/local/lib/python3.12/site-packages/rapidocr/models
ENV CONVERSION_ENGINE=local

View file

@ -85,22 +85,42 @@ frontend/src/
## Quick Start ## Quick Start
### Docker (fastest) Docling Studio ships two Docker image variants:
| Variant | Image tag | Size | Description |
|---------|-----------|------|-------------|
| **remote** | `latest-remote` | ~270 MB | Lightweight — delegates to an external [Docling Serve](https://github.com/DS4SD/docling-serve) instance |
| **local** | `latest-local` | ~1.9 GB | Full — runs Docling in-process, CPU-only (downloads ML models on first run) |
### Docker — remote mode (fastest)
```bash ```bash
docker run -p 3000:3000 ghcr.io/scub-france/docling-studio:latest docker run -p 3000:3000 \
-e DOCLING_SERVE_URL=http://your-docling-serve:5001 \
ghcr.io/scub-france/docling-studio:latest-remote
``` ```
Open [http://localhost:3000](http://localhost:3000) ### Docker — local mode (self-contained)
```bash
docker run -p 3000:3000 ghcr.io/scub-france/docling-studio:latest-local
```
> **Note:** The first analysis takes longer as Docling downloads its ML models (~400 MB). Subsequent runs are fast. > **Note:** The first analysis takes longer as Docling downloads its ML models (~400 MB). Subsequent runs are fast.
Open [http://localhost:3000](http://localhost:3000)
### Docker Compose (for development) ### Docker Compose (for development)
```bash ```bash
git clone https://github.com/scub-france/Docling-Studio.git git clone https://github.com/scub-france/Docling-Studio.git
cd Docling-Studio cd Docling-Studio
# Local mode (default)
docker compose up --build docker compose up --build
# Remote mode
CONVERSION_MODE=remote DOCLING_SERVE_URL=http://your-docling-serve:5001 docker compose up --build
``` ```
### Local Development ### Local Development
@ -109,7 +129,13 @@ docker compose up --build
```bash ```bash
cd document-parser cd document-parser
python -m venv .venv && source .venv/bin/activate python -m venv .venv && source .venv/bin/activate
# Remote mode (lightweight)
pip install -r requirements.txt pip install -r requirements.txt
# Local mode (with Docling)
pip install -r requirements-local.txt
uvicorn main:app --reload --port 8000 uvicorn main:app --reload --port 8000
``` ```
@ -156,6 +182,9 @@ All configuration is done via environment variables. See [`.env.example`](.env.e
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `CONVERSION_ENGINE` | `local` | `local` (in-process Docling) or `remote` (Docling Serve) |
| `DOCLING_SERVE_URL` | `http://localhost:5001` | Docling Serve endpoint (remote mode only) |
| `DOCLING_SERVE_API_KEY` | — | API key for Docling Serve (optional) |
| `CORS_ORIGINS` | `http://localhost:3000,...` | CORS allowed origins (comma-separated) | | `CORS_ORIGINS` | `http://localhost:3000,...` | CORS allowed origins (comma-separated) |
| `UPLOAD_DIR` | `./uploads` | File storage directory | | `UPLOAD_DIR` | `./uploads` | File storage directory |
| `DB_PATH` | `./data/docling_studio.db` | SQLite database path | | `DB_PATH` | `./data/docling_studio.db` | SQLite database path |
@ -168,7 +197,7 @@ GitHub Actions pipelines (see [`.github/workflows/`](.github/workflows/)):
| Workflow | Trigger | What it does | | Workflow | Trigger | What it does |
|----------|---------|--------------| |----------|---------|--------------|
| **CI** | push to `main`, pull requests | Lint + type check + Backend tests + Frontend tests + build | | **CI** | push to `main`, pull requests | Lint + type check + Backend tests + Frontend tests + build |
| **Release** | push tag `v*` | Build & push multi-arch Docker image to `ghcr.io` | | **Release** | push tag `v*` | Build & push **two** multi-arch Docker images (`remote` + `local`) to `ghcr.io` |
| **Docs** | push to `main` (docs changes) | Build & deploy MkDocs to GitHub Pages | | **Docs** | push to `main` (docs changes) | Build & deploy MkDocs to GitHub Pages |
We follow [Semantic Versioning](https://semver.org/) with a simplified Git Flow. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full release process. We follow [Semantic Versioning](https://semver.org/) with a simplified Git Flow. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full release process.
@ -183,12 +212,11 @@ We follow [Semantic Versioning](https://semver.org/) with a simplified Git Flow.
### Docker Desktop settings ### Docker Desktop settings
The document parser needs **at least 4 GB of RAM**: | | Remote image | Local image |
|---|---|---|
| Resource | Minimum | Recommended | | **Image size** | ~270 MB | ~1.9 GB |
|----------|---------|-------------| | **Memory** | 2 GB | 6 GB (recommended 8 GB+) |
| Memory | 6 GB | 8 GB+ | | **CPUs** | 2 | 4 (recommended 8+) |
| CPUs | 4 | 8+ |
### Platform support ### Platform support

View file

@ -2,6 +2,7 @@ services:
document-parser: document-parser:
build: build:
context: ./document-parser context: ./document-parser
target: ${CONVERSION_MODE:-local}
expose: expose:
- "8000" - "8000"
volumes: volumes:
@ -9,6 +10,8 @@ services:
- db_data:/app/data - db_data:/app/data
environment: environment:
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:5173} CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:5173}
DOCLING_SERVE_URL: ${DOCLING_SERVE_URL:-}
DOCLING_SERVE_API_KEY: ${DOCLING_SERVE_API_KEY:-}
deploy: deploy:
resources: resources:
limits: limits:

View file

@ -20,7 +20,13 @@
```bash ```bash
cd document-parser cd document-parser
python -m venv .venv && source .venv/bin/activate python -m venv .venv && source .venv/bin/activate
# Remote mode (lightweight — delegates to Docling Serve)
pip install -r requirements.txt pip install -r requirements.txt
# Local mode (full — runs Docling in-process)
pip install -r requirements-local.txt
pip install ruff pytest pytest-asyncio httpx pip install ruff pytest pytest-asyncio httpx
uvicorn main:app --reload --port 8000 uvicorn main:app --reload --port 8000
``` ```

View file

@ -1,33 +1,67 @@
# Getting Started # Getting Started
## Docker Compose (recommended) Docling Studio ships two Docker image variants:
| Variant | Image tag | Size | Description |
|---------|-----------|------|-------------|
| **remote** | `latest-remote` | ~270 MB | Lightweight — delegates to an external [Docling Serve](https://github.com/DS4SD/docling-serve) instance |
| **local** | `latest-local` | ~1.9 GB | Full — runs Docling in-process, CPU-only (downloads ML models on first run) |
## Docker — remote mode (fastest)
```bash
docker run -p 3000:3000 \
-e DOCLING_SERVE_URL=http://your-docling-serve:5001 \
ghcr.io/scub-france/docling-studio:latest-remote
```
## Docker — local mode (self-contained)
```bash
docker run -p 3000:3000 ghcr.io/scub-france/docling-studio:latest-local
```
> **Note:** The first analysis takes longer as Docling downloads its ML models (~400 MB). Subsequent runs are fast.
Open [http://localhost:3000](http://localhost:3000).
## Docker Compose (recommended for development)
```bash ```bash
git clone https://github.com/scub-france/Docling-Studio.git git clone https://github.com/scub-france/Docling-Studio.git
cd Docling-Studio cd Docling-Studio
docker compose up --build
```
Open [http://localhost:3000](http://localhost:3000). # Local mode (default)
docker compose up --build
# Remote mode
CONVERSION_MODE=remote DOCLING_SERVE_URL=http://your-docling-serve:5001 docker compose up --build
```
## Local Development ## Local Development
### Backend (Python 3.12+) === "Backend (Python 3.12+)"
```bash ```bash
cd document-parser cd document-parser
python -m venv .venv && source .venv/bin/activate python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn main:app --reload --port 8000
```
### Frontend (Node 20+) # Remote mode (lightweight)
pip install -r requirements.txt
```bash # Local mode (with Docling)
cd frontend pip install -r requirements-local.txt
npm install
npm run dev uvicorn main:app --reload --port 8000
``` ```
=== "Frontend (Node 20+)"
```bash
cd frontend
npm install
npm run dev
```
The frontend runs on `http://localhost:3000` and proxies API calls to `http://localhost:8000`. The frontend runs on `http://localhost:3000` and proxies API calls to `http://localhost:8000`.
@ -71,6 +105,9 @@ All configuration is done via environment variables:
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `CONVERSION_ENGINE` | `local` | `local` (in-process Docling) or `remote` (Docling Serve) |
| `DOCLING_SERVE_URL` | `http://localhost:5001` | Docling Serve endpoint (remote mode only) |
| `DOCLING_SERVE_API_KEY` | — | API key for Docling Serve (optional) |
| `CORS_ORIGINS` | `http://localhost:3000,...` | CORS allowed origins | | `CORS_ORIGINS` | `http://localhost:3000,...` | CORS allowed origins |
| `UPLOAD_DIR` | `./uploads` | File storage directory | | `UPLOAD_DIR` | `./uploads` | File storage directory |
| `DB_PATH` | `./data/docling_studio.db` | SQLite database path | | `DB_PATH` | `./data/docling_studio.db` | SQLite database path |
@ -78,9 +115,10 @@ All configuration is done via environment variables:
## System Requirements ## System Requirements
| Resource | Minimum | Recommended | | | Remote image | Local image |
|----------|---------|-------------| |---|---|---|
| Memory | 6 GB | 8 GB+ | | **Image size** | ~270 MB | ~1.9 GB |
| CPUs | 4 | 8+ | | **Memory** | 2 GB | 6 GB (recommended 8 GB+) |
| **CPUs** | 2 | 4 (recommended 8+) |
All Docker images are multi-arch (`linux/amd64` + `linux/arm64`). No GPU required. All Docker images are multi-arch (`linux/amd64` + `linux/arm64`). No GPU required.

View file

@ -1,9 +1,17 @@
FROM python:3.12-slim # syntax=docker/dockerfile:1
# =============================================================================
# Docling Studio — backend image (multi-target: remote / local)
#
# Usage:
# docker build --target remote -t docling-studio-backend:remote .
# docker build --target local -t docling-studio-backend:local .
# =============================================================================
# --- Base: common deps for both targets ---
FROM python:3.12-slim AS base
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
poppler-utils \ poppler-utils \
libgl1 \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
@ -24,3 +32,25 @@ ENV DB_PATH=/app/data/docling_studio.db
USER appuser USER appuser
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# --- Remote: lightweight, delegates to Docling Serve ---
FROM base AS remote
ENV CONVERSION_ENGINE=remote
# --- Local: full Docling in-process ---
FROM base AS local
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
libgl1 \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
COPY requirements-local.txt .
RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu \
&& pip install --no-cache-dir -r requirements-local.txt
RUN chown -R appuser:appuser /app \
&& chown -R appuser:appuser /usr/local/lib/python3.12/site-packages/rapidocr/models
USER appuser
ENV CONVERSION_ENGINE=local

View file

@ -103,7 +103,7 @@ app.include_router(documents_router)
app.include_router(analyses_router) app.include_router(analyses_router)
@app.get("/health") @app.get("/api/health")
def health(): def health():
"""Health check endpoint.""" """Health check endpoint."""
return { return {

View file

@ -0,0 +1,3 @@
-r requirements.txt
docling>=2.80.0,<3.0.0
docling-core>=2.0.0,<3.0.0

View file

@ -1,5 +1,3 @@
docling>=2.80.0,<3.0.0
docling-core>=2.0.0,<3.0.0
fastapi>=0.115.0,<1.0.0 fastapi>=0.115.0,<1.0.0
uvicorn[standard]>=0.32.0,<1.0.0 uvicorn[standard]>=0.32.0,<1.0.0
python-multipart>=0.0.12 python-multipart>=0.0.12

View file

@ -26,7 +26,7 @@ def mock_analysis_service(client):
class TestHealthEndpoint: class TestHealthEndpoint:
def test_health(self, client): def test_health(self, client):
resp = client.get("/health") resp = client.get("/api/health")
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
assert data["status"] == "ok" assert data["status"] == "ok"

View file

@ -44,7 +44,7 @@ export const useFeatureFlagStore = defineStore('feature-flags', () => {
async function load(): Promise<void> { async function load(): Promise<void> {
try { try {
const data = await apiFetch<HealthResponse>('/health') const data = await apiFetch<HealthResponse>('/api/health')
engine.value = data.engine engine.value = data.engine
loaded.value = true loaded.value = true
error.value = null error.value = null