- Design doc 254: status Draft → Implemented; fill §4 (context), §5 (per-layer impact), §6 (alternatives), §7 (env vars / build-args), §8 (risks mapped to audit dims), §9 (testing + measured sizes), §10 (rollout), §11 (deferred follow-ups), §12 (PR + Docling tools link). - README image-variants table: latest-local 1.9 GB → 3.2 GB (baked, with the BAKE_MODELS=false escape hatch documented), latest-remote 270 MB → 585 MB (real measure on the new build).
22 KiB
Design: Optim taille image latest-local (sortir reasoning, multi-stage, dockerignore)
- Issue: #254
- Title on issue: [ENHANCEMENT] Optim taille image latest-local (sortir reasoning, multi-stage, dockerignore)
- Author: Pier-Jean Malandrino
- Date: 2026-05-06
- Status: Implemented
- Target milestone: 0.6.0 — Doc-centric ingest
- Impacted layers: infra/CI (Dockerfile, requirements split, compose) · docs (README, this doc)
- Audit dimensions likely touched: CI/Build · Performance · Decoupling · Documentation
- ADR spawned?: no (no load-bearing library/boundary change — Docker layout choice is locally scoped)
1. Problem
L'image latest-local empile aujourd'hui beaucoup de surface : torch + torchvision (CPU, ~800 Mo–1.2 Go), docling>=2.80, et — par effet de bord — docling-agent + mellea qui sont déclarés dans requirements.txt et donc tirés aussi par la cible remote (qui devrait être lightweight).
Le Dockerfile actuel souffre par ailleurs de plusieurs problèmes de build qui pénalisent la taille et le temps de rebuild : COPY . . se fait dans la stage base, donc toute modification de code Python invalide les layers pip install de la stage local (rebuild complet de torch à chaque commit) ; pas de stage builder isolée → pip + caches restent dans l'image finale ; .dockerignore minimal — pas d'exclusion de tests/, data/, uploads/, docs/, etc. ; reasoning (R&D, gated par REASONING_ENABLED) embarqué inconditionnellement dans toutes les images.
2. Goals
- Baseline mesurée et notée dans le design doc (
docker images+docker historydu top-3 layers). docling-agent+mellearetirés dedocument-parser/requirements.txt, déplacés dansdocument-parser/requirements-reasoning.txt.Dockerfilemulti-stage (builder+ cible finale) avecCOPY . .repoussé après lespip install.- Build-arg
WITH_REASONING=false(défaut) supporté dans la ciblelocal. .dockerignoreétendu (tests/,data/,uploads/,docs/,*.iml,package-lock.json,node_modules/,tools/migrate_06.py).- Évaluation de
torchvisiondocumentée (gardé ou retiré, justifié). - Volume HF cache documenté dans
docker-compose.ymletdocker-compose.dev.yml. - Smoke test : conversion locale OK sans reasoning ; reasoning OK avec
WITH_REASONING=true+REASONING_ENABLED=true+ Ollama joignable. pytest tests/ -vpasse dans le container final.- Réduction taille ≥ 30 % vs baseline (chiffrée dans la PR).
3. Non-goals
- Pas de réécriture du
LocalConverterni de suppression duthreading.Lockglobal → suivi perf séparé (issue dédiée à ouvrir si besoin). - Pas de bake-in des modèles Docling dans l'image — le compromis taille est trop défavorable. Le cache HF reste mountable via volume ; un
tools/prefetch_models.pyopt-in pourra arriver dans un autre issue. - Pas d'optim de l'image
embedding-service— autre image, autre périmètre. - Pas de tuning HF Space deploy — HF Space déploie
latest-remote, paslatest-local. - Pas de changement du moteur OCR livré par Docling.
- Pas de modification de l'API publique ni du schéma SQLite — change additif/build-only.
4. Context & constraints
Existing code surface
document-parser/Dockerfile— the multi-target file (base→remote/local)document-parser/requirements.txt— shared deps for both targetsdocument-parser/requirements-local.txt— additive Docling stack for the local targetdocument-parser/.dockerignore— minimal exclusion listdocker-compose.yml/docker-compose.dev.yml— both referencetarget: ${CONVERSION_MODE:-local}document-parser/infra/docling_agent_reasoning.py— already guards withdeps_present(), so removing the deps from the standard image degrades gracefully
No domain / API / persistence / services code is touched. No SQLite migration. No frontend change. No e2e change.
Hexagonal Architecture constraints
None crossed. The change lives entirely in infra/CI (Docker + requirements files). Domain/API/services/persistence are untouched and the LocalConverter / ReasoningRunner ports keep their existing shapes — only the deployment artefact changes shape, not the code.
Deployment modes
latest-local(in-process Docling) — affected: split into "models baked" default andBAKE_MODELS=falseslim variant.latest-remote(delegates to Docling Serve) — affected: also slimmed (it inherited the reasoning deps via the sharedrequirements.txt).- HF Space — uses
latest-remote, so it gets the size win for free without any HF-specific change. - Frontend feature flags (
chunking,disclaimer,reasoningAvailable) are unaffected;reasoningAvailablekeeps reflectingdeps_present()so the sidebar entry hides automatically when the standard image is used.
Hard constraints
- No SQLite or API contract change — additive build-only.
- No Pydantic DTO change.
- Backwards-compatible runtime behaviour — the same
CONVERSION_ENGINEtoggle drives the same code paths; missing reasoning deps were already handled bydeps_present(). - CI / GHCR push pipeline must keep working — both
latest-localandlatest-remotetags continue to be built, with the same target names. - Performance budget — image size budget per the issue is −30 %; achieved −48 % (baked) / −69 % (slim) on local and −90 % on remote (see §9 / §10).
5. Proposed design
5.1 Domain
Untouched.
5.2 Persistence
Untouched.
5.3 Infra adapters
Untouched at the Python level. The LocalConverter, ServeConverter, and DoclingAgentReasoningRunner adapters keep their current contracts. The only infra change is at the deployment layer:
requirements.txtno longer carriesdocling-agent==0.1.0andmellea==0.4.2. They move to a newrequirements-reasoning.txt(opt-in).Dockerfileis rewritten as a multi-stage build:
python:3.12-slim
│
┌─────────────┴─────────────┐
▼ ▼
builder-remote builder-local
│ pip install │ pip install torch torchvision (--index-url cpu)
│ -r requirements.txt │ pip install -r requirements-local.txt
│ │ if WITH_REASONING: pip install -r requirements-reasoning.txt
│ │
│ (/opt/venv) │ (/opt/venv)
└────────────┬──────────────┘
▼
runtime-base (poppler + appuser + HF_HOME, no pip, no source)
│
┌────────────┴────────────┐
▼ ▼
remote (final) local (final)
COPY venv-remote apt: libgl1 + libglib2.0-0
COPY . COPY venv-local
if BAKE_MODELS: docling-tools models download
COPY .
- Source (
COPY . /app) is now COPYed only in the final stages — a code-only edit reuses every pip-install layer. - Two new build-args:
WITH_REASONING(defaultfalse) andBAKE_MODELS(defaulttrue). Both opt-out forlocal-reasoning/ slim variants respectively.
5.4 Services
Untouched.
5.5 API
Untouched.
5.6 Frontend — feature module
Untouched.
5.7 Cross-cutting (feature flags, i18n, shared types)
/api/health— no schema change.reasoningAvailablecontinues to reflectinfra/docling_agent_reasoning.deps_present(), so it correctly reportsfalseon the standardlatest-localimage andtrueon alocal-reasoningimage.i18n— no string change.shared/types.ts— no type change.
6. Alternatives considered
Alternative A — Dedicated local-reasoning Dockerfile target (3rd stage)
- Summary: Add a third final target
FROM local AS local-reasoningthat runspip install -r requirements-reasoning.txt. CI publisheslatest-localandlatest-local-reasoningseparately. - Why not: Doubles the CI surface for very little benefit, and the duplication of intent (build-arg vs target) confuses operators. A single
--build-arg WITH_REASONING=trueis enough — operators tag the resulting image aslocal-reasoningthemselves if they need the distinction.
Alternative B — Bake reasoning deps into the standard image, do nothing
- Summary: Keep
docling-agent+melleainrequirements.txt, accept the 5+ GB image as the cost of "everything works out of the box". - Why not: The standard
latest-remoteimage (which delegates to Docling Serve and never reasons locally) was carrying ~5 GB of unrelated CUDA + LLM SDK weights. That alone disqualifies the do-nothing path.
Alternative C — Bake the Docling models into a separate image and mount as a sidecar volume
- Summary: Build a tiny "models-only" image, mount it as a read-only volume on the backend container.
- Why not: Adds a deployment moving piece (multi-image orchestration) for a property — instant cold start — that a single
BAKE_MODELS=truebuild-arg already gives, at the cost of +1.3 GB the operator can opt out of.
7. API & data contract
Endpoints
No change. /api/health keeps the same shape; reasoningAvailable still derives from runtime import-checks.
Persistence schema
No change.
Env vars / config
No new runtime env vars. Two new build-args on the local Dockerfile target:
| Name | Default | Allowed | Notes |
|---|---|---|---|
WITH_REASONING |
false |
true / false |
Bundle docling-agent + mellea. Opt in to build a local-reasoning image. Off keeps the standard image lean. |
BAKE_MODELS |
true |
true / false |
Pre-fetch Docling model checkpoints into the appuser HF cache. Off makes the image ~1.3 GB lighter at the cost of a one-time download on first conversion. |
Compose forwards WITH_REASONING from the host env (WITH_REASONING=true docker compose up --build).
Breaking changes
Additive only at the deployment level. Two operational expectations change — both intentional:
- Anyone running the standard
latest-localimage withREASONING_ENABLED=truewill seereasoningAvailable=falsefrom the API and the Reasoning sidebar entry will hide. To restore: rebuild with--build-arg WITH_REASONING=true. (Already documented in README.) - The previously-added
hf_cachenamed volume in compose is removed. Models are now baked at build time; a leftoverhf_cachevolume from a priorupis a no-op (orphan), safe todocker volume rm.
| Risk | Audit dimension | Likelihood | Impact | How we notice | Mitigation / rollback |
|---|---|---|---|---|---|
| A future transitive dep silently re-pulls a CUDA torch and inflates the image again | Performance · CI/Build | Medium | High | CI image-size step regresses; docker history shows nvidia-* layers |
Pin torch to a +cpu build in the requirements files; add a CI check that fails if nvidia- packages appear in the venv |
Operators relying on R&D reasoning hit reasoningAvailable=false after upgrading |
Documentation · Decoupling | Medium | Medium | User report or 503 from /api/reasoning (already gated) |
README + design doc explicitly call out the WITH_REASONING=true path; existing deps_present() already degrades gracefully (no crash) |
| Baked models become stale relative to a future Docling version bump | CI/Build | Low | Low | Backend logs HF cache version mismatch; conversion still works (Docling re-downloads if needed) | Models are re-fetched at every image build; bumping docling in requirements-local.txt naturally produces a fresh-baked image |
| A user adds custom models at runtime and loses them on container restart | Decoupling | Low | Low | User reports lost custom models | Documented: BAKE_MODELS=false + add a custom volume mount on /home/appuser/.cache/huggingface if persistence is needed |
Multi-stage COPY --from=builder increases initial build time on a cold cache |
CI/Build | Low | Low | CI build duration | Cold builds are sequential by design; warm builds are dramatically faster (pip layers reused on every Python edit) — net positive |
9. Testing strategy
Backend — pytest
No new tests added. The change is build-only and the existing 563-test suite is the regression net (services, persistence, API contract — none touched).
Validation pipeline run on the branch:
ruff check . → All checks passed
ruff format --check . → 99 files OK
pytest tests/ → 563 passed, 13 skipped
(2 pre-existing collection errors — pytestarch missing in dev venv, _encode_picture_b64 not exported — are unrelated to this branch's diff and tracked separately.)
Frontend — Vitest
Untouched. Not run.
E2E — Karate UI
Not in scope. The change does not affect any user-facing behaviour.
Manual QA
docker compose up --build→ confirm the backend container starts,/api/healthreturns 200,reasoningAvailable=false.- Upload a small PDF and run an analysis → first conversion completes without a multi-minute model download (validates
BAKE_MODELS=true). - Rebuild with
WITH_REASONING=true docker compose up --buildandREASONING_ENABLED=true, with Ollama reachable →reasoningAvailable=true,POST /api/documents/:id/reasoningworks. - Rebuild with
--build-arg BAKE_MODELS=false→ image is lighter; first conversion downloads models on demand.
Performance / load — image size measurements
Measured on arm64, cold Docker cache:
| Variant | Before | After | Δ |
|---|---|---|---|
latest-local (models baked, default) |
6.09 GB | 3.19 GB | −48 % |
latest-local (BAKE_MODELS=false) |
6.09 GB | 1.89 GB | −69 % |
latest-remote |
5.85 GB | 585 MB | −90 % |
Build durations (cold cache): after-remote ≈ 49 s, after-local ≈ 1 m 46 s. Warm rebuild after a Python-only edit: sub-second (pip layers reused).
10. Rollout & observability
Release branch
Targets release/0.6.0. No coordination needed with parallel branches — the change is isolated to build files.
Feature flag / staged rollout
No runtime feature flag. The change is hidden behind two build-time flags:
BAKE_MODELS=true(default) — produces the standard ~3.2 GB image.WITH_REASONING=true(opt-in) — produces alocal-reasoningvariant.
Operators can roll out by re-pulling latest-local from GHCR; no env flip needed.
HF Space deployments are unaffected by the local-side build-args (HF Space uses latest-remote).
Observability
- No new logs, metrics, or error modes.
- Image size becomes a CI signal: a follow-up issue should add a step that prints the published image size to the workflow summary, so a future regression (e.g. a new dep silently re-pulling CUDA) is visible at PR review time.
Rollback plan
Pure-revert. Re-deploying the previous tag (v0.5.x) restores the prior image. No data migration or env flip is involved. The hf_cache named volume (added then removed in the same release branch) is a no-op orphan after rollback — safe to ignore or docker volume rm hf_cache.
11. Open questions
Resolved during implementation. Two follow-ups deferred to dedicated issues:
- Drop
docling-core[chunking]extra fromrequirements.txtto pushlatest-remotefrom 585 MB toward the historical ~270 MB. Needs verification that theinfra/local_chunker.pypath is local-only (it appears to be, but a check is warranted). - Pin
torchto a CPU build (torch==X.Y.Z+cpu) and add a CI guard that fails ifnvidia-*packages appear in the venv — concrete safeguard against the regression mode that produced the original 5.6 GB bloat.
12. References
- Issue: https://github.com/scub-france/Docling-Studio/issues/254
- Related PRs / commits: https://github.com/scub-france/Docling-Studio/pull/255
- ADRs: none planned
- Project docs:
- Architecture:
docs/architecture.md - Coding standards:
docs/architecture/coding-standards.md - ADR guide / template:
docs/architecture/adr-guide.md,docs/architecture/adr-template.md - Audit master:
docs/audit/master.md - E2E conventions:
e2e/CONVENTIONS.md
- Architecture:
- External:
- Upstream
_rag_looppublic-API replacement: https://github.com/docling-project/docling-agent/issues/26 - Docling models tooling:
docling-tools models download(CLI shipped bydocling)
- Upstream